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

@ -2627,9 +2627,11 @@ fn keychain_admin_sigchain_exports_allowed_signers_and_verifies() {
other => panic!("unexpected response: {other:?}"),
}
let response =
geth_node::handle_request(&node, geth_control::ControlRequest::KeychainAllowedSigners)
.expect("allowed signers");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainAllowedSigners { out: None },
)
.expect("allowed signers");
match response {
geth_control::ControlResponse::KeychainAllowedSigners {
entries,
@ -2642,6 +2644,278 @@ fn keychain_admin_sigchain_exports_allowed_signers_and_verifies() {
other => panic!("unexpected response: {other:?}"),
}
let allowed_signers_path = home.path().join("allowed_signers");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainAllowedSigners {
out: Some(allowed_signers_path.clone()),
},
)
.expect("allowed signers file");
match response {
geth_control::ControlResponse::KeychainAllowedSigners { out, .. } => {
assert_eq!(out.as_deref(), Some(allowed_signers_path.as_path()));
let allowed_signers_file =
std::fs::read_to_string(&allowed_signers_path).expect("read allowed_signers");
assert!(allowed_signers_file.contains("second-admin ssh-ed25519 "));
}
other => panic!("unexpected response: {other:?}"),
}
let authorized_keys_path = home.path().join("authorized_keys");
std::fs::write(
&authorized_keys_path,
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGexample external-login-key\n",
)
.expect("write authorized_keys snapshot");
let authorized_keys_signature_path = home.path().join("authorized_keys.sig");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainSignFile {
input: authorized_keys_path.clone(),
out: Some(authorized_keys_signature_path.clone()),
namespace: None,
signing_key_path: Some(admin_key_path.clone()),
admin_key_path: Some(admin_key_path.with_extension("pub")),
},
)
.expect("sign authorized_keys snapshot");
match response {
geth_control::ControlResponse::KeychainFileSigned {
input,
out,
namespace,
signer,
..
} => {
assert_eq!(input, authorized_keys_path);
assert_eq!(
out.as_deref(),
Some(authorized_keys_signature_path.as_path())
);
assert_eq!(namespace, geth_keychain::AUTHORIZED_KEYS_NAMESPACE);
assert!(!signer.is_empty());
assert!(authorized_keys_signature_path.exists());
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainVerifyFile {
input: authorized_keys_path.clone(),
signature: authorized_keys_signature_path.clone(),
namespace: None,
allowed_signers_path: Some(allowed_signers_path.clone()),
principal: None,
},
)
.expect("verify authorized_keys snapshot");
match response {
geth_control::ControlResponse::KeychainFileVerified {
verified,
principal,
..
} => {
assert!(verified);
assert_eq!(principal.as_deref(), Some("admin"));
}
other => panic!("unexpected response: {other:?}"),
}
let sigchain_path = home.path().join("keychain.sigchain.jsonl");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainSigchainExport {
out: Some(sigchain_path.clone()),
},
)
.expect("sigchain export");
match response {
geth_control::ControlResponse::KeychainSigchainExported { entries, out, .. } => {
assert_eq!(out.as_deref(), Some(sigchain_path.as_path()));
assert!(entries.len() >= 3);
let jsonl = std::fs::read_to_string(&sigchain_path).expect("read sigchain");
let decoded = geth_keychain::decode_sigchain_jsonl(&jsonl).expect("decode sigchain");
assert_eq!(decoded, entries);
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainVerifySigchain {
input: sigchain_path.clone(),
},
)
.expect("verify sigchain file");
match response {
geth_control::ControlResponse::KeychainSigchainFileVerified { report, .. } => {
assert_eq!(report.rejected_ops, 0);
assert_eq!(report.active_admin_keys, 2);
}
other => panic!("unexpected response: {other:?}"),
}
let bundle_dir = home.path().join("public-bundle");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainPublishBundle {
out: bundle_dir.clone(),
base_url: None,
signing_key_path: admin_key_path.clone(),
admin_key_path: Some(admin_key_path.with_extension("pub")),
snapshots: vec![format!(
"authorized_keys={}",
authorized_keys_path.display()
)],
},
)
.expect("publish bundle");
match response {
geth_control::ControlResponse::KeychainBundlePublished {
base_url,
allowed_signers_path,
sigchain_path: bundle_sigchain_path,
checkpoint_path,
checkpoint_signature_path,
checkpoint,
snapshots,
..
} => {
assert_eq!(base_url, geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL);
assert!(allowed_signers_path.exists());
assert!(bundle_sigchain_path.exists());
assert!(checkpoint_path.exists());
assert!(checkpoint_signature_path.exists());
assert_eq!(
checkpoint.base_url,
geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL
);
assert_eq!(snapshots.len(), 1);
assert_eq!(snapshots[0].name, "authorized_keys");
assert!(snapshots[0].path.exists());
assert!(snapshots[0].signature_path.exists());
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainVerifyCheckpoint {
checkpoint: bundle_dir.join("geth.sigchain.checkpoint.json"),
signature: bundle_dir.join("geth.sigchain.checkpoint.json.sig"),
sigchain: bundle_dir.join("geth.sigchain.jsonl"),
allowed_signers: bundle_dir.join("allowed_signers"),
base_url: Some(geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL.to_owned()),
principal: None,
},
)
.expect("verify checkpoint");
match response {
geth_control::ControlResponse::KeychainCheckpointVerified {
verified,
principal,
..
} => {
assert!(verified);
assert_eq!(principal.as_deref(), Some("admin"));
}
other => panic!("unexpected response: {other:?}"),
}
let fetched_home = tempfile::tempdir().expect("fetch tempdir");
let fetched_paths = geth_config::GethPaths::from_home(fetched_home.path());
let fetched_node = geth_node::init_node(&fetched_paths).expect("init fetched node");
let response = geth_node::handle_request(
&fetched_node,
geth_control::ControlRequest::KeychainFetch {
url: format!("file://{}", bundle_dir.display()),
out: Some(fetched_home.path().join("bundle")),
import: true,
},
)
.expect("fetch bundle");
match response {
geth_control::ControlResponse::KeychainFetched {
imported: Some(imported),
checkpoint,
..
} => {
assert!(imported.ops_imported >= 3);
assert!(imported.signatures_imported >= 3);
assert_eq!(imported.invalid_ops_rejected, 0);
assert_eq!(
checkpoint.base_url,
geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL
);
}
other => panic!("unexpected response: {other:?}"),
}
let jsonl = std::fs::read_to_string(&sigchain_path).expect("read sigchain for explain");
let decoded =
geth_keychain::decode_sigchain_jsonl(&jsonl).expect("decode sigchain for explain");
let explain_op_id = decoded.last().expect("last sigchain op").op.id.to_string();
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainExplain {
op_id: explain_op_id.clone(),
},
)
.expect("explain op");
match response {
geth_control::ControlResponse::KeychainExplained { subject, lines } => {
assert_eq!(subject, explain_op_id);
assert!(lines.iter().any(|line| line.contains("accepted_by_replay")));
}
other => panic!("unexpected response: {other:?}"),
}
let admin_public_key =
std::fs::read_to_string(admin_key_path.with_extension("pub")).expect("admin pub");
let admin_key = geth_keychain::admin_key_fingerprint(&admin_public_key);
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainExplainSigner { key: admin_key },
)
.expect("explain signer");
match response {
geth_control::ControlResponse::KeychainExplained { lines, .. } => {
assert!(lines.iter().any(|line| line == "active_admin_signer: true"));
assert!(
lines
.iter()
.any(|line| line.starts_with("signed_operations:"))
);
}
other => panic!("unexpected response: {other:?}"),
}
let import_home = tempfile::tempdir().expect("import tempdir");
let import_paths = geth_config::GethPaths::from_home(import_home.path());
let import_node = geth_node::init_node(&import_paths).expect("init import node");
let response = geth_node::handle_request(
&import_node,
geth_control::ControlRequest::KeychainImportSigchain {
input: sigchain_path.clone(),
},
)
.expect("import sigchain file");
match response {
geth_control::ControlResponse::KeychainSigchainImported {
ops_imported,
signatures_imported,
invalid_ops_rejected,
..
} => {
assert!(ops_imported >= 3);
assert!(signatures_imported >= 3);
assert_eq!(invalid_ops_rejected, 0);
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(&node, geth_control::ControlRequest::KeychainVerify)
.expect("verify sigchain");
match response {