use base64::Engine;
use chrono::{Duration, SecondsFormat, SubsecRound, Utc};
use dpp_crypto::keystore::KeyStore;
use serde_json::{Value, json};
use super::*;
use crate::test_support::temp_store;
const KEY: &str = "snapshot-signer";
fn public_key_b64(store: &KeyStore) -> String {
let entry = store.load_key(KEY).expect("load key");
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(entry.verifying_key.to_bytes())
}
fn frozen_public_proof() -> String {
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
format!(
"{}.{}.{}",
b64.encode(br#"{"alg":"EdDSA"}"#),
b64.encode(br#"{"frozen":true}"#),
b64.encode(b"not-a-real-signature"),
)
}
fn public_view() -> Value {
json!({
"id": "0192f3c0-0000-7000-8000-000000000000",
"productName": "Widget",
"status": "active",
"publicJwsSignature": frozen_public_proof(),
})
}
fn snapshot(store: &KeyStore, as_of: chrono::DateTime<Utc>, valid_for: Duration) -> Value {
let rfc3339 = |t: chrono::DateTime<Utc>| t.to_rfc3339_opts(SecondsFormat::Secs, true);
let mut document = public_view();
let object = document.as_object_mut().expect("object");
object.insert("asOf".to_owned(), json!(rfc3339(as_of)));
object.insert("validUntil".to_owned(), json!(rfc3339(as_of + valid_for)));
let proof = dpp_crypto::jws::sign(store, KEY, &document).expect("sign snapshot");
document
.as_object_mut()
.expect("object")
.insert("snapshotJwsSignature".to_owned(), json!(proof));
document
}
#[test]
fn a_document_with_no_outer_proof_is_unbound() {
let store = temp_store("snapshot-unbound", KEY);
let bound = verify_snapshot_bound(&public_view(), &public_key_b64(&store), Utc::now());
assert_eq!(bound, SnapshotBound::Absent);
assert!(!bound.is_expired());
assert_eq!(bound.proven(), None);
}
#[test]
fn a_snapshot_inside_its_bound_is_current() {
let store = temp_store("snapshot-current", KEY);
let as_of = Utc::now();
let document = snapshot(&store, as_of, Duration::days(7));
let bound = verify_snapshot_bound(
&document,
&public_key_b64(&store),
as_of + Duration::days(1),
);
assert!(matches!(bound, SnapshotBound::Current { .. }), "{bound:?}");
assert!(!bound.is_expired());
assert!(bound.proven().is_some());
}
#[test]
fn a_snapshot_past_its_bound_is_expired() {
let store = temp_store("snapshot-expired", KEY);
let as_of = Utc::now() - Duration::days(30);
let document = snapshot(&store, as_of, Duration::days(7));
let bound = verify_snapshot_bound(&document, &public_key_b64(&store), Utc::now());
let SnapshotBound::Expired {
as_of: reported,
valid_until,
} = bound.clone()
else {
panic!("expected Expired, got {bound:?}");
};
assert!(bound.is_expired());
assert_eq!(reported, as_of.trunc_subsecs(0));
assert_eq!(valid_until, (as_of + Duration::days(7)).trunc_subsecs(0));
}
#[test]
fn an_expired_snapshot_is_not_reported_as_tampering() {
let store = temp_store("snapshot-expired-not-tampered", KEY);
let as_of = Utc::now() - Duration::days(30);
let document = snapshot(&store, as_of, Duration::days(7));
let bound = verify_snapshot_bound(&document, &public_key_b64(&store), Utc::now());
assert!(
!matches!(bound, SnapshotBound::Unproven(_)),
"an intact but stale snapshot must not be reported as unproven: {bound:?}"
);
}
#[test]
fn a_forged_valid_until_does_not_extend_the_bound() {
let store = temp_store("snapshot-forged-deadline", KEY);
let as_of = Utc::now() - Duration::days(30);
let mut document = snapshot(&store, as_of, Duration::days(7));
document.as_object_mut().expect("object").insert(
"validUntil".to_owned(),
json!((Utc::now() + Duration::days(365)).to_rfc3339_opts(SecondsFormat::Secs, true)),
);
let bound = verify_snapshot_bound(&document, &public_key_b64(&store), Utc::now());
assert!(
matches!(bound, SnapshotBound::Unproven(_)),
"a rewritten deadline must break the proof, not extend the bound: {bound:?}"
);
assert!(!bound.is_expired());
assert_eq!(bound.proven(), None);
}
#[test]
fn the_outer_proof_covers_the_publish_time_proof() {
let store = temp_store("snapshot-covers-inner", KEY);
let mut document = snapshot(&store, Utc::now(), Duration::days(7));
document.as_object_mut().expect("object").insert(
"publicJwsSignature".to_owned(),
json!(format!("{}-swapped", frozen_public_proof())),
);
let bound = verify_snapshot_bound(&document, &public_key_b64(&store), Utc::now());
assert!(matches!(bound, SnapshotBound::Unproven(_)), "{bound:?}");
}
#[test]
fn a_tampered_passport_field_leaves_the_bound_unproven() {
let store = temp_store("snapshot-tampered-field", KEY);
let mut document = snapshot(&store, Utc::now(), Duration::days(7));
document
.as_object_mut()
.expect("object")
.insert("productName".to_owned(), json!("Something Else"));
let bound = verify_snapshot_bound(&document, &public_key_b64(&store), Utc::now());
assert!(matches!(bound, SnapshotBound::Unproven(_)), "{bound:?}");
}
#[test]
fn a_proof_from_another_key_is_unproven() {
let signer = temp_store("snapshot-wrong-key-signer", KEY);
let other = temp_store("snapshot-wrong-key-other", KEY);
let document = snapshot(&signer, Utc::now(), Duration::days(7));
let bound = verify_snapshot_bound(&document, &public_key_b64(&other), Utc::now());
assert!(matches!(bound, SnapshotBound::Unproven(_)), "{bound:?}");
}
#[test]
fn a_stripped_proof_leaves_the_dates_vouched_for_by_nobody() {
let store = temp_store("snapshot-stripped", KEY);
let as_of = Utc::now() - Duration::days(30);
let mut document = snapshot(&store, as_of, Duration::days(7));
document
.as_object_mut()
.expect("object")
.remove("snapshotJwsSignature");
let bound = verify_snapshot_bound(&document, &public_key_b64(&store), Utc::now());
assert_eq!(bound, SnapshotBound::Absent);
assert!(
document.get("validUntil").is_some(),
"the date is still there"
);
assert_eq!(
bound.proven(),
None,
"an unvouched date must never be returned as a proven bound"
);
}
#[test]
fn the_expiry_edge_is_the_stated_skew_tolerance() {
let store = temp_store("snapshot-skew", KEY);
let as_of = Utc::now().trunc_subsecs(0);
let document = snapshot(&store, as_of, Duration::days(7));
let key = public_key_b64(&store);
let deadline = as_of + Duration::days(7);
let at_the_edge = verify_snapshot_bound(&document, &key, deadline + CLOCK_SKEW_TOLERANCE);
assert!(
matches!(at_the_edge, SnapshotBound::Current { .. }),
"a clock exactly one tolerance fast must still see the snapshot as current: {at_the_edge:?}"
);
let past_it = verify_snapshot_bound(
&document,
&key,
deadline + CLOCK_SKEW_TOLERANCE + Duration::seconds(1),
);
assert!(past_it.is_expired(), "{past_it:?}");
}
#[test]
fn a_snapshot_survives_the_round_trip_through_bytes() {
let store = temp_store("snapshot-round-trip", KEY);
let as_of = Utc::now();
let rfc3339 = as_of.to_rfc3339_opts(SecondsFormat::Secs, true);
let mut document = json!({
"id": "0192f3c0-0000-7000-8000-000000000000",
"productName": "Wäsche-Trockner \"Pro\"",
"co2ePerUnitKg": 1.8,
"nominalCapacityAh": 100.0,
"serial": 9_007_199_254_740_991_i64,
"materials": [],
"manufacturer": { "name": "Ünïcode GmbH", "country": "DE" },
"publicJwsSignature": frozen_public_proof(),
"asOf": rfc3339,
"validUntil": (as_of + Duration::days(7)).to_rfc3339_opts(SecondsFormat::Secs, true),
});
let proof = dpp_crypto::jws::sign(&store, KEY, &document).expect("sign snapshot");
document
.as_object_mut()
.expect("object")
.insert("snapshotJwsSignature".to_owned(), json!(proof));
let bytes = serde_json::to_vec(&document).expect("serialise snapshot");
let parsed: Value = serde_json::from_slice(&bytes).expect("parse snapshot");
let bound = verify_snapshot_bound(&parsed, &public_key_b64(&store), as_of + Duration::days(1));
assert!(
matches!(bound, SnapshotBound::Current { .. }),
"a snapshot must still verify after the round trip it actually makes: {bound:?}"
);
}
#[test]
fn a_non_object_document_is_unproven_rather_than_unbound() {
let store = temp_store("snapshot-not-an-object", KEY);
let bound = verify_snapshot_bound(
&json!(["not", "a", "snapshot"]),
&public_key_b64(&store),
Utc::now(),
);
assert!(matches!(bound, SnapshotBound::Unproven(_)), "{bound:?}");
}
#[test]
fn a_non_string_proof_is_unproven() {
let store = temp_store("snapshot-proof-not-a-string", KEY);
let mut document = snapshot(&store, Utc::now(), Duration::days(7));
document
.as_object_mut()
.expect("object")
.insert("snapshotJwsSignature".to_owned(), json!(42));
let bound = verify_snapshot_bound(&document, &public_key_b64(&store), Utc::now());
assert!(matches!(bound, SnapshotBound::Unproven(_)), "{bound:?}");
}