177 lines
4.9 KiB
Rust
177 lines
4.9 KiB
Rust
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
|
use rand_core::OsRng;
|
|
use serde::Serialize;
|
|
use std::path::Path;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum CryptoError {
|
|
#[error("io error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
#[error("invalid hex key: {0}")]
|
|
Hex(#[from] hex::FromHexError),
|
|
#[error("invalid ed25519 key material")]
|
|
InvalidKey,
|
|
#[error("signature verification failed")]
|
|
Verify,
|
|
#[error("canonical encoding failed: {0}")]
|
|
Codec(#[from] geth_codec::CodecError),
|
|
}
|
|
|
|
pub struct AgentKey {
|
|
signing_key: SigningKey,
|
|
}
|
|
|
|
impl AgentKey {
|
|
#[must_use]
|
|
pub fn generate() -> Self {
|
|
Self {
|
|
signing_key: SigningKey::generate(&mut OsRng),
|
|
}
|
|
}
|
|
|
|
pub fn load_or_create(path: &Path) -> Result<Self, CryptoError> {
|
|
if path.exists() {
|
|
return Self::load(path);
|
|
}
|
|
let key = Self::generate();
|
|
key.save(path)?;
|
|
Ok(key)
|
|
}
|
|
|
|
pub fn load(path: &Path) -> Result<Self, CryptoError> {
|
|
let hex_key = std::fs::read_to_string(path)?;
|
|
let bytes = hex::decode(hex_key.trim())?;
|
|
let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| CryptoError::InvalidKey)?;
|
|
Ok(Self {
|
|
signing_key: SigningKey::from_bytes(&key_bytes),
|
|
})
|
|
}
|
|
|
|
pub fn save(&self, path: &Path) -> Result<(), CryptoError> {
|
|
if let Some(parent) = path.parent() {
|
|
std::fs::create_dir_all(parent)?;
|
|
}
|
|
let tmp = path.with_extension("tmp");
|
|
std::fs::write(&tmp, hex::encode(self.signing_key.to_bytes()))?;
|
|
std::fs::rename(tmp, path)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn verifying_key(&self) -> VerifyingKey {
|
|
self.signing_key.verifying_key()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn public_key_hex(&self) -> String {
|
|
hex::encode(self.verifying_key().to_bytes())
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn agent_id(&self) -> geth_types::AgentId {
|
|
geth_types::AgentId::new(key_fingerprint(&self.verifying_key().to_bytes()))
|
|
}
|
|
|
|
#[must_use]
|
|
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> {
|
|
let key_bytes: [u8; 32] = public_key.try_into().map_err(|_| CryptoError::InvalidKey)?;
|
|
let verifying_key =
|
|
VerifyingKey::from_bytes(&key_bytes).map_err(|_| CryptoError::InvalidKey)?;
|
|
let sig = Signature::from_slice(signature).map_err(|_| CryptoError::InvalidKey)?;
|
|
verifying_key
|
|
.verify(message, &sig)
|
|
.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()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn key_fingerprint(public_key: &[u8]) -> String {
|
|
format!("ed25519:{}", blake3_hex(public_key))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn agent_identity_persists() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
let path = dir.path().join("agent.ed25519");
|
|
let first = AgentKey::load_or_create(&path).expect("create");
|
|
let second = AgentKey::load_or_create(&path).expect("load");
|
|
assert_eq!(first.agent_id(), second.agent_id());
|
|
}
|
|
|
|
#[test]
|
|
fn blake3_helper_matches_known_hash() {
|
|
assert_eq!(
|
|
blake3_hex(b"hello geth"),
|
|
"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)
|
|
));
|
|
}
|
|
}
|