use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use clap::Args;
use kanade_shared::artifact::{ArtifactIdentity, ArtifactIntegrity, ArtifactSha256};
use kanade_shared::package_manifest::{
ManifestClaims, PublisherSigningKey, TrustedPublisherKey, sign_manifest, verify_offline,
};
use sha2::{Digest, Sha256};
const MAX_INPUT_BYTES: u64 = 64 * 1024;
#[derive(Args, Debug)]
pub struct SignArgs {
#[arg(long)]
pub identity: PathBuf,
#[arg(long)]
pub package_id: String,
#[arg(long)]
pub min_os_version: String,
#[arg(long = "capability")]
pub capabilities: Vec<String>,
#[arg(long)]
pub out: PathBuf,
#[arg(long)]
pub secret_stdin: bool,
pub artifact: PathBuf,
}
#[derive(Args, Debug)]
pub struct VerifyArgs {
#[arg(long)]
pub trusted_key: PathBuf,
#[arg(long)]
pub identity: PathBuf,
pub manifest: PathBuf,
pub artifact: PathBuf,
}
#[derive(Args, Debug)]
pub struct TrustArgs {
#[arg(long)]
pub secret_stdin: bool,
}
pub(super) fn read_small(path: &Path) -> Result<Vec<u8>> {
let mut buf = Vec::new();
std::fs::File::open(path)
.with_context(|| format!("open {}", path.display()))?
.take(MAX_INPUT_BYTES + 1)
.read_to_end(&mut buf)
.with_context(|| format!("read {}", path.display()))?;
if buf.len() as u64 > MAX_INPUT_BYTES {
bail!("{} exceeds {MAX_INPUT_BYTES} bytes", path.display());
}
Ok(buf)
}
fn read_identity(path: &Path) -> Result<ArtifactIdentity> {
serde_json::from_slice(&read_small(path)?).context("parse explicit artifact identity")
}
fn measure(path: &Path) -> Result<ArtifactIntegrity> {
let mut file = std::fs::File::open(path).with_context(|| format!("open {}", path.display()))?;
let mut hash = Sha256::new();
let mut size = 0u64;
let mut buffer = vec![0; 128 * 1024];
loop {
let n = file.read(&mut buffer).context("read artifact")?;
if n == 0 {
break;
}
size = size
.checked_add(n as u64)
.context("artifact size overflow")?;
hash.update(&buffer[..n]);
}
if size == 0 {
bail!("artifact file is empty");
}
Ok(ArtifactIntegrity {
sha256: ArtifactSha256::from_bytes(hash.finalize().into()),
size_bytes: size,
})
}
pub(super) fn read_signing_key(from_stdin: bool) -> Result<PublisherSigningKey> {
let mut secret =
super::login::read_password("Publisher private key (input hidden): ", from_stdin)?
.into_bytes();
let parsed = std::str::from_utf8(&secret)
.map_err(|_| anyhow::anyhow!("private key must be ASCII base64"))
.and_then(|s| PublisherSigningKey::from_seed_base64(s).map_err(Into::into));
secret.fill(0);
parsed
}
pub fn sign(args: SignArgs) -> Result<()> {
let identity = read_identity(&args.identity)?;
let mut caps = args.capabilities;
caps.sort();
let integrity = measure(&args.artifact)?;
let claims = ManifestClaims::new(
identity,
args.package_id,
args.min_os_version,
caps,
integrity,
)?;
let key = read_signing_key(args.secret_stdin)?;
write_signed(&args.out, claims, &key)
}
fn write_signed(out_path: &Path, claims: ManifestClaims, key: &PublisherSigningKey) -> Result<()> {
let signed = sign_manifest(claims, key);
let bytes = signed.to_canonical_bytes();
let mut out = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(out_path)
.with_context(|| format!("create {} (refusing to overwrite)", out_path.display()))?;
if let Err(e) = out.write_all(&bytes).and_then(|()| out.sync_all()) {
drop(out);
let _ = std::fs::remove_file(out_path);
return Err(e).with_context(|| format!("write {}", out_path.display()));
}
println!("Wrote signed manifest {}", out_path.display());
println!("publisher key_id: {}", signed.key_id().as_str());
println!(
"NOTE: signing does not publish anything and does not make the artifact trusted. \
Verifiers must hold this key's trust record independently (`app publisher-trust`)."
);
Ok(())
}
pub fn verify(args: VerifyArgs) -> Result<()> {
let trusted = TrustedPublisherKey::from_trust_json(&read_small(&args.trusted_key)?)?;
let expected = read_identity(&args.identity)?;
let manifest = read_small(&args.manifest)?;
let observed = measure(&args.artifact)?;
let ok = verify_offline(&manifest, &trusted, &expected, &observed)
.context("offline manifest verification FAILED")?;
let c = ok.claims();
println!("OFFLINE VERIFIED (signature + integrity only)");
println!("publisher key_id: {}", ok.key_id().as_str());
println!("package_id: {}", c.package_id());
println!("min_os_version: {}", c.min_os_version());
println!(
"capabilities: {}",
c.required_agent_capabilities().join(", ")
);
println!("size_bytes: {}", c.integrity().size_bytes);
println!(
"NOT CHECKED: current publisher authorization, freshness, revocation. \
This result is not deploy-ready status and grants no install privilege."
);
Ok(())
}
pub fn publisher_trust(args: TrustArgs) -> Result<()> {
let key = read_signing_key(args.secret_stdin)?;
std::io::stdout().write_all(&key.trusted_key().to_trust_json())?;
Ok(())
}