Add canonical signed operation envelopes

This commit is contained in:
Eric Wendland 2026-05-16 14:37:16 +02:00
commit 373ff53d5c
12 changed files with 255 additions and 11 deletions

View file

@ -1,4 +1,6 @@
use serde::{Serialize, de::DeserializeOwned};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
pub const CANONICAL_ENVELOPE_VERSION: u16 = 1;
#[derive(Debug, thiserror::Error)]
pub enum CodecError {
@ -6,6 +8,59 @@ pub enum CodecError {
Encode(#[from] postcard::Error),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CanonicalEnvelope<T> {
pub version: u16,
pub namespace: String,
pub payload: T,
}
impl<T> CanonicalEnvelope<T> {
#[must_use]
pub fn new(namespace: impl Into<String>, payload: T) -> Self {
Self {
version: CANONICAL_ENVELOPE_VERSION,
namespace: namespace.into(),
payload,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedEnvelope<T, S> {
pub envelope: CanonicalEnvelope<T>,
pub signer: S,
pub signature: Vec<u8>,
}
impl<T, S> SignedEnvelope<T, S> {
#[must_use]
pub fn new(namespace: impl Into<String>, payload: T, signer: S, signature: Vec<u8>) -> 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<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, CodecError> {
postcard::to_allocvec(value).map_err(CodecError::from)
}
@ -26,6 +81,24 @@ 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<T: Serialize + ?Sized>(
namespace: &str,
payload: &T,
) -> Result<Vec<u8>, CodecError> {
encode_canonical(&CanonicalEnvelopeRef {
version: CANONICAL_ENVELOPE_VERSION,
namespace,
payload,
})
}
pub fn signing_payload_hash<T: Serialize + ?Sized>(
namespace: &str,
payload: &T,
) -> Result<geth_types::BlobHash, CodecError> {
Ok(blake3_hash_bytes(&signing_payload(namespace, payload)?))
}
#[cfg(test)]
mod tests {
use super::*;
@ -59,4 +132,28 @@ mod tests {
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<Sample> =
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);
}
}