use base64::Engine;
use chrono::{DateTime, Duration, Utc};
use dpp_crypto::jws::{canonical::canonicalize, verify_jws};
use serde_json::Value;
use super::bound::SnapshotBound;
const PROOF_KEY: &str = "snapshotJwsSignature";
const AS_OF_KEY: &str = "asOf";
const VALID_UNTIL_KEY: &str = "validUntil";
pub const CLOCK_SKEW_TOLERANCE: Duration = Duration::minutes(5);
#[must_use]
pub fn verify_snapshot_bound(
document: &Value,
public_key_b64: &str,
now: DateTime<Utc>,
) -> SnapshotBound {
let Some(object) = document.as_object() else {
return SnapshotBound::Unproven("snapshot document is not a JSON object".to_owned());
};
let proof = match object.get(PROOF_KEY) {
None => return SnapshotBound::Absent,
Some(Value::String(jws)) => jws,
Some(_) => {
return SnapshotBound::Unproven(format!("`{PROOF_KEY}` is present but not a string"));
}
};
match verify_jws(proof, public_key_b64) {
Ok(true) => {}
Ok(false) => {
return SnapshotBound::Unproven("snapshot proof does not verify".to_owned());
}
Err(e) => {
return SnapshotBound::Unproven(format!("snapshot proof is malformed: {e}"));
}
}
let mut covered = object.clone();
covered.remove(PROOF_KEY);
let Ok(expected) = canonicalize(&Value::Object(covered)) else {
return SnapshotBound::Unproven("snapshot document cannot be canonicalized".to_owned());
};
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let signed = proof.split('.').nth(1).and_then(|p| b64.decode(p).ok());
if signed.as_deref() != Some(expected.as_slice()) {
return SnapshotBound::Unproven(
"snapshot proof covers different bytes than this document".to_owned(),
);
}
let (Some(as_of), Some(valid_until)) = (
rfc3339(object.get(AS_OF_KEY)),
rfc3339(object.get(VALID_UNTIL_KEY)),
) else {
return SnapshotBound::Unproven(format!(
"snapshot proof verifies but `{AS_OF_KEY}`/`{VALID_UNTIL_KEY}` are missing or unreadable"
));
};
if now > valid_until + CLOCK_SKEW_TOLERANCE {
SnapshotBound::Expired { as_of, valid_until }
} else {
SnapshotBound::Current { as_of, valid_until }
}
}
fn rfc3339(value: Option<&Value>) -> Option<DateTime<Utc>> {
let text = value?.as_str()?;
DateTime::parse_from_rfc3339(text)
.ok()
.map(|t| t.with_timezone(&Utc))
}