use anyhow::{Context, Result, anyhow, bail};
use base64::Engine;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
const HUB_PUBKEY_B64: &str = "PInrfQ8KL5Mngm+VZOkq2HkMMHnwyfmtkSD+ZfiIHx0=";
fn b64(s: &str) -> Result<Vec<u8>> {
base64::engine::general_purpose::STANDARD
.decode(s.trim())
.context("base64 decode")
}
fn canonical_payload(version: &str, skills: &str, plugins: &str, tools: &str) -> Vec<u8> {
format!("rsclaw-hub-meta\nv1\n{version}\n{skills}\n{plugins}\n{tools}\n").into_bytes()
}
pub fn verify_meta_sig(
version: &str,
skills: &str,
plugins: &str,
tools: &str,
sig_b64: &str,
) -> Result<()> {
if sig_b64.trim().is_empty() {
bail!("hub meta has no signature — refusing (fail-closed)");
}
let pk: [u8; 32] = b64(HUB_PUBKEY_B64)?
.try_into()
.map_err(|_| anyhow!("pinned HUB_PUBKEY is not 32 bytes"))?;
let vk = VerifyingKey::from_bytes(&pk).context("pinned HUB_PUBKEY is invalid")?;
let sig: [u8; 64] = b64(sig_b64)?
.try_into()
.map_err(|_| anyhow!("hub meta signature is not 64 bytes"))?;
let sig = Signature::from_bytes(&sig);
vk.verify(&canonical_payload(version, skills, plugins, tools), &sig)
.context("hub meta signature verification failed")
}
pub fn verify_signed_meta(meta_json: &str) -> Result<(String, String, String)> {
let v: serde_json::Value = serde_json::from_str(meta_json).context("parse meta.json")?;
let s = |p: &str| v.pointer(p).and_then(|x| x.as_str()).unwrap_or("");
let (version, skills, plugins, tools, sig) = (
s("/version"),
s("/sha256/skills"),
s("/sha256/plugins"),
s("/sha256/tools"),
s("/sig"),
);
verify_meta_sig(version, skills, plugins, tools, sig)?;
Ok((skills.to_owned(), plugins.to_owned(), tools.to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
const GOLD_SIG: &str =
"DGm3uiJwAeYeyi1km20QNFk8rrUD33QgAJYhgcuPlgF3JOXTUViZBDtkhys5D6wTfSvdBpgyAc7zWRuLdBCiCg==";
#[test]
fn verifies_golden_vector() {
verify_meta_sig("2026-01-01.000000", "AAAA", "BBBB", "CCCC", GOLD_SIG)
.expect("golden vector must verify");
}
#[test]
fn rejects_tampered_hash() {
assert!(verify_meta_sig("2026-01-01.000000", "AAAA", "BBBB", "XXXX", GOLD_SIG).is_err());
}
#[test]
fn rejects_tampered_version() {
assert!(verify_meta_sig("2026-01-01.000001", "AAAA", "BBBB", "CCCC", GOLD_SIG).is_err());
}
#[test]
fn rejects_empty_sig() {
assert!(verify_meta_sig("2026-01-01.000000", "AAAA", "BBBB", "CCCC", "").is_err());
}
#[test]
fn rejects_garbage_sig() {
assert!(
verify_meta_sig("2026-01-01.000000", "AAAA", "BBBB", "CCCC", "not-base64!!").is_err()
);
}
}