use serde::{Deserialize, Serialize, de::DeserializeOwned}; pub const CANONICAL_ENVELOPE_VERSION: u16 = 1; #[derive(Debug, thiserror::Error)] pub enum CodecError { #[error("canonical encoding failed: {0}")] Encode(#[from] postcard::Error), } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CanonicalEnvelope { pub version: u16, pub namespace: String, pub payload: T, } impl CanonicalEnvelope { #[must_use] pub fn new(namespace: impl Into, payload: T) -> Self { Self { version: CANONICAL_ENVELOPE_VERSION, namespace: namespace.into(), payload, } } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct SignedEnvelope { pub envelope: CanonicalEnvelope, pub signer: S, pub signature: Vec, } impl SignedEnvelope { #[must_use] pub fn new(namespace: impl Into, payload: T, signer: S, signature: Vec) -> Self { Self { envelope: CanonicalEnvelope::new(namespace, payload), signer, signature, } } #[must_use] pub fn namespace(&self) -> &str { &self.envelope.namespace } #[must_use] pub fn payload(&self) -> &T { &self.envelope.payload } } #[derive(Serialize)] struct CanonicalEnvelopeRef<'a, T: ?Sized> { version: u16, namespace: &'a str, payload: &'a T, } pub fn encode_canonical(value: &T) -> Result, CodecError> { postcard::to_allocvec(value).map_err(CodecError::from) } pub fn decode_canonical(bytes: &[u8]) -> Result { postcard::from_bytes(bytes).map_err(CodecError::from) } pub fn hash_canonical( value: &T, ) -> Result { let bytes = encode_canonical(value)?; Ok(blake3_hash_bytes(&bytes)) } #[must_use] pub fn blake3_hash_bytes(bytes: &[u8]) -> geth_types::BlobHash { geth_types::BlobHash::new(blake3::hash(bytes).to_hex().to_string()) } pub fn signing_payload( namespace: &str, payload: &T, ) -> Result, CodecError> { encode_canonical(&CanonicalEnvelopeRef { version: CANONICAL_ENVELOPE_VERSION, namespace, payload, }) } pub fn signing_payload_hash( namespace: &str, payload: &T, ) -> Result { Ok(blake3_hash_bytes(&signing_payload(namespace, payload)?)) } #[cfg(test)] mod tests { use super::*; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] struct Sample { version: u8, name: String, values: Vec, } #[test] fn canonical_encoding_is_deterministic() { let sample = Sample { version: 1, name: "geth".to_owned(), values: vec![1, 2, 3], }; assert_eq!( encode_canonical(&sample).expect("encode"), encode_canonical(&sample).expect("encode again") ); assert_eq!( hash_canonical(&sample).expect("hash"), hash_canonical(&sample).expect("hash again") ); assert_eq!( decode_canonical::(&encode_canonical(&sample).expect("encode")) .expect("decode"), sample ); } #[test] fn signing_payload_includes_namespace_and_version() { let sample = Sample { version: 1, name: "geth".to_owned(), values: vec![1, 2, 3], }; let keychain_payload = signing_payload("geth.keychain.v1@geth.local", &sample).expect("encode"); let auth_payload = signing_payload("geth.auth-op.v1@geth.local", &sample).expect("encode"); assert_eq!( keychain_payload, signing_payload("geth.keychain.v1@geth.local", &sample).expect("encode again") ); assert_ne!(keychain_payload, auth_payload); let decoded: CanonicalEnvelope = decode_canonical(&keychain_payload).expect("decode envelope"); assert_eq!(decoded.version, CANONICAL_ENVELOPE_VERSION); assert_eq!(decoded.namespace, "geth.keychain.v1@geth.local"); assert_eq!(decoded.payload, sample); } }