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

@ -12,6 +12,7 @@ hex.workspace = true
rand_core.workspace = true
serde.workspace = true
thiserror.workspace = true
geth-codec = { path = "../geth-codec" }
geth-types = { path = "../geth-types" }
[dev-dependencies]

View file

@ -1,5 +1,6 @@
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use rand_core::OsRng;
use serde::Serialize;
use std::path::Path;
#[derive(Debug, thiserror::Error)]
@ -12,6 +13,8 @@ pub enum CryptoError {
InvalidKey,
#[error("signature verification failed")]
Verify,
#[error("canonical encoding failed: {0}")]
Codec(#[from] geth_codec::CodecError),
}
pub struct AgentKey {
@ -73,6 +76,14 @@ impl AgentKey {
pub fn sign(&self, bytes: &[u8]) -> Vec<u8> {
self.signing_key.sign(bytes).to_bytes().to_vec()
}
pub fn sign_canonical<T: Serialize + ?Sized>(
&self,
namespace: &str,
payload: &T,
) -> Result<Vec<u8>, CryptoError> {
Ok(self.sign(&geth_codec::signing_payload(namespace, payload)?))
}
}
pub fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
@ -85,6 +96,19 @@ pub fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(),
.map_err(|_| CryptoError::Verify)
}
pub fn verify_canonical<T: Serialize + ?Sized>(
public_key: &[u8],
namespace: &str,
payload: &T,
signature: &[u8],
) -> Result<(), CryptoError> {
verify(
public_key,
&geth_codec::signing_payload(namespace, payload)?,
signature,
)
}
#[must_use]
pub fn blake3_hex(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
@ -115,4 +139,39 @@ mod tests {
"3a4aa805ade0d4694a1bb69ad5b9a2f1dffcd4de2136df9a465023722d26e325"
);
}
#[derive(Clone, serde::Serialize)]
struct CanonicalSample {
version: u8,
name: String,
}
#[test]
fn canonical_signature_verifies_only_for_matching_namespace() {
let key = AgentKey::generate();
let sample = CanonicalSample {
version: 1,
name: "geth".to_owned(),
};
let signature = key
.sign_canonical("geth.keychain.v1@geth.local", &sample)
.expect("sign");
verify_canonical(
&key.verifying_key().to_bytes(),
"geth.keychain.v1@geth.local",
&sample,
&signature,
)
.expect("verify matching namespace");
assert!(matches!(
verify_canonical(
&key.verifying_key().to_bytes(),
"geth.auth-op.v1@geth.local",
&sample,
&signature,
),
Err(CryptoError::Verify)
));
}
}