make sshsigchain the only portable sigchain format
This commit is contained in:
parent
9e5871d02a
commit
73500e1944
15 changed files with 812 additions and 1603 deletions
|
|
@ -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!(
|
||||
|
|
|
|||
Loading…
Reference in a new issue