use base64::Engine;
use sha2::{Digest, Sha256};
use super::types::{RulesetError, RulesetManifest, SignedBundle, VerifiedRuleset};
pub trait JwsVerify {
fn verify_eddsa(&self, jws: &str, public_key_b64: &str) -> Result<bool, RulesetError>;
}
#[must_use]
pub fn content_hash(content: &serde_json::Value) -> String {
let bytes = serde_jcs::to_vec(content).expect("JCS canonicalisation is infallible");
hex::encode(Sha256::digest(&bytes))
}
pub fn verify_bundle(
bundle: &SignedBundle,
publisher_pubkey_b64: &str,
verifier: &dyn JwsVerify,
) -> Result<VerifiedRuleset, RulesetError> {
if !verifier.verify_eddsa(&bundle.manifest_jws, publisher_pubkey_b64)? {
return Err(RulesetError::BadSignature);
}
let manifest: RulesetManifest = decode_jws_payload(&bundle.manifest_jws)?;
if content_hash(&bundle.content) != manifest.content_sha256 {
return Err(RulesetError::ContentHashMismatch);
}
Ok(VerifiedRuleset {
manifest,
content: bundle.content.clone(),
})
}
fn decode_jws_payload<T: for<'de> serde::Deserialize<'de>>(jws: &str) -> Result<T, RulesetError> {
let payload_b64 = jws
.split('.')
.nth(1)
.ok_or_else(|| RulesetError::Malformed("JWS has no payload segment".into()))?;
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload_b64)
.map_err(|e| RulesetError::Malformed(format!("payload base64: {e}")))?;
serde_json::from_slice(&bytes)
.map_err(|e| RulesetError::Malformed(format!("payload json: {e}")))
}