make sshsigchain the only portable sigchain format

This commit is contained in:
Eric Wendland 2026-07-18 21:00:06 +02:00
commit 73500e1944
15 changed files with 812 additions and 1603 deletions

View file

@ -1048,57 +1048,16 @@ pub enum KeychainCommand {
#[arg(long)]
principal: Option<String>,
},
/// Export the canonical SSH signature chain
Sigchain {
#[arg(long)]
out: Option<PathBuf>,
},
/// Build a signed static-publication bundle
PublishBundle {
#[arg(long)]
out: PathBuf,
#[arg(long, default_value = geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL)]
base_url: String,
#[arg(long)]
signing_key: PathBuf,
#[arg(long)]
admin_key: Option<PathBuf>,
#[arg(long = "snapshot")]
snapshots: Vec<String>,
},
/// Verify a signature chain without importing it
/// Verify a linked SSHSIGCHAIN JSONL file against an explicit root key
VerifySigchain {
#[arg(long = "in")]
input: PathBuf,
},
/// Verify and import a signature chain
ImportSigchain {
#[arg(long = "in")]
input: PathBuf,
},
/// Verify a published checkpoint and its discovery metadata
VerifyCheckpoint {
#[arg(long)]
checkpoint: PathBuf,
chain_id: String,
#[arg(long)]
signature: PathBuf,
#[arg(long)]
sigchain: PathBuf,
#[arg(long)]
allowed_signers: PathBuf,
#[arg(long)]
base_url: Option<String>,
#[arg(long)]
principal: Option<String>,
},
/// Fetch a published signature chain over HTTPS
Fetch {
#[arg(long, default_value = geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL)]
url: String,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
import: bool,
root_key: PathBuf,
#[arg(long, default_value = geth_keychain::SSH_SIGCHAIN_NAMESPACE)]
namespace: String,
},
/// Explain why one keychain operation was accepted or rejected
Explain { op_id: String },
@ -1785,6 +1744,8 @@ fn argument_help(path: &str, id: &str) -> Option<&'static str> {
"signature" => Some("OpenSSH signature file path"),
"allowed_signers" => Some("OpenSSH allowed_signers file used for verification"),
"base_url" => Some("Publication base URL recorded in signed discovery metadata"),
"chain_id" => Some("Pinned 32-byte SSHSIGCHAIN chain ID as 64 hexadecimal characters"),
"root_key" => Some("Pinned OpenSSH root public-key file for SSHSIGCHAIN verification"),
"snapshots" => Some("Named snapshot mapping NAME=PATH; repeatable"),
"checkpoint" => Some("Signed publication checkpoint file"),
"sigchain" => Some("Canonical keychain signature-chain JSONL file"),
@ -2671,52 +2632,20 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
allowed_signers_path: allowed_signers,
principal,
},
Command::Keychain {
command: KeychainCommand::Sigchain { out },
} => ControlRequest::KeychainSigchainExport { out },
Command::Keychain {
command:
KeychainCommand::PublishBundle {
out,
base_url,
signing_key,
admin_key,
snapshots,
KeychainCommand::VerifySigchain {
input,
chain_id,
root_key,
namespace,
},
} => ControlRequest::KeychainPublishBundle {
out,
base_url: Some(base_url),
signing_key_path: signing_key,
admin_key_path: admin_key,
snapshots,
} => ControlRequest::KeychainVerifySigchain {
input,
chain_id,
root_key_path: root_key,
namespace: Some(namespace),
},
Command::Keychain {
command: KeychainCommand::VerifySigchain { input },
} => ControlRequest::KeychainVerifySigchain { input },
Command::Keychain {
command: KeychainCommand::ImportSigchain { input },
} => ControlRequest::KeychainImportSigchain { input },
Command::Keychain {
command:
KeychainCommand::VerifyCheckpoint {
checkpoint,
signature,
sigchain,
allowed_signers,
base_url,
principal,
},
} => ControlRequest::KeychainVerifyCheckpoint {
checkpoint,
signature,
sigchain,
allowed_signers,
base_url,
principal,
},
Command::Keychain {
command: KeychainCommand::Fetch { url, out, import },
} => ControlRequest::KeychainFetch { url, out, import },
Command::Keychain {
command: KeychainCommand::Explain { op_id },
} => ControlRequest::KeychainExplain { op_id },
@ -4175,109 +4104,25 @@ fn print_response(response: ControlResponse, output: OutputMode) -> Result<()> {
println!("verified: {verified}");
eprintln!("note: {note}");
}
ControlResponse::KeychainSigchainExported {
jsonl, out, note, ..
} => {
if let Some(out) = out {
println!("wrote keychain sigchain: {}", out.display());
} else {
print!("{jsonl}");
}
eprintln!("note: {note}");
}
ControlResponse::KeychainBundlePublished {
out,
base_url,
allowed_signers_path,
sigchain_path,
checkpoint_path,
checkpoint_signature_path,
snapshots,
note,
..
} => {
println!("bundle: {}", out.display());
println!("base_url: {base_url}");
println!("allowed_signers: {}", allowed_signers_path.display());
println!("sigchain: {}", sigchain_path.display());
println!("checkpoint: {}", checkpoint_path.display());
println!(
"checkpoint_signature: {}",
checkpoint_signature_path.display()
);
for snapshot in snapshots {
println!(
"snapshot: {} {} {}",
snapshot.name,
snapshot.path.display(),
snapshot.signature_path.display()
);
}
eprintln!("note: {note}");
}
ControlResponse::KeychainSigchainFileVerified {
ControlResponse::KeychainSigchainVerified {
input,
report,
chain_id,
records,
head,
active_admin_keys,
users,
devices,
nodes,
note,
} => {
println!("sigchain: {}", input.display());
print_keychain_sigchain_report(&report);
eprintln!("note: {note}");
}
ControlResponse::KeychainSigchainImported {
input,
ops_imported,
signatures_imported,
invalid_ops_rejected,
note,
} => {
println!("sigchain: {}", input.display());
println!("ops_imported: {ops_imported}");
println!("signatures_imported: {signatures_imported}");
println!("invalid_ops_rejected: {invalid_ops_rejected}");
eprintln!("note: {note}");
}
ControlResponse::KeychainCheckpointVerified {
checkpoint,
verified,
principal,
note,
} => {
println!(
"checkpoint_head: {}",
checkpoint
.head
.as_ref()
.map(|h| h.as_str())
.unwrap_or("none")
);
println!("base_url: {}", checkpoint.base_url);
println!("verified: {verified}");
println!("principal: {}", principal.as_deref().unwrap_or("none"));
eprintln!("note: {note}");
}
ControlResponse::KeychainFetched {
url,
out,
checkpoint,
imported,
note,
} => {
println!("url: {url}");
println!("out: {}", out.display());
println!(
"checkpoint_head: {}",
checkpoint
.head
.as_ref()
.map(|h| h.as_str())
.unwrap_or("none")
);
if let Some(imported) = imported {
println!("ops_imported: {}", imported.ops_imported);
println!("signatures_imported: {}", imported.signatures_imported);
println!("invalid_ops_rejected: {}", imported.invalid_ops_rejected);
}
println!("chain_id: {chain_id}");
println!("records: {records}");
println!("head: {head}");
println!("active_admin_keys: {active_admin_keys}");
println!("users: {users}");
println!("devices: {devices}");
println!("nodes: {nodes}");
eprintln!("note: {note}");
}
ControlResponse::KeychainExplained { subject, lines } => {
@ -5473,6 +5318,41 @@ mod tests {
assert!(Cli::try_parse_from(["geth", "daemon", "logs", "--lines", "0"]).is_err());
}
#[test]
fn sshsigchain_exposes_one_pinned_verifier_not_static_compatibility_commands() {
let parsed = Cli::try_parse_from([
"geth",
"keychain",
"verify-sigchain",
"--in",
"chain.jsonl",
"--chain-id",
"00",
"--root-key",
"root.pub",
])
.expect("parse SSHSIGCHAIN verifier");
assert!(matches!(
parsed.command,
Command::Keychain {
command: KeychainCommand::VerifySigchain { .. }
}
));
for removed in [
"sigchain",
"publish-bundle",
"import-sigchain",
"verify-checkpoint",
"fetch",
"verify-sigchain-v1",
] {
assert!(
Cli::try_parse_from(["geth", "keychain", removed]).is_err(),
"removed static command {removed} must not parse"
);
}
}
#[test]
fn service_install_copies_transient_binaries_unless_explicitly_overridden() {
assert!(