use std::collections::HashSet;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use ed25519_dalek::{Signature, VerifyingKey};
use hatchling_core::CapabilityName;
use crate::{Error, Manifest, SignedManifest};
#[derive(Debug, Clone)]
pub struct Admission {
manifest: SignedManifest,
granted: HashSet<CapabilityName>,
signer: [u8; 32],
}
impl Admission {
#[must_use]
pub const fn manifest(&self) -> &Manifest {
&self.manifest.manifest
}
#[must_use]
pub const fn signer(&self) -> &[u8; 32] {
&self.signer
}
#[must_use]
pub fn signer_fingerprint(&self) -> String {
use sha2::Digest as _;
use std::fmt::Write as _;
sha2::Sha256::digest(self.signer)
.iter()
.fold(String::with_capacity(64), |mut out, byte| {
let _ = write!(out, "{byte:02x}");
out
})
}
#[must_use]
pub const fn capabilities(&self) -> &HashSet<CapabilityName> {
&self.granted
}
pub fn check_call(&self, capability: &CapabilityName) -> Result<(), Error> {
if self.granted.contains(capability) {
Ok(())
} else {
Err(Error::UndeclaredCapability {
name: self.manifest.manifest.name.clone(),
capability: capability.clone(),
})
}
}
}
pub fn check_trust_root(root: &[u8; 32]) -> Result<(), Error> {
let key = VerifyingKey::from_bytes(root)
.map_err(|e| Error::InvalidTrustRoot(format!("malformed Ed25519 public key: {e}")))?;
if key.is_weak() {
return Err(Error::InvalidTrustRoot(String::from(
"weak (small-order) key: any signature could verify under it",
)));
}
Ok(())
}
pub fn verify_admission<S: std::hash::BuildHasher>(
signed: &SignedManifest,
trust_root: &[u8; 32],
user_grants: &HashSet<CapabilityName, S>,
) -> Result<Admission, Error> {
let manifest = &signed.manifest;
let sig_b64 = signed.signature.as_ref().ok_or_else(|| Error::Unsigned {
name: manifest.name.clone(),
})?;
let verifying_key = VerifyingKey::from_bytes(trust_root)
.map_err(|e| Error::InvalidTrustRoot(format!("malformed Ed25519 public key: {e}")))?;
let sig_bytes = BASE64.decode(sig_b64).map_err(|_| Error::BadSignature {
name: manifest.name.clone(),
})?;
let signature = Signature::from_slice(&sig_bytes).map_err(|_| Error::BadSignature {
name: manifest.name.clone(),
})?;
let message = manifest
.canonical_bytes()
.map_err(|_| Error::BadSignature {
name: manifest.name.clone(),
})?;
verifying_key
.verify_strict(&message, &signature)
.map_err(|_| Error::BadSignature {
name: manifest.name.clone(),
})?;
manifest.validate()?;
let excess: Vec<CapabilityName> = manifest
.capabilities
.iter()
.filter(|cap| !user_grants.contains(*cap))
.cloned()
.collect();
if !excess.is_empty() {
return Err(Error::OverPrivileged {
name: manifest.name.clone(),
excess,
});
}
let declared: HashSet<CapabilityName> = manifest.capabilities.iter().cloned().collect();
Ok(Admission {
manifest: signed.clone(),
granted: declared,
signer: *trust_root,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
fn test_keypair() -> (SigningKey, [u8; 32]) {
let secret = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
0x1d, 0x1e, 0x1f, 0x20,
];
let signing = SigningKey::from_bytes(&secret);
let verifying = signing.verifying_key().to_bytes();
(signing, verifying)
}
fn sign_manifest(manifest: &Manifest, signing: &SigningKey) -> SignedManifest {
let msg = manifest.canonical_bytes().unwrap();
let sig = signing.sign(&msg);
let sig_b64 = BASE64.encode(sig.to_bytes());
SignedManifest {
manifest: manifest.clone(),
signature: Some(sig_b64),
}
}
fn sample_manifest() -> Manifest {
Manifest {
name: String::from("test-skill"),
version: String::from("1.0.0"),
capabilities: vec![cap("net.egress")],
entry_point: String::from("main.py"),
}
}
fn cap(raw: &str) -> CapabilityName {
raw.parse().unwrap()
}
fn net_grants() -> HashSet<CapabilityName> {
HashSet::from([cap("net.egress")])
}
#[test]
fn valid_signature_admits() {
let (signing, verifying) = test_keypair();
let signed = sign_manifest(&sample_manifest(), &signing);
let admission = verify_admission(&signed, &verifying, &net_grants()).unwrap();
assert_eq!(admission.manifest().name, "test-skill");
assert_eq!(admission.capabilities(), &net_grants());
}
#[test]
fn an_admission_records_the_key_that_signed_it() {
use sha2::Digest as _;
let (signing, verifying) = test_keypair();
let signed = sign_manifest(&sample_manifest(), &signing);
let admission = verify_admission(&signed, &verifying, &net_grants()).unwrap();
assert_eq!(admission.signer(), &verifying);
let fingerprint = admission.signer_fingerprint();
assert_eq!(fingerprint.len(), 64);
assert!(
fingerprint
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
);
let digest = sha2::Sha256::digest(verifying);
let expected: Vec<String> = digest.iter().map(|byte| format!("{byte:02x}")).collect();
let expected = expected.concat();
assert_eq!(fingerprint, expected);
let other = SigningKey::from_bytes(&[0x42; 32]);
let other_root = other.verifying_key().to_bytes();
let impostor = verify_admission(
&sign_manifest(&sample_manifest(), &other),
&other_root,
&net_grants(),
)
.unwrap();
assert_eq!(impostor.manifest().name, admission.manifest().name);
assert_ne!(impostor.signer_fingerprint(), fingerprint);
assert_eq!(impostor.signer(), &other_root);
}
#[test]
fn unsigned_is_refused() {
let (_, verifying) = test_keypair();
let signed = SignedManifest {
manifest: sample_manifest(),
signature: None,
};
let err = verify_admission(&signed, &verifying, &net_grants()).unwrap_err();
assert!(matches!(err, Error::Unsigned { .. }));
}
#[test]
fn tampered_is_refused() {
let (signing, verifying) = test_keypair();
let mut signed = sign_manifest(&sample_manifest(), &signing);
signed.manifest.version = String::from("9.9.9");
let err = verify_admission(&signed, &verifying, &net_grants()).unwrap_err();
assert!(matches!(err, Error::BadSignature { .. }));
}
#[test]
fn wrong_key_is_refused() {
let (signing, _) = test_keypair();
let other_secret = [
0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae,
0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc,
0xbd, 0xbe, 0xbf, 0xc0,
];
let other_signing = SigningKey::from_bytes(&other_secret);
let other_verifying = other_signing.verifying_key().to_bytes();
let signed = sign_manifest(&sample_manifest(), &signing);
let err = verify_admission(&signed, &other_verifying, &net_grants()).unwrap_err();
assert!(matches!(err, Error::BadSignature { .. }));
}
#[test]
fn signature_without_the_domain_tag_is_refused() {
let (signing, verifying) = test_keypair();
let manifest = sample_manifest();
let bare = serde_json::to_vec(&manifest).unwrap();
let signed = SignedManifest {
manifest,
signature: Some(BASE64.encode(signing.sign(&bare).to_bytes())),
};
let err = verify_admission(&signed, &verifying, &net_grants()).unwrap_err();
assert!(matches!(err, Error::BadSignature { .. }));
}
#[test]
fn trust_roots_are_checked_for_shape_and_strength() {
let (_, verifying) = test_keypair();
assert!(check_trust_root(&verifying).is_ok());
let mut identity = [0u8; 32];
identity[0] = 1;
assert!(matches!(
check_trust_root(&identity),
Err(Error::InvalidTrustRoot(reason)) if reason.contains("weak")
));
}
#[test]
fn weak_trust_root_is_refused() {
let mut identity = [0u8; 32];
identity[0] = 1;
let mut sig = [0u8; 64];
sig[0] = 1;
let signed = SignedManifest {
manifest: sample_manifest(),
signature: Some(BASE64.encode(sig)),
};
let result = verify_admission(&signed, &identity, &net_grants());
assert!(
matches!(
result,
Err(Error::BadSignature { .. } | Error::InvalidTrustRoot(_))
),
"a weak trust root must never admit: {result:?}"
);
}
#[test]
fn signed_but_invalid_manifest_is_refused() {
let (signing, verifying) = test_keypair();
let mut manifest = sample_manifest();
manifest.entry_point = String::from("../evil.py");
let signed = sign_manifest(&manifest, &signing);
let err = verify_admission(&signed, &verifying, &net_grants()).unwrap_err();
assert!(matches!(err, Error::InvalidManifest(_)), "{err:?}");
}
#[test]
fn over_privileged_is_refused_at_admission() {
let (signing, verifying) = test_keypair();
let mut manifest = sample_manifest();
manifest.capabilities = vec![cap("net.egress"), cap("fs.write")];
let signed = sign_manifest(&manifest, &signing);
let err = verify_admission(&signed, &verifying, &net_grants()).unwrap_err();
assert!(
matches!(err, Error::OverPrivileged { .. }),
"expected OverPrivileged, got {err:?}"
);
if let Error::OverPrivileged { name, excess } = err {
assert_eq!(name, "test-skill");
assert_eq!(excess, vec![cap("fs.write")]);
}
}
#[test]
fn undeclared_capability_denied_at_call_time() {
let (signing, verifying) = test_keypair();
let signed = sign_manifest(&sample_manifest(), &signing);
let admission = verify_admission(&signed, &verifying, &net_grants()).unwrap();
assert!(admission.check_call(&cap("net.egress")).is_ok());
let err = admission.check_call(&cap("fs.read")).unwrap_err();
assert!(
matches!(&err, Error::UndeclaredCapability { capability, .. } if *capability == cap("fs.read")),
"{err:?}"
);
}
}