use base64::Engine;
use super::types::{
AcceptancePolicy, RulesetAcceptance, RulesetError, RulesetManifest, SignedBundle,
};
pub trait JwsVerify {
fn verify_eddsa(&self, jws: &str, public_key_b64: &str) -> Result<bool, RulesetError>;
}
pub fn content_hash(content: &serde_json::Value) -> Result<String, RulesetError> {
crate::canonical::content_hash(content)
.map_err(|e| RulesetError::Malformed(format!("JCS canonicalisation failed: {e}")))
}
pub fn verify_bundle(
bundle: &SignedBundle,
publisher_pubkey_b64: &str,
verifier: &dyn JwsVerify,
policy: &AcceptancePolicy<'_>,
) -> Result<RulesetAcceptance, 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);
}
if manifest.effective_date > policy.now {
return Err(RulesetError::NotYetEffective {
bundle_version: manifest.bundle_version,
effective_date: manifest.effective_date,
now: policy.now,
});
}
if let Some(in_force) = policy.in_force
&& manifest.effective_date < in_force.effective_date
{
return Err(RulesetError::Superseded {
offered_version: manifest.bundle_version,
offered: manifest.effective_date,
in_force_version: in_force.bundle_version.clone(),
in_force: in_force.effective_date,
});
}
Ok(RulesetAcceptance::verified(
manifest,
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}")))
}