Record SSH-signed keychain init ops

This commit is contained in:
Eric Wendland 2026-05-19 16:04:20 +02:00
commit 48a83c5a26
12 changed files with 283 additions and 18 deletions

View file

@ -19,7 +19,7 @@ use geth_discovery::{
};
use geth_document::{DocumentResource, DocumentState};
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
use geth_keychain::{KeychainOp, KeychainOpKind};
use geth_keychain::{KeychainOp, KeychainOpKind, KeychainOpSignature};
use geth_kv::{KvEntry, KvResource, KvSyncEntry};
use geth_pipe::{PipeConnection, PipeListener};
use geth_pubsub::PubsubMessage;
@ -34,8 +34,8 @@ use geth_ssh_identity::{
use geth_ssh_proxy::SshProxyConnection;
use geth_store::{
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredFileConflict,
StoredFileRoot, StoredKeychainOp, StoredKvEntry, StoredKvStore, StoredModuleState,
StoredPeerCard, StoredResource, StoredResourceSecret, StoredSshCertRequest,
StoredFileRoot, StoredKeychainOp, StoredKeychainSignature, StoredKvEntry, StoredKvStore,
StoredModuleState, StoredPeerCard, StoredResource, StoredResourceSecret, StoredSshCertRequest,
StoredSshCertificate, StoredSshRevocation,
};
use geth_types::{
@ -63,6 +63,8 @@ pub enum NodeError {
Db(#[from] geth_db::DbError),
#[error("control error: {0}")]
Control(#[from] geth_control::ControlError),
#[error("codec error: {0}")]
Codec(#[from] geth_codec::CodecError),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("io error: {0}")]
@ -2860,7 +2862,10 @@ pub fn handle_request(
conflict: file_conflict_from_stored(conflict)?,
})
}
ControlRequest::KeychainInit { admin_key_path } => {
ControlRequest::KeychainInit {
admin_key_path,
signing_key_path,
} => {
let mut ops = Vec::new();
let created_at = UnixMillis(geth_store::now_ms());
let init = KeychainOp {
@ -2871,7 +2876,7 @@ pub fn handle_request(
store_keychain_op(&store, &init)?;
ops.push(init);
if let Some(admin_key_path) = admin_key_path {
if let Some(admin_key_path) = admin_key_path.as_ref() {
let public_key = std::fs::read_to_string(admin_key_path)?;
let created_at = UnixMillis(geth_store::now_ms());
let admin_key = KeyId::new(ssh_public_key_fingerprint(&public_key));
@ -2884,7 +2889,25 @@ pub fn handle_request(
ops.push(op);
}
Ok(ControlResponse::KeychainInitialized { ops })
let signatures = if let Some(signing_key_path) = signing_key_path {
let signer =
keychain_signer_from_paths(&signing_key_path, admin_key_path.as_deref())?;
let mut signatures = Vec::new();
for op in &ops {
signatures.push(sign_keychain_op_with_ssh(
&store,
node,
op,
&signing_key_path,
&signer,
)?);
}
signatures
} else {
Vec::new()
};
Ok(ControlResponse::KeychainInitialized { ops, signatures })
}
ControlRequest::KeychainStatus => {
let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?);
@ -3994,6 +4017,64 @@ fn store_keychain_op(store: &Store, op: &KeychainOp) -> Result<(), NodeError> {
Ok(())
}
fn keychain_signer_from_paths(
signing_key_path: &Path,
admin_key_path: Option<&Path>,
) -> Result<KeyId, NodeError> {
let public_key_path = admin_key_path
.map(Path::to_path_buf)
.unwrap_or_else(|| Path::new(&format!("{}.pub", signing_key_path.display())).to_path_buf());
let public_key = std::fs::read_to_string(public_key_path)?;
Ok(KeyId::new(ssh_public_key_fingerprint(&public_key)))
}
fn sign_keychain_op_with_ssh(
store: &Store,
node: &LocalNode,
op: &KeychainOp,
signing_key_path: &Path,
signer: &KeyId,
) -> Result<KeychainOpSignature, NodeError> {
geth_ssh_identity::ensure_ssh_keygen_available()?;
let signature_dir = node.paths.home().join("keychain-signatures");
std::fs::create_dir_all(&signature_dir)?;
let payload_path = signature_dir.join(format!(
"{}.payload",
geth_crypto::blake3_hex(op.id.as_str().as_bytes())
));
std::fs::write(&payload_path, geth_keychain::keychain_signing_payload(op)?)?;
let output = geth_ssh_identity::sign_command(
signing_key_path,
geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE,
&payload_path,
)
.output()?;
if !output.status.success() {
return Err(geth_ssh_identity::SshIdentityError::SshKeygenFailed(
String::from_utf8_lossy(&output.stderr).trim().to_owned(),
)
.into());
}
let signature_path = Path::new(&format!("{}.sig", payload_path.display())).to_path_buf();
let signature_bytes = std::fs::read(signature_path)?;
let created_at = UnixMillis(geth_store::now_ms());
let signature = KeychainOpSignature {
op_id: op.id.clone(),
signer: signer.clone(),
namespace: geth_keychain::KEYCHAIN_SIGNATURE_NAMESPACE.to_owned(),
signature: signature_bytes,
created_at,
};
store.insert_keychain_signature(&StoredKeychainSignature {
op_id: signature.op_id.to_string(),
signer: signature.signer.to_string(),
namespace: signature.namespace.clone(),
signature: signature.signature.clone(),
created_at_ms: signature.created_at.0,
})?;
Ok(signature)
}
fn load_keychain_ops(store: &Store) -> Result<Vec<KeychainOp>, NodeError> {
store
.list_keychain_ops()?