Compare commits

...

9 commits

Author SHA1 Message Date
33a0228c9c
0.3.4: bump release version
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-04-12 20:27:47 +03:00
d501843f06
Remove unused HASH_CALCULATION_SH static
It was used in early stages of this project to provide sha256 feature
flag before 0.3.0.

Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-03-17 18:17:58 +03:00
652196c247
0.3.3: Ununsafe binhost (server) and update deps
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-03-17 18:13:10 +03:00
debff608d2
Remove runner from workspace
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-02-21 20:15:36 +03:00
b082b645f0
web.sh: fixing shellcheck warnings
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-02-18 13:10:49 +03:00
beb4b46caf
web.sh: add alternative shasum command (perl)
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-02-18 10:14:23 +03:00
c31f86b104
0.3.2: display manifest hashsum on index
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-02-04 07:28:53 +03:00
bcb4fed2f4
0.3.1: ARGS for args and back to sh
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-02-03 22:31:40 +03:00
cae10cc45e
Fix missing "Request"
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
2024-02-01 20:56:28 +03:00
7 changed files with 178 additions and 152 deletions

4
API.md
View file

@ -42,12 +42,16 @@ GET /bin/<BIN>/<PLATFORM>/<ARCH>/sign HTTP/1.1
Manifest is a file, that contains ED25519 public key and SHA256sums list
#### Request
```http request
GET /runner/manifest HTTP/1.1
```
### Get binary runner
#### Request
```http request
GET /runner/<runner> HTTP/1.1
```

View file

@ -1,28 +1,20 @@
workspace = { members = [ "runner" ] }
[package]
name = "binhost"
version = "0.3.0"
version = "0.3.4"
edition = "2021"
license = "MIT"
repository = "https://github.com/ivabus/binhost"
description = "HTTP server to easily serve files"
[dependencies]
clap = { version = "4.4.18", features = ["derive"] }
clap = { version = "4.5.3", features = ["derive"] }
rocket = "0.5.0"
serde = { version = "1.0", features = ["derive"] }
sha2 = { version = "0.10"}
ed25519-compact = { version = "2.0.6", default-features = false, features = [ "random", "self-verify", "std"] }
# Reducing size as much as possible
[profile.release]
strip = true
opt-level = "s"
lto = true
panic = "abort"
codegen-units = 1
[profile.dev]
# Doesn't build on my machine without
opt-level = 1
sha2 = { version = "0.10" }
ed25519-compact = { version = "2.1.1", default-features = false, features = [
"random",
"self-verify",
"std",
] }
once_cell = "1.19"

View file

@ -53,17 +53,19 @@ runners
Manifest validity check provides a fully-secured binary distribution chain.
```shell
curl ADDRESS:PORT/<bin> | KEY=... bash
curl ADDRESS:PORT/<bin> | KEY=... sh
```
`KEY` first few symbols from hex representation of SHA256 sum of manifest (printed to stdout on `binhost` startup).
Additional arguments are set with `ARGS` environment variable
Only this option should be considered as secure.
### Execute specific binary <bin> without validity check
```shell
curl ADDRESS:PORT/<bin> | bash
curl ADDRESS:PORT/<bin> | sh
```
### Download and reuse script

View file

@ -1,11 +1,21 @@
[package]
name = "runner"
version = "0.1.0"
version = "0.3.4"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libc = { version = "0.2.152", default-features = false }
ed25519-compact = { version = "2.0.6", default-features = false }
numtoa = "0.2.4"
numtoa = "0.2.4"
# Reducing size as much as possible
[profile.release]
strip = true
opt-level = "s"
lto = "fat"
panic = "abort"
codegen-units = 1
[profile.dev]
# Doesn't build on my machine without
opt-level = 1

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// "Runner" is written in no_std Rust for the smaller executable size: ~49KiB (Darwin arm64) vs ~300KiB
// "Runner" is written in no_std Rust for the smaller executable size: ~49KiB (Darwin arm64)
#![no_main]
#![no_std]

View file

@ -1,5 +1,7 @@
// SPDX-License-Identifier: MIT
#![forbid(unsafe_code)]
#[macro_use]
extern crate rocket;
@ -8,10 +10,12 @@ use std::time::Instant;
use clap::Parser;
use ed25519_compact::{KeyPair, Noise};
use once_cell::sync::Lazy;
use rocket::figment::Figment;
use rocket::fs::{FileServer, NamedFile};
use rocket::http::Status;
use rocket::response::content::RawText;
use rocket::tokio::sync::RwLock;
use sha2::digest::FixedOutput;
use sha2::Digest;
@ -19,17 +23,60 @@ use structs::*;
mod structs;
static mut BINS: Option<(HashMap<String, Bin>, Instant)> = None;
static mut MANIFEST: Option<Vec<u8>> = None;
static mut KEYPAIR: Option<ed25519_compact::KeyPair> = None;
static BINS: Lazy<RwLock<(HashMap<String, Bin>, Instant)>> =
Lazy::new(|| RwLock::new((get_bins(&Args::parse()), Instant::now())));
static KEYPAIR: Lazy<KeyPair> = Lazy::new(|| {
println!("Generating keypair");
let kp = KeyPair::generate();
println!(
"Keypair generated. Public key: {}",
kp.pk.iter().map(|x| format!("{:x}", x)).collect::<Vec<String>>().join("")
);
kp
});
static MANIFEST: Lazy<Vec<u8>> = Lazy::new(|| {
let args = Args::parse();
println!("Generating manifest");
let mut manifest: Vec<u8> = Vec::new();
let mut bin_pub_key: Vec<u8> = KEYPAIR.pk.to_vec();
manifest.append(&mut bin_pub_key);
let mut runners = 0;
for element in std::fs::read_dir(args.runners_dir).unwrap() {
let en = element.unwrap();
if en.path().is_file() {
let mut hasher = sha2::Sha256::new();
hasher.update(std::fs::read(en.path()).unwrap().as_slice());
let mut contents = Vec::from(
format!(
"{:x} {}\n",
hasher.finalize_fixed(),
en.path().file_name().unwrap().to_str().unwrap()
)
.as_bytes(),
);
runners += 1;
manifest.append(&mut contents);
}
}
let mut hasher = sha2::Sha256::new();
hasher.update(&manifest);
println!(
"Manifest generated with {} runners and SHA256: {:x}",
runners,
hasher.finalize_fixed()
);
manifest
});
static WEB_SH: &str = include_str!("../web.sh");
static HASH_CALCULATION_SH: &str = "";
fn reload_bins(bins: (&mut HashMap<String, Bin>, &mut Instant), args: &Args) {
if (Instant::now() - *bins.1).as_secs() > args.refresh {
*bins.0 = get_bins(args);
*bins.1 = Instant::now();
async fn reload_bins(args: &Args) {
let (bins, time) = &mut *BINS.write().await;
if (Instant::now() - *time).as_secs() > args.refresh {
*bins = get_bins(args);
*time = Instant::now();
}
}
@ -71,72 +118,63 @@ fn format_platform_list(bin: &Bin) -> String {
async fn index() -> RawText<String> {
let args = Args::parse();
let mut ret = String::new();
unsafe {
if let Some((bins, time)) = &mut BINS {
reload_bins((bins, time), &args);
if bins.is_empty() {
return RawText(String::from("No binaries found"));
}
for (name, bin) in bins {
ret.push_str(&format!(
"- {} (platforms: {:?})\n",
name,
bin.platforms
.iter()
.map(|plat| format!("{}-{}", plat.system, plat.arch))
.collect::<Vec<String>>()
))
}
}
let mut hasher = sha2::Sha256::new();
hasher.update(&*MANIFEST);
ret.push_str(&format!("Manifest hashsum: {:x}\n", hasher.finalize_fixed()));
reload_bins(&args).await;
let (bins, _) = &*BINS.read().await;
if bins.is_empty() {
return RawText(String::from("No binaries found"));
}
for (name, bin) in bins {
ret.push_str(&format!(
"- {} (platforms: {:?})\n",
name,
bin.platforms
.iter()
.map(|plat| format!("{}-{}", plat.system, plat.arch))
.collect::<Vec<String>>()
))
}
RawText(ret)
}
#[get("/runner/manifest")]
async fn get_manifest() -> Vec<u8> {
unsafe {
match &MANIFEST {
Some(man) => man.clone(),
_ => unreachable!(),
}
}
async fn get_manifest<'a>() -> Vec<u8> {
let manifest = &*MANIFEST;
manifest.clone()
}
#[get("/<bin>")]
async fn get_script(bin: &str) -> ScriptResponse {
let args = Args::parse();
unsafe {
if let Some((bins, time)) = &mut BINS {
reload_bins((bins, time), &args);
return match bins.get(bin) {
None => ScriptResponse::Status(Status::NotFound),
Some(bin) => {
let mut script = String::from(WEB_SH);
script = script
.replace("{{HASH_CALCULATION}}", HASH_CALCULATION_SH)
.replace("{{NAME}}", &bin.name)
.replace("{{PLATFORM_LIST}}", &format_platform_list(bin))
.replace("{{EXTERNAL_ADDRESS}}", &args.url);
ScriptResponse::Text(RawText(script))
}
};
reload_bins(&args).await;
let (bins, _) = &*BINS.read().await;
match bins.get(bin) {
None => ScriptResponse::Status(Status::NotFound),
Some(bin) => {
let mut script = String::from(WEB_SH);
script = script
.replace("{{NAME}}", &bin.name)
.replace("{{PLATFORM_LIST}}", &format_platform_list(bin))
.replace("{{EXTERNAL_ADDRESS}}", &args.url);
ScriptResponse::Text(RawText(script))
}
}
ScriptResponse::Status(Status::NotFound)
}
#[get("/<bin>/platforms")]
async fn get_platforms(bin: &str) -> ScriptResponse {
let args = Args::parse();
unsafe {
if let Some((bins, time)) = &mut BINS {
reload_bins((bins, time), &args);
return match bins.get(bin) {
None => ScriptResponse::Status(Status::NotFound),
Some(bin) => ScriptResponse::Text(RawText(format_platform_list(bin))),
};
}
reload_bins(&args).await;
let (bins, _) = &*BINS.read().await;
match bins.get(bin) {
None => ScriptResponse::Status(Status::NotFound),
Some(bin) => ScriptResponse::Text(RawText(format_platform_list(bin))),
}
ScriptResponse::Status(Status::NotFound)
}
#[get("/bin/<bin>/<platform>/<arch>")]
@ -171,12 +209,7 @@ async fn get_binary_sign(bin: &str, platform: &str, arch: &str) -> SignResponse
Ok(f) => f,
Err(_) => return SignResponse::Status(Status::BadRequest),
};
let keypair = unsafe {
match &KEYPAIR {
Some(pair) => pair,
_ => unreachable!(),
}
};
let keypair = &*KEYPAIR;
SignResponse::Bin(keypair.sk.sign(file.as_slice(), Some(Noise::generate())).as_slice().to_vec())
}
#[launch]
@ -187,47 +220,8 @@ async fn rocket() -> _ {
std::process::exit(1);
}
unsafe {
BINS = Some((get_bins(&args), Instant::now()));
println!("Generating keypair");
let kp = KeyPair::generate();
KEYPAIR = Some(kp.clone());
println!(
"Keypair generated. Public key: {}",
&kp.pk.iter().map(|x| format!("{:x}", x)).collect::<Vec<String>>().join("")
);
println!("Generating manifest");
let mut manifest: Vec<u8> = Vec::new();
let mut bin_pub_key: Vec<u8> = kp.pk.to_vec();
manifest.append(&mut bin_pub_key);
let mut runners = 0;
for element in std::fs::read_dir(args.runners_dir).unwrap() {
let en = element.unwrap();
if en.path().is_file() {
let mut hasher = sha2::Sha256::new();
hasher.update(std::fs::read(en.path()).unwrap().as_slice());
let mut contents = Vec::from(
format!(
"{:x} {}\n",
hasher.finalize_fixed(),
en.path().file_name().unwrap().to_str().unwrap()
)
.as_bytes(),
);
runners += 1;
manifest.append(&mut contents);
}
}
let mut hasher = sha2::Sha256::new();
hasher.update(&manifest);
println!(
"Manifest generated with {} runners and SHA256: {:x}",
runners,
hasher.finalize_fixed()
);
MANIFEST = Some(manifest)
};
let _ = &*BINS.read().await;
let _ = &*MANIFEST;
let figment = Figment::from(rocket::Config::default())
.merge(("ident", "Binhost"))

70
web.sh Normal file → Executable file
View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/sh
# SPDX-License-Identifier: MIT
set -e
@ -12,16 +12,42 @@ fail() {
}
requireCommands() {
for cmd in $*; do
if ! command -v $cmd &> /dev/null; then
for cmd in "$@"; do
if ! command -v "$cmd" > /dev/null 2>&1; then
fail "Cannot find required command: $cmd"
fi
done
}
requireCommands uname sha256sum cut dd chmod rm realpath expr
requireCommands uname cut dd chmod rm realpath expr
if [ "$(realpath $(command -v sha256sum))" = "/bin/busybox" ]; then
# Finding alternative, but supported sha256sums
SHA256SUM=""
SHASUMFLAGS=""
PLATFORM="$(uname)"
ARCH="$(uname -m)"
if command -v sha256sum > /dev/null 2>&1; then
SHA256SUM="sha256sum"
SHASUMFLAGS="-c hashes --ignore-missing"
else
if command -v sha256 > /dev/null 2>&1; then
SHA256SUM="sha256"
SHASUMFLAGS="-C hashes runner-$PLATFORM-$ARCH"
fi
if command -v shasum > /dev/null 2>&1; then
SHASUMVER=$(shasum -v | cut -c 1)
if [ "$SHASUMVER" -ge 6 ]; then
SHA256SUM="shasum -a 256"
SHASUMFLAGS="-c hashes --ignore-missing"
fi
fi
fi
if [ SHA256SUM = "" ]; then
fail "Could not find suitable sha256sum executable"
fi
if [ "$(realpath "$SHA256SUM" 2> /dev/null)" = "/bin/busybox" ]; then
fail "Busybox sha256sum detected, will not work. Refusing to continue"
fi
@ -32,38 +58,36 @@ DOWNLOAD_COMMAND="curl"
OUTPUT_ARG="-o"
DIR="/tmp/binhost-$NAME-$(date +%s)"
FILE="$DIR/$NAME"
PLATFORM="$(uname)"
ARCH="$(uname -m)"
if ! command -v curl &> /dev/null; then
if ! command -v wget &> /dev/null; then
if ! command -v curl > /dev/null 2>&1; then
if ! command -v wget > /dev/null 2>&1; then
fail "No curl or wget found, install one and rerun the script"
fi
export DOWNLOAD_COMMAND="wget"
export OUTPUT_ARG="-O"
DOWNLOAD_COMMAND="wget"
OUTPUT_ARG="-O"
fi
PLATFORM_LIST="{{PLATFORM_LIST}}"
# Making script truly portable
if [ ! "{{NAME}}" = $NAME ]; then
print ":: Fetching platforms"
export PLATFORM_LIST=$($DOWNLOAD_COMMAND $EXTERNAL_ADDRESS/$NAME/platforms $OUTPUT_ARG /dev/stdout)
PLATFORM_LIST=$($DOWNLOAD_COMMAND $EXTERNAL_ADDRESS/$NAME/platforms $OUTPUT_ARG /dev/stdout)
fi
if ! expr "$PLATFORM_LIST" : "\(.*$(uname)-$(uname -m).*\)" > /dev/null; then
fail "Platform \"$(uname)-$(uname -m)\" is not supported"
fi
mkdir $DIR
cd $DIR
mkdir "$DIR"
cd "$DIR"
print ":: Downloading manifest"
$DOWNLOAD_COMMAND $EXTERNAL_ADDRESS/runner/manifest $OUTPUT_ARG manifest
MANIFEST_HASHSUM=$(sha256sum manifest)
MANIFEST_HASHSUM=$(cat manifest | $SHA256SUM)
if [ ! -z $KEY ]; then
if [ ! $KEY = "$(echo $MANIFEST_HASHSUM | cut -c 1-${#KEY})" ]; then
if [ -n "$KEY" ]; then
if [ ! "$KEY" = "$(echo "$MANIFEST_HASHSUM" | cut -c 1-${#KEY})" ]; then
fail "Invalid manifest hashsum"
fi
else
@ -71,16 +95,16 @@ else
fi
print ":: Downloading signature"
$DOWNLOAD_COMMAND $EXTERNAL_ADDRESS/bin/$NAME/$PLATFORM/$ARCH/sign $OUTPUT_ARG signature
$DOWNLOAD_COMMAND "$EXTERNAL_ADDRESS/bin/$NAME/$PLATFORM/$ARCH/sign" $OUTPUT_ARG signature
dd if=manifest of=public_key count=32 bs=1 2> /dev/null
dd if=manifest of=hashes skip=32 bs=1 2> /dev/null
print ":: Downloading runner"
$DOWNLOAD_COMMAND $EXTERNAL_ADDRESS/runner/runner-$PLATFORM-$ARCH $OUTPUT_ARG "runner-$PLATFORM-$ARCH"
$DOWNLOAD_COMMAND "$EXTERNAL_ADDRESS/runner/runner-$PLATFORM-$ARCH" $OUTPUT_ARG "runner-$PLATFORM-$ARCH"
if ! sha256sum -c hashes --ignore-missing; then
if ! $SHA256SUM $SHASUMFLAGS >&2 ; then
fail "Incorrect hashsum of runner"
fi
@ -88,15 +112,15 @@ chmod +x "runner-$PLATFORM-$ARCH"
print ":: Downloading binary"
$DOWNLOAD_COMMAND $EXTERNAL_ADDRESS/bin/$NAME/$PLATFORM/$ARCH $OUTPUT_ARG "$FILE"
$DOWNLOAD_COMMAND "$EXTERNAL_ADDRESS/bin/$NAME/$PLATFORM/$ARCH" $OUTPUT_ARG "$FILE"
if ! ./runner-$PLATFORM-$ARCH "$FILE" >&2; then
if ! "./runner-$PLATFORM-$ARCH" "$FILE" >&2; then
exit 2
fi
chmod +x "$FILE"
$FILE < /dev/tty
$FILE $ARGS < /dev/tty
cd