Add static keychain publication workflow

This commit is contained in:
Eric Wendland 2026-05-27 00:59:52 +02:00
commit cfd41522d1
11 changed files with 1852 additions and 46 deletions

View file

@ -81,6 +81,8 @@ pub enum NodeError {
Control(#[from] geth_control::ControlError),
#[error("codec error: {0}")]
Codec(#[from] geth_codec::CodecError),
#[error("keychain error: {0}")]
Keychain(#[from] geth_keychain::KeychainError),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("io error: {0}")]
@ -99,6 +101,8 @@ pub enum NodeError {
InvalidInitGrant(String),
#[error("invalid enrollment capability, expected <resource>=<capability>: {0}")]
InvalidEnrollmentCapability(String),
#[error("invalid keychain snapshot, expected <name>=<path>: {0}")]
InvalidKeychainSnapshot(String),
#[error("node enrollment request not found: {0}")]
NodeEnrollmentRequestNotFound(String),
#[error("node enrollment request has invalid provenance: {0}")]
@ -7680,18 +7684,181 @@ pub fn handle_request(
note: "recorded signed admin key revocation in the keychain sigchain".to_owned(),
})
}
ControlRequest::KeychainAllowedSigners => {
ControlRequest::KeychainAllowedSigners { out } => {
let entries = geth_keychain::allowed_signers(
&load_keychain_ops(&store)?,
&load_keychain_signatures(&store)?,
);
let allowed_signers = geth_keychain::render_allowed_signers(&entries);
if let Some(path) = &out {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, &allowed_signers)?;
}
Ok(ControlResponse::KeychainAllowedSigners {
entries,
allowed_signers,
out,
note: "derived from active AdminKeyAdd/AdminKeyRevoke operations; compatible with ssh-keygen -Y allowed_signers format".to_owned(),
})
}
ControlRequest::KeychainSignFile {
input,
out,
namespace,
signing_key_path,
admin_key_path,
} => {
let signing_key_path = signing_key_path
.as_ref()
.ok_or_else(|| NodeError::SigningKeyRequired("keychain sign-file".to_owned()))?;
let namespace =
namespace.unwrap_or_else(|| geth_keychain::AUTHORIZED_KEYS_NAMESPACE.to_owned());
let entries = geth_keychain::allowed_signers(
&load_keychain_ops(&store)?,
&load_keychain_signatures(&store)?,
);
let (signer, _) =
keychain_signer_from_paths(signing_key_path, admin_key_path.as_deref())?;
if !entries.iter().any(|entry| entry.key == signer) {
return Err(NodeError::Unauthorized(format!(
"signing key {signer} is not an active keychain admin signer"
)));
}
let signature_out = sign_file_with_ssh(signing_key_path, &namespace, &input, out)?;
Ok(ControlResponse::KeychainFileSigned {
input,
out: Some(signature_out),
namespace,
signer: signer.to_string(),
note: "signed file with an active keychain admin signer; publish the file, signature, and allowed_signers projection together".to_owned(),
})
}
ControlRequest::KeychainVerifyFile {
input,
signature,
namespace,
allowed_signers_path,
principal,
} => {
let namespace =
namespace.unwrap_or_else(|| geth_keychain::AUTHORIZED_KEYS_NAMESPACE.to_owned());
let (verified, matched_principal) = verify_file_with_keychain_signers(
&store,
node,
&input,
&signature,
&namespace,
allowed_signers_path.as_deref(),
principal.as_deref(),
)?;
Ok(ControlResponse::KeychainFileVerified {
input,
signature,
namespace,
verified,
principal: matched_principal,
note: "verified detached OpenSSH signature against allowed_signers from the keychain or provided file".to_owned(),
})
}
ControlRequest::KeychainSigchainExport { out } => {
let ops = load_keychain_ops(&store)?;
let signatures = load_keychain_signatures(&store)?;
let entries = geth_keychain::sigchain_entries(&ops, &signatures);
let jsonl = geth_keychain::encode_sigchain_jsonl(&entries)?;
if let Some(path) = &out {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, &jsonl)?;
}
Ok(ControlResponse::KeychainSigchainExported {
entries,
jsonl,
out,
note: "appendable JSONL keychain sigchain export; publish with cache validators or range requests for efficient static hosting".to_owned(),
})
}
ControlRequest::KeychainPublishBundle {
out,
base_url,
signing_key_path,
admin_key_path,
snapshots,
} => publish_keychain_bundle(
&store,
node,
out,
base_url,
signing_key_path,
admin_key_path,
snapshots,
),
ControlRequest::KeychainVerifySigchain { input } => {
let text = std::fs::read_to_string(&input)?;
let entries = geth_keychain::decode_sigchain_jsonl(&text)?;
let (ops, signatures) = geth_keychain::flatten_sigchain_entries(&entries);
let report = verify_keychain_sigchain_entries_with_ssh(node, &ops, &signatures);
Ok(ControlResponse::KeychainSigchainFileVerified {
input,
report,
note: "verified JSONL sigchain by replaying operations and OpenSSH signatures"
.to_owned(),
})
}
ControlRequest::KeychainImportSigchain { input } => {
let (ops, signatures, report) = read_and_verify_sigchain_file(node, &input)?;
let imported =
import_verified_sigchain(&store, &ops, &signatures, report.rejected_ops)?;
Ok(ControlResponse::KeychainSigchainImported {
input,
ops_imported: imported.ops_imported,
signatures_imported: imported.signatures_imported,
invalid_ops_rejected: imported.invalid_ops_rejected,
note: if imported.invalid_ops_rejected == 0 {
"imported verified JSONL sigchain entries".to_owned()
} else {
"sigchain contained rejected operations; nothing imported".to_owned()
},
})
}
ControlRequest::KeychainVerifyCheckpoint {
checkpoint,
signature,
sigchain,
allowed_signers,
base_url,
principal,
} => {
let (checkpoint, verified, principal) = verify_keychain_checkpoint_files(
node,
&checkpoint,
&signature,
&sigchain,
&allowed_signers,
base_url.as_deref(),
principal.as_deref(),
)?;
Ok(ControlResponse::KeychainCheckpointVerified {
checkpoint,
verified,
principal,
note: "verified checkpoint signature, hashes, base URL, and sigchain head"
.to_owned(),
})
}
ControlRequest::KeychainFetch { url, out, import } => {
fetch_keychain_bundle(&store, node, url, out, import)
}
ControlRequest::KeychainExplain { op_id } => Ok(ControlResponse::KeychainExplained {
subject: op_id.clone(),
lines: explain_keychain_op(&store, node, &op_id)?,
}),
ControlRequest::KeychainExplainSigner { key } => Ok(ControlResponse::KeychainExplained {
subject: key.clone(),
lines: explain_keychain_signer(&store, &key)?,
}),
ControlRequest::KeychainVerify => Ok(ControlResponse::KeychainVerified {
report: verify_keychain_sigchain_with_ssh(&store, node)?,
}),
@ -10501,6 +10668,510 @@ fn store_keychain_op(store: &Store, op: &KeychainOp) -> Result<(), NodeError> {
Ok(())
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
struct StaticKeychainSourceState {
head: Option<AuthOpId>,
ops: usize,
sigchain_hash: String,
generated_at_ms: i64,
}
fn read_and_verify_sigchain_file(
node: &LocalNode,
input: &Path,
) -> Result<
(
Vec<KeychainOp>,
Vec<KeychainOpSignature>,
geth_keychain::KeychainSigchainReport,
),
NodeError,
> {
let text = std::fs::read_to_string(input)?;
let entries = geth_keychain::decode_sigchain_jsonl(&text)?;
let (ops, signatures) = geth_keychain::flatten_sigchain_entries(&entries);
let report = verify_keychain_sigchain_entries_with_ssh(node, &ops, &signatures);
Ok((ops, signatures, report))
}
fn import_verified_sigchain(
store: &Store,
ops: &[KeychainOp],
signatures: &[KeychainOpSignature],
rejected_ops: usize,
) -> Result<geth_control::KeychainFetchImportReport, NodeError> {
if rejected_ops != 0 {
return Ok(geth_control::KeychainFetchImportReport {
ops_imported: 0,
signatures_imported: 0,
invalid_ops_rejected: rejected_ops,
});
}
let existing_ops = load_keychain_ops(store)?
.into_iter()
.map(|op| op.id)
.collect::<BTreeSet<_>>();
let existing_signatures = load_keychain_signatures(store)?
.into_iter()
.map(|signature| {
(
signature.op_id,
signature.signer,
signature.namespace,
signature.signature,
)
})
.collect::<BTreeSet<_>>();
let mut ops_imported = 0;
for op in ops {
if !existing_ops.contains(&op.id) {
store_keychain_op(store, op)?;
ops_imported += 1;
}
}
let mut signatures_imported = 0;
for signature in signatures {
let key = (
signature.op_id.clone(),
signature.signer.clone(),
signature.namespace.clone(),
signature.signature.clone(),
);
if !existing_signatures.contains(&key) {
store.insert_keychain_signature(&StoredKeychainSignature {
op_id: signature.op_id.to_string(),
signer: signature.signer.to_string(),
signer_public_key: signature.signer_public_key.clone(),
namespace: signature.namespace.clone(),
signature: signature.signature.clone(),
created_at_ms: signature.created_at.0,
})?;
signatures_imported += 1;
}
}
Ok(geth_control::KeychainFetchImportReport {
ops_imported,
signatures_imported,
invalid_ops_rejected: 0,
})
}
fn verify_keychain_checkpoint_files(
node: &LocalNode,
checkpoint_path: &Path,
signature_path: &Path,
sigchain_path: &Path,
allowed_signers_path: &Path,
expected_base_url: Option<&str>,
principal: Option<&str>,
) -> Result<(geth_keychain::KeychainCheckpoint, bool, Option<String>), NodeError> {
let checkpoint_text = std::fs::read_to_string(checkpoint_path)?;
let checkpoint: geth_keychain::KeychainCheckpoint = serde_json::from_str(&checkpoint_text)?;
if let Some(expected) = expected_base_url {
let expected = geth_keychain::normalize_base_url(expected.to_owned());
if checkpoint.base_url != expected {
return Ok((checkpoint, false, None));
}
}
let sigchain_text = std::fs::read_to_string(sigchain_path)?;
let allowed_signers = std::fs::read_to_string(allowed_signers_path)?;
if checkpoint.sigchain_bytes != sigchain_text.len() as u64
|| checkpoint.sigchain_hash != geth_keychain::blake3_tagged_hash(sigchain_text.as_bytes())
|| checkpoint.allowed_signers_hash
!= geth_keychain::blake3_tagged_hash(allowed_signers.as_bytes())
{
return Ok((checkpoint, false, None));
}
let entries = geth_keychain::decode_sigchain_jsonl(&sigchain_text)?;
let (ops, signatures) = geth_keychain::flatten_sigchain_entries(&entries);
let report = verify_keychain_sigchain_entries_with_ssh(node, &ops, &signatures);
if report.rejected_ops != 0 || report.accepted_head != checkpoint.head {
return Ok((checkpoint, false, None));
}
let (verified, matched_principal) = verify_file_with_keychain_signers(
&Store::open(&node.paths.metadata_db())?,
node,
checkpoint_path,
signature_path,
geth_keychain::KEYCHAIN_CHECKPOINT_NAMESPACE,
Some(allowed_signers_path),
principal,
)?;
Ok((checkpoint, verified, matched_principal))
}
fn fetch_keychain_bundle(
store: &Store,
node: &LocalNode,
url: String,
out: Option<PathBuf>,
import: bool,
) -> Result<ControlResponse, NodeError> {
let base_url = geth_keychain::normalize_base_url(url.clone());
let out = out.unwrap_or_else(|| {
node.paths
.home()
.join("keychain-fetch")
.join(geth_crypto::blake3_hex(base_url.as_bytes()))
});
std::fs::create_dir_all(&out)?;
for name in [
"allowed_signers",
"geth.sigchain.jsonl",
"geth.sigchain.checkpoint.json",
"geth.sigchain.checkpoint.json.sig",
] {
fetch_bundle_file(&base_url, name, &out.join(name))?;
}
let checkpoint_path = out.join("geth.sigchain.checkpoint.json");
let signature_path = out.join("geth.sigchain.checkpoint.json.sig");
let sigchain_path = out.join("geth.sigchain.jsonl");
let allowed_signers_path = out.join("allowed_signers");
let (checkpoint, verified, _) = verify_keychain_checkpoint_files(
node,
&checkpoint_path,
&signature_path,
&sigchain_path,
&allowed_signers_path,
None,
None,
)?;
if !verified {
return Err(NodeError::Unauthorized(
"fetched keychain checkpoint did not verify".to_owned(),
));
}
check_static_source_rollback(store, &base_url, &checkpoint)?;
let imported = if import {
let (ops, signatures, report) = read_and_verify_sigchain_file(node, &sigchain_path)?;
let imported = import_verified_sigchain(store, &ops, &signatures, report.rejected_ops)?;
if imported.invalid_ops_rejected == 0 {
remember_static_source_checkpoint(store, &base_url, &checkpoint)?;
}
Some(imported)
} else {
None
};
let note = format!(
"fetched and verified static SSH sigchain bundle; checkpoint base URL is {}",
checkpoint.base_url
);
Ok(ControlResponse::KeychainFetched {
url: base_url,
out,
checkpoint,
imported,
note,
})
}
fn fetch_bundle_file(base_url: &str, name: &str, out: &Path) -> Result<(), NodeError> {
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)?;
}
if let Some(root) = base_url.strip_prefix("file://") {
std::fs::copy(Path::new(root).join(name), out)?;
return Ok(());
}
if base_url.starts_with("http://") || base_url.starts_with("https://") {
let url = format!("{base_url}{name}");
let output = std::process::Command::new("curl")
.arg("-fsSL")
.arg(&url)
.arg("-o")
.arg(out)
.output()?;
if output.status.success() {
return Ok(());
}
return Err(NodeError::IrohPeer(format!(
"curl failed fetching {url}: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
std::fs::copy(Path::new(base_url).join(name), out)?;
Ok(())
}
fn static_source_state_key(base_url: &str) -> String {
format!(
"keychain-static-source:{}",
geth_crypto::blake3_hex(base_url.as_bytes())
)
}
fn check_static_source_rollback(
store: &Store,
base_url: &str,
checkpoint: &geth_keychain::KeychainCheckpoint,
) -> Result<(), NodeError> {
let Some(state) = store.get_module_state(&static_source_state_key(base_url))? else {
return Ok(());
};
let previous: StaticKeychainSourceState = serde_json::from_str(&state.state_json)?;
if previous.generated_at_ms > checkpoint.generated_at.0 || previous.ops > checkpoint.ops {
return Err(NodeError::Unauthorized(
"fetched keychain checkpoint is older than the last accepted checkpoint".to_owned(),
));
}
Ok(())
}
fn remember_static_source_checkpoint(
store: &Store,
base_url: &str,
checkpoint: &geth_keychain::KeychainCheckpoint,
) -> Result<(), NodeError> {
let state = StaticKeychainSourceState {
head: checkpoint.head.clone(),
ops: checkpoint.ops,
sigchain_hash: checkpoint.sigchain_hash.clone(),
generated_at_ms: checkpoint.generated_at.0,
};
store.put_module_state(&StoredModuleState {
module: static_source_state_key(base_url),
state_json: serde_json::to_string(&state)?,
updated_at_ms: geth_store::now_ms(),
})?;
Ok(())
}
fn explain_keychain_op(
store: &Store,
node: &LocalNode,
op_id: &str,
) -> Result<Vec<String>, NodeError> {
let ops = load_keychain_ops(store)?;
let signatures = load_keychain_signatures(store)?;
let Some(op) = ops.iter().find(|op| op.id.as_str() == op_id) else {
return Ok(vec![format!("operation not found: {op_id}")]);
};
let sorted = geth_keychain::sorted_keychain_ops(ops.clone());
let position = sorted.iter().position(|candidate| candidate.id == op.id);
let op_signatures = signatures
.iter()
.filter(|signature| signature.op_id == op.id)
.collect::<Vec<_>>();
let report = verify_keychain_sigchain_entries_with_ssh(node, &ops, &signatures);
let mut lines = Vec::new();
lines.push(format!("op_id: {}", op.id));
lines.push(format!("created_at_ms: {}", op.created_at.0));
lines.push(format!("kind: {:?}", op.kind));
if let Some(position) = position {
lines.push(format!("position: {position}"));
}
lines.push(format!("signatures: {}", op_signatures.len()));
for signature in op_signatures {
let claimed = geth_keychain::signature_uses_claimed_key(signature);
let verified = verify_keychain_signature_with_ssh(
node,
op,
&stored_keychain_signature_from_signature(signature),
)
.unwrap_or(false);
lines.push(format!(
"signature signer={} namespace={} claimed_key={} cryptographic_verify={}",
signature.signer, signature.namespace, claimed, verified
));
}
lines.push(format!(
"replay_accepted_head: {}",
report
.accepted_head
.as_ref()
.map(|head| head.as_str())
.unwrap_or("none")
));
lines.push(format!("replay_rejected_ops: {}", report.rejected_ops));
let accepted = report.accepted_head.as_ref().is_some_and(|_| {
let accepted_ops = report.accepted_ops;
sorted
.iter()
.take(accepted_ops)
.any(|accepted_op| accepted_op.id == op.id)
});
lines.push(format!("accepted_by_replay: {accepted}"));
Ok(lines)
}
fn explain_keychain_signer(store: &Store, key: &str) -> Result<Vec<String>, NodeError> {
let ops = load_keychain_ops(store)?;
let signatures = load_keychain_signatures(store)?;
let key_id = KeyId::new(key.to_owned());
let allowed = geth_keychain::allowed_signers(&ops, &signatures);
let mut lines = Vec::new();
lines.push(format!("key: {key}"));
if let Some(entry) = allowed.iter().find(|entry| entry.key == key_id) {
lines.push("active_admin_signer: true".to_owned());
lines.push(format!("principal: {}", entry.principal));
lines.push(format!("public_key: {}", entry.public_key.trim()));
} else {
lines.push("active_admin_signer: false".to_owned());
}
for op in geth_keychain::sorted_keychain_ops(ops) {
match &op.kind {
KeychainOpKind::AdminKeyAdd { key: op_key, .. } if op_key == &key_id => {
lines.push(format!("added_by_op: {} at {}", op.id, op.created_at.0));
}
KeychainOpKind::AdminKeyRevoke { key: op_key } if op_key == &key_id => {
lines.push(format!("revoked_by_op: {} at {}", op.id, op.created_at.0));
}
_ => {}
}
}
let signed = signatures
.iter()
.filter(|signature| signature.signer == key_id)
.count();
lines.push(format!("signed_operations: {signed}"));
Ok(lines)
}
fn publish_keychain_bundle(
store: &Store,
_node: &LocalNode,
out: PathBuf,
base_url: Option<String>,
signing_key_path: PathBuf,
admin_key_path: Option<PathBuf>,
snapshots: Vec<String>,
) -> Result<ControlResponse, NodeError> {
let base_url = base_url
.map(geth_keychain::normalize_base_url)
.unwrap_or_else(|| geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL.to_owned());
let entries = geth_keychain::allowed_signers(
&load_keychain_ops(store)?,
&load_keychain_signatures(store)?,
);
let (signer, _) = keychain_signer_from_paths(&signing_key_path, admin_key_path.as_deref())?;
if !entries.iter().any(|entry| entry.key == signer) {
return Err(NodeError::Unauthorized(format!(
"signing key {signer} is not an active keychain admin signer"
)));
}
std::fs::create_dir_all(&out)?;
let ops = load_keychain_ops(store)?;
let signatures = load_keychain_signatures(store)?;
let sigchain_entries = geth_keychain::sigchain_entries(&ops, &signatures);
let sigchain_jsonl = geth_keychain::encode_sigchain_jsonl(&sigchain_entries)?;
let allowed_signers = geth_keychain::render_allowed_signers(&entries);
let allowed_signers_path = out.join("allowed_signers");
let sigchain_path = out.join("geth.sigchain.jsonl");
let checkpoint_path = out.join("geth.sigchain.checkpoint.json");
std::fs::write(&allowed_signers_path, &allowed_signers)?;
std::fs::write(&sigchain_path, &sigchain_jsonl)?;
let checkpoint = geth_keychain::keychain_checkpoint(
&ops,
&signatures,
&sigchain_jsonl,
&allowed_signers,
base_url.clone(),
UnixMillis(geth_store::now_ms()),
)?;
std::fs::write(&checkpoint_path, serde_json::to_vec_pretty(&checkpoint)?)?;
let checkpoint_signature_path = sign_file_with_ssh(
&signing_key_path,
"geth.sigchain-checkpoint.v1@eric.wendland.dev",
&checkpoint_path,
Some(out.join("geth.sigchain.checkpoint.json.sig")),
)?;
let mut published_snapshots = Vec::new();
for snapshot in snapshots {
let (name, source) = parse_snapshot_arg(&snapshot)?;
let path = out.join(&name);
std::fs::copy(&source, &path)?;
let namespace = snapshot_namespace(&name);
let signature_path = sign_file_with_ssh(
&signing_key_path,
&namespace,
&path,
Some(out.join(format!("{name}.sig"))),
)?;
published_snapshots.push(geth_control::KeychainPublishedSnapshot {
name,
source,
path,
signature_path,
namespace,
});
}
Ok(ControlResponse::KeychainBundlePublished {
out,
base_url,
allowed_signers_path,
sigchain_path,
checkpoint_path,
checkpoint_signature_path,
checkpoint,
snapshots: published_snapshots,
note:
"published static SSH sigchain bundle; serve this directory at the discovery base URL"
.to_owned(),
})
}
fn verify_keychain_sigchain_entries_with_ssh(
node: &LocalNode,
ops: &[KeychainOp],
signatures: &[KeychainOpSignature],
) -> geth_keychain::KeychainSigchainReport {
struct SshKeychainVerifier<'a> {
node: &'a LocalNode,
}
impl geth_keychain::KeychainSignatureVerifier for SshKeychainVerifier<'_> {
fn verify_keychain_signature(
&self,
op: &KeychainOp,
signature: &KeychainOpSignature,
) -> bool {
verify_keychain_signature_with_ssh(
self.node,
op,
&stored_keychain_signature_from_signature(signature),
)
.unwrap_or(false)
}
}
let verifier = SshKeychainVerifier { node };
geth_keychain::verify_sigchain(ops, signatures, &verifier)
}
fn parse_snapshot_arg(value: &str) -> Result<(String, PathBuf), NodeError> {
let Some((name, path)) = value.split_once('=') else {
return Err(NodeError::InvalidKeychainSnapshot(value.to_owned()));
};
validate_snapshot_name(name)?;
let path = PathBuf::from(path);
if !path.is_file() {
return Err(NodeError::InvalidKeychainSnapshot(value.to_owned()));
}
Ok((name.to_owned(), path))
}
fn validate_snapshot_name(name: &str) -> Result<(), NodeError> {
let valid = !name.is_empty()
&& name != "."
&& name != ".."
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'));
if valid {
Ok(())
} else {
Err(NodeError::InvalidKeychainSnapshot(name.to_owned()))
}
}
fn snapshot_namespace(name: &str) -> String {
format!("geth.snapshot.{name}.v1@eric.wendland.dev")
}
fn verify_keychain_sigchain_with_ssh(
store: &Store,
node: &LocalNode,
@ -10562,9 +11233,16 @@ fn keychain_signer_from_paths(
signing_key_path: &Path,
admin_key_path: Option<&Path>,
) -> Result<(KeyId, String), 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_path = admin_key_path.map(Path::to_path_buf).unwrap_or_else(|| {
if signing_key_path
.extension()
.is_some_and(|extension| extension == "pub")
{
signing_key_path.to_path_buf()
} 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)),
@ -10572,6 +11250,131 @@ fn keychain_signer_from_paths(
))
}
fn sign_file_with_ssh(
signing_key_path: &Path,
namespace: &str,
input_path: &Path,
out: Option<PathBuf>,
) -> Result<PathBuf, NodeError> {
geth_ssh_identity::ensure_ssh_keygen_available()?;
let signature_path = Path::new(&format!("{}.sig", input_path.display())).to_path_buf();
if signature_path.exists() {
std::fs::remove_file(&signature_path)?;
}
let output =
geth_ssh_identity::sign_command(signing_key_path, namespace, input_path).output()?;
if !output.status.success() {
return Err(geth_ssh_identity::SshIdentityError::SshKeygenFailed(
String::from_utf8_lossy(&output.stderr).trim().to_owned(),
)
.into());
}
let Some(out) = out else {
return Ok(signature_path);
};
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)?;
}
if out != signature_path {
if out.exists() {
std::fs::remove_file(&out)?;
}
match std::fs::rename(&signature_path, &out) {
Ok(()) => {}
Err(_) => {
std::fs::copy(&signature_path, &out)?;
std::fs::remove_file(&signature_path)?;
}
}
}
Ok(out)
}
fn verify_file_with_keychain_signers(
store: &Store,
node: &LocalNode,
input_path: &Path,
signature_path: &Path,
namespace: &str,
allowed_signers_path: Option<&Path>,
principal: Option<&str>,
) -> Result<(bool, Option<String>), NodeError> {
geth_ssh_identity::ensure_ssh_keygen_available()?;
let verify_dir = node
.paths
.home()
.join("keychain-signatures")
.join("file-verify");
std::fs::create_dir_all(&verify_dir)?;
let stable_id = geth_crypto::blake3_hex(
format!(
"{}\0{}\0{}\0{}",
input_path.display(),
signature_path.display(),
namespace,
principal.unwrap_or("")
)
.as_bytes(),
);
let owned_allowed_signers_path;
let allowed_signers_path = if let Some(path) = allowed_signers_path {
path
} else {
let entries = geth_keychain::allowed_signers(
&load_keychain_ops(store)?,
&load_keychain_signatures(store)?,
);
let allowed_signers = geth_keychain::render_allowed_signers(&entries);
owned_allowed_signers_path = verify_dir.join(format!("{stable_id}.allowed-signers"));
std::fs::write(&owned_allowed_signers_path, allowed_signers)?;
&owned_allowed_signers_path
};
let principals = if let Some(principal) = principal {
vec![principal.to_owned()]
} else {
read_allowed_signer_principals(allowed_signers_path)?
};
for principal in principals {
let payload = std::fs::File::open(input_path)?;
let output = std::process::Command::new("ssh-keygen")
.arg("-Y")
.arg("verify")
.arg("-f")
.arg(allowed_signers_path)
.arg("-I")
.arg(&principal)
.arg("-n")
.arg(namespace)
.arg("-s")
.arg(signature_path)
.stdin(std::process::Stdio::from(payload))
.output()?;
if output.status.success() {
return Ok((true, Some(principal)));
}
}
Ok((false, None))
}
fn read_allowed_signer_principals(path: &Path) -> Result<Vec<String>, NodeError> {
let text = std::fs::read_to_string(path)?;
let mut principals = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(field) = line.split_whitespace().next() {
for principal in field.split(',') {
if !principal.is_empty() && !principals.iter().any(|item| item == principal) {
principals.push(principal.to_owned());
}
}
}
}
Ok(principals)
}
fn sign_keychain_op_with_ssh(
store: &Store,
node: &LocalNode,
@ -11167,15 +11970,18 @@ async fn start_daemon_iroh_endpoint(
let store = Store::open(&node.paths.metadata_db())?;
store.upsert_node_endpoint(endpoint_id, &node.node_id, &node.agent_id, "iroh")?;
}
let blob_store =
iroh_blobs::store::fs::FsStore::load(node.paths.cas_dir().join("iroh-blobs"))
.await
.map_err(|error| {
NodeError::IrohPeer(format!("failed to open iroh-blobs store: {error}"))
})?;
let iroh_blobs_path = node.paths.cas_dir().join("iroh-blobs");
std::fs::create_dir_all(&iroh_blobs_path)?;
let iroh_docs_path = node.paths.home().join("iroh-docs");
std::fs::create_dir_all(&iroh_docs_path)?;
let blob_store = iroh_blobs::store::fs::FsStore::load(iroh_blobs_path)
.await
.map_err(|error| {
NodeError::IrohPeer(format!("failed to open iroh-blobs store: {error}"))
})?;
let gossip = iroh_gossip::net::Gossip::builder().spawn(endpoint.endpoint());
let blob_api: iroh_blobs::api::Store = blob_store.clone().into();
let docs = iroh_docs::protocol::Docs::persistent(node.paths.home().join("iroh-docs"))
let docs = iroh_docs::protocol::Docs::persistent(iroh_docs_path)
.spawn(endpoint.endpoint(), blob_api, gossip.clone())
.await
.map_err(|error| {