//! Attestations (spec §14.4): signed statements about references and batches //! of changes. Only the `ref` and `seal` kinds exist so far. use crate::cbor::{self, Value}; use crate::hash::{hash_object, ChangeHash, ObjectRef, ObjectType}; use crate::objects::{array, bytes, check_version, field, map, oref, text, uint, Hlc, ObjectError, FORMAT_VERSION}; pub const MAX_SEAL_HASHES: usize = 4096; #[derive(Clone, Debug, PartialEq, Eq)] pub enum AttestBody { /// A bookmark moved from `old_state` to `new_state` (spec §10.2). Ref { name: String, old_state: ObjectRef, new_state: ObjectRef, serial: u64 }, /// One signature covering a batch of changes (spec §14.4). Seal { hashes: Vec }, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Attest { /// Repository ID chosen at init. pub repo: [u8; 16], pub hlc: Hlc, pub body: AttestBody, } impl Attest { pub fn kind(&self) -> &'static str { match self.body { AttestBody::Ref { .. } => "ref", AttestBody::Seal { .. } => "seal", } } pub fn seal(repo: [u8; 16], mut hashes: Vec) -> Attest { hashes.sort(); hashes.dedup(); Attest { repo, hlc: Hlc::now(), body: AttestBody::Seal { hashes } } } pub fn to_value(&self) -> Value { let body = match &self.body { AttestBody::Ref { name, old_state, new_state, serial } => Value::map([ (0, Value::Text(name.clone())), (1, Value::Bytes(old_state.0.to_vec())), (2, Value::Bytes(new_state.0.to_vec())), (3, Value::Uint(*serial)), ]), AttestBody::Seal { hashes } => { Value::map([(0, Value::Array(hashes.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()))]) } }; Value::map([ (0, Value::Uint(FORMAT_VERSION)), (1, Value::Text(self.kind().into())), (2, Value::Bytes(self.repo.to_vec())), (3, self.hlc.to_value()), (4, body), ]) } pub fn encode(&self) -> Vec { cbor::encode(&self.to_value()) } pub fn hash(&self) -> ObjectRef { hash_object(ObjectType::Attest, &self.encode()) } pub fn decode(b: &[u8]) -> Result { let v = cbor::decode(b)?; let m = map(&v, "attest")?; check_version(m)?; let kind = text(field(m, 1, "kind")?, "kind")?; let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")? .try_into() .map_err(|_| ObjectError::Schema("repo: expected 16 bytes".into()))?; let hlc = Hlc::from_value(field(m, 3, "hlc")?)?; let b = map(field(m, 4, "body")?, "body")?; let body = match kind { "ref" => AttestBody::Ref { name: text(field(b, 0, "name")?, "name")?.to_owned(), old_state: oref(field(b, 1, "old_state")?, "old_state")?, new_state: oref(field(b, 2, "new_state")?, "new_state")?, serial: uint(field(b, 3, "serial")?, "serial")?, }, "seal" => { let hashes = array(field(b, 0, "hashes")?, "hashes")?.iter().map(|h| oref(h, "hash")).collect::, _>>()?; if hashes.is_empty() || hashes.len() > MAX_SEAL_HASHES || !hashes.windows(2).all(|w| w[0] < w[1]) { return Err(ObjectError::Schema("seal: hashes must be 1..4096, sorted and unique".into())); } AttestBody::Seal { hashes } } other => return Err(ObjectError::Schema(format!("unknown attestation kind {other:?}"))), }; Ok(Attest { repo, hlc, body }) } } #[cfg(test)] mod tests { use super::*; #[test] fn round_trips() { let r = Attest { repo: [1; 16], hlc: Hlc { wall_ms: 3, logical: 0 }, body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 7 }, }; assert_eq!(Attest::decode(&r.encode()).unwrap(), r); assert_eq!(r.kind(), "ref"); let s = Attest::seal([1; 16], vec![ObjectRef([9; 32]), ObjectRef([3; 32]), ObjectRef([9; 32])]); let d = Attest::decode(&s.encode()).unwrap(); assert!(matches!(&d.body, AttestBody::Seal { hashes } if hashes == &vec![ObjectRef([3; 32]), ObjectRef([9; 32])])); assert_ne!(s.hash(), r.hash()); } #[test] fn rejects_bad_seals() { let empty = Attest { repo: [0; 16], hlc: Hlc::default(), body: AttestBody::Seal { hashes: vec![] } }; assert!(Attest::decode(&empty.encode()).is_err()); let unsorted = Attest { repo: [0; 16], hlc: Hlc::default(), body: AttestBody::Seal { hashes: vec![ObjectRef([2; 32]), ObjectRef([1; 32])] } }; assert!(Attest::decode(&unsorted.encode()).is_err()); } }