geth/crates/geth-keychain/src/lib.rs

169 lines
4.9 KiB
Rust
Raw Normal View History

2026-05-15 15:08:20 +02:00
use geth_types::{AgentId, DeviceId, KeyId, NodeId, UnixMillis, UserId};
use serde::{Deserialize, Serialize};
pub const KEYCHAIN_SIGNATURE_NAMESPACE: &str = "geth.keychain.v1@geth.local";
pub type SignedKeychainOp = geth_codec::SignedEnvelope<KeychainOp, KeyId>;
pub fn keychain_signing_payload(op: &KeychainOp) -> Result<Vec<u8>, geth_codec::CodecError> {
geth_codec::signing_payload(KEYCHAIN_SIGNATURE_NAMESPACE, op)
}
pub fn keychain_signing_payload_hash(
op: &KeychainOp,
) -> Result<geth_types::BlobHash, geth_codec::CodecError> {
geth_codec::signing_payload_hash(KEYCHAIN_SIGNATURE_NAMESPACE, op)
}
#[must_use]
pub fn signed_keychain_op(op: KeychainOp, signer: KeyId, signature: Vec<u8>) -> SignedKeychainOp {
geth_codec::SignedEnvelope::new(KEYCHAIN_SIGNATURE_NAMESPACE, op, signer, signature)
2026-05-15 15:08:20 +02:00
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeychainOp {
pub id: geth_types::AuthOpId,
pub created_at: UnixMillis,
pub kind: KeychainOpKind,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum KeychainOpKind {
KeychainInit,
AdminKeyAdd {
key: KeyId,
},
AdminKeyRevoke {
key: KeyId,
},
UserAdd {
user: UserId,
name: String,
},
UserRename {
user: UserId,
name: String,
},
UserRevoke {
user: UserId,
},
DeviceAdd {
device: DeviceId,
user: UserId,
},
DeviceRevoke {
device: DeviceId,
},
DeviceKeyAdd {
device: DeviceId,
key: KeyId,
},
DeviceKeyRevoke {
device: DeviceId,
key: KeyId,
},
NodeAdd {
node: NodeId,
device: DeviceId,
name: String,
},
NodeRename {
node: NodeId,
name: String,
},
NodeRevoke {
node: NodeId,
},
NodeEndpointAdd {
node: NodeId,
endpoint: String,
},
NodeEndpointRevoke {
node: NodeId,
endpoint: String,
},
AgentBind {
agent: AgentId,
node: NodeId,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeychainView {
pub initialized: bool,
pub admin_keys: Vec<KeyId>,
pub users: Vec<UserId>,
pub devices: Vec<DeviceId>,
pub nodes: Vec<NodeId>,
}
pub fn reduce_keychain_ops(ops: &[KeychainOp]) -> KeychainView {
let mut view = KeychainView::default();
for op in ops {
match &op.kind {
KeychainOpKind::KeychainInit => view.initialized = true,
KeychainOpKind::AdminKeyAdd { key } if !view.admin_keys.contains(key) => {
view.admin_keys.push(key.clone());
}
KeychainOpKind::AdminKeyRevoke { key } => view.admin_keys.retain(|item| item != key),
KeychainOpKind::UserAdd { user, .. } if !view.users.contains(user) => {
view.users.push(user.clone());
}
KeychainOpKind::UserRevoke { user } => view.users.retain(|item| item != user),
KeychainOpKind::DeviceAdd { device, .. } if !view.devices.contains(device) => {
view.devices.push(device.clone());
}
KeychainOpKind::DeviceRevoke { device } => view.devices.retain(|item| item != device),
KeychainOpKind::NodeAdd { node, .. } if !view.nodes.contains(node) => {
view.nodes.push(node.clone());
}
KeychainOpKind::NodeRevoke { node } => view.nodes.retain(|item| item != node),
_ => {}
}
}
view
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keychain_structs_roundtrip() {
let op = KeychainOp {
id: "op:1".into(),
created_at: UnixMillis(1),
kind: KeychainOpKind::UserAdd {
user: "user:eric".into(),
name: "Eric".to_owned(),
},
};
let json = serde_json::to_string(&op).expect("json");
let decoded: KeychainOp = serde_json::from_str(&json).expect("decode");
assert_eq!(decoded, op);
}
#[test]
fn keychain_signing_payload_is_canonical_and_namespaced() {
let op = KeychainOp {
id: "op:1".into(),
created_at: UnixMillis(1),
kind: KeychainOpKind::AdminKeyAdd {
key: "key:admin".into(),
},
};
assert_eq!(
keychain_signing_payload(&op).expect("payload"),
keychain_signing_payload(&op).expect("payload again")
);
assert_ne!(
keychain_signing_payload_hash(&op).expect("hash"),
geth_codec::hash_canonical(&op).expect("raw op hash")
);
let signed = signed_keychain_op(op.clone(), "key:admin".into(), vec![1, 2, 3]);
assert_eq!(signed.namespace(), KEYCHAIN_SIGNATURE_NAMESPACE);
assert_eq!(signed.payload(), &op);
}
2026-05-15 15:08:20 +02:00
}