Add static keychain publication workflow
This commit is contained in:
parent
b6ffcde54c
commit
cfd41522d1
11 changed files with 1852 additions and 46 deletions
|
|
@ -153,6 +153,24 @@ const GUIDE_KEYS: &str = r#"Key terminology:
|
|||
shells out to ssh-keygen -Y sign with explicit namespaces to sign canonical
|
||||
geth keychain/auth operations. The private key is not copied into geth state.
|
||||
|
||||
Signing sources:
|
||||
Local key file:
|
||||
--signing-key ~/.ssh/id_ed25519 --admin-key ~/.ssh/id_ed25519.pub
|
||||
|
||||
Encrypted key file:
|
||||
Load it into ssh-agent with `ssh-add ~/.ssh/id_ed25519`, then sign through
|
||||
the agent by passing the public key path:
|
||||
--signing-key ~/.ssh/id_ed25519.pub
|
||||
|
||||
FIDO/YubiKey OpenSSH key:
|
||||
Use the security-key stub or load it into ssh-agent:
|
||||
--signing-key ~/.ssh/id_ed25519_sk --admin-key ~/.ssh/id_ed25519_sk.pub
|
||||
|
||||
PKCS#11:
|
||||
Direct ssh-keygen -Y signing does not expose a portable -D provider option.
|
||||
Load the token key into ssh-agent with `ssh-add -s <provider>`, then pass
|
||||
the public key path with --signing-key.
|
||||
|
||||
Examples:
|
||||
Software key:
|
||||
--admin-key ~/.ssh/id_ed25519.pub --signing-key ~/.ssh/id_ed25519
|
||||
|
|
@ -160,6 +178,15 @@ Examples:
|
|||
YubiKey/FIDO OpenSSH key:
|
||||
--admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk
|
||||
|
||||
Generate the active OpenSSH allowed_signers projection:
|
||||
geth keychain allowed-signers --out ~/.config/geth/allowed_signers
|
||||
|
||||
Sign an arbitrary authorized_keys snapshot with an active admin key:
|
||||
geth keychain sign-file --in ~/.ssh/authorized_keys --out ~/.ssh/authorized_keys.sig --signing-key ~/.ssh/id_ed25519_sk
|
||||
|
||||
Verify the snapshot signature against the current keychain trust root:
|
||||
geth keychain verify-file --in ~/.ssh/authorized_keys --signature ~/.ssh/authorized_keys.sig
|
||||
|
||||
If you use --owner, --node-name, or --capability during init, geth requires both
|
||||
key options because those fields create signed owner/device/node statements.
|
||||
"#;
|
||||
|
|
@ -654,7 +681,86 @@ pub enum KeychainCommand {
|
|||
#[arg(long)]
|
||||
admin_key: Option<PathBuf>,
|
||||
},
|
||||
AllowedSigners,
|
||||
AllowedSigners {
|
||||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
},
|
||||
SignFile {
|
||||
#[arg(long = "in")]
|
||||
input: PathBuf,
|
||||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
namespace: Option<String>,
|
||||
#[arg(long)]
|
||||
signing_key: PathBuf,
|
||||
#[arg(long)]
|
||||
admin_key: Option<PathBuf>,
|
||||
},
|
||||
VerifyFile {
|
||||
#[arg(long = "in")]
|
||||
input: PathBuf,
|
||||
#[arg(long)]
|
||||
signature: PathBuf,
|
||||
#[arg(long)]
|
||||
namespace: Option<String>,
|
||||
#[arg(long)]
|
||||
allowed_signers: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
principal: Option<String>,
|
||||
},
|
||||
Sigchain {
|
||||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
},
|
||||
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>,
|
||||
},
|
||||
VerifySigchain {
|
||||
#[arg(long = "in")]
|
||||
input: PathBuf,
|
||||
},
|
||||
ImportSigchain {
|
||||
#[arg(long = "in")]
|
||||
input: PathBuf,
|
||||
},
|
||||
VerifyCheckpoint {
|
||||
#[arg(long)]
|
||||
checkpoint: PathBuf,
|
||||
#[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 {
|
||||
#[arg(long, default_value = geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL)]
|
||||
url: String,
|
||||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
import: bool,
|
||||
},
|
||||
Explain {
|
||||
op_id: String,
|
||||
},
|
||||
ExplainSigner {
|
||||
key: String,
|
||||
},
|
||||
Verify,
|
||||
Sync {
|
||||
node: String,
|
||||
|
|
@ -1482,8 +1588,92 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
admin_key_path: admin_key,
|
||||
},
|
||||
Command::Keychain {
|
||||
command: KeychainCommand::AllowedSigners,
|
||||
} => ControlRequest::KeychainAllowedSigners,
|
||||
command: KeychainCommand::AllowedSigners { out },
|
||||
} => ControlRequest::KeychainAllowedSigners { out },
|
||||
Command::Keychain {
|
||||
command:
|
||||
KeychainCommand::SignFile {
|
||||
input,
|
||||
out,
|
||||
namespace,
|
||||
signing_key,
|
||||
admin_key,
|
||||
},
|
||||
} => ControlRequest::KeychainSignFile {
|
||||
input,
|
||||
out,
|
||||
namespace,
|
||||
signing_key_path: Some(signing_key),
|
||||
admin_key_path: admin_key,
|
||||
},
|
||||
Command::Keychain {
|
||||
command:
|
||||
KeychainCommand::VerifyFile {
|
||||
input,
|
||||
signature,
|
||||
namespace,
|
||||
allowed_signers,
|
||||
principal,
|
||||
},
|
||||
} => ControlRequest::KeychainVerifyFile {
|
||||
input,
|
||||
signature,
|
||||
namespace,
|
||||
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,
|
||||
},
|
||||
} => ControlRequest::KeychainPublishBundle {
|
||||
out,
|
||||
base_url: Some(base_url),
|
||||
signing_key_path: signing_key,
|
||||
admin_key_path: admin_key,
|
||||
snapshots,
|
||||
},
|
||||
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 },
|
||||
Command::Keychain {
|
||||
command: KeychainCommand::ExplainSigner { key },
|
||||
} => ControlRequest::KeychainExplainSigner { key },
|
||||
Command::Keychain {
|
||||
command: KeychainCommand::Verify,
|
||||
} => ControlRequest::KeychainVerify,
|
||||
|
|
@ -1990,6 +2180,23 @@ fn service_executable(bin: Option<PathBuf>) -> Result<PathBuf> {
|
|||
.context("resolve current geth executable")
|
||||
}
|
||||
|
||||
fn print_keychain_sigchain_report(report: &geth_keychain::KeychainSigchainReport) {
|
||||
println!("ops: {}", report.ops);
|
||||
println!("signatures: {}", report.signatures);
|
||||
println!("accepted_ops: {}", report.accepted_ops);
|
||||
println!("rejected_ops: {}", report.rejected_ops);
|
||||
println!("active_admin_keys: {}", report.active_admin_keys);
|
||||
println!(
|
||||
"accepted_head: {}",
|
||||
report
|
||||
.accepted_head
|
||||
.as_ref()
|
||||
.map(|head| head.as_str())
|
||||
.unwrap_or("none")
|
||||
);
|
||||
println!("note: {}", report.note);
|
||||
}
|
||||
|
||||
fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&response)?);
|
||||
|
|
@ -2529,30 +2736,168 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
}
|
||||
ControlResponse::KeychainAllowedSigners {
|
||||
allowed_signers,
|
||||
out,
|
||||
note,
|
||||
..
|
||||
} => {
|
||||
print!("{allowed_signers}");
|
||||
if allowed_signers.is_empty() {
|
||||
println!("no active admin public keys available");
|
||||
if let Some(out) = out {
|
||||
println!("wrote allowed_signers: {}", out.display());
|
||||
if allowed_signers.is_empty() {
|
||||
println!("warning: generated file has no active admin public keys");
|
||||
}
|
||||
} else {
|
||||
print!("{allowed_signers}");
|
||||
if allowed_signers.is_empty() {
|
||||
println!("no active admin public keys available");
|
||||
}
|
||||
}
|
||||
eprintln!("note: {note}");
|
||||
}
|
||||
ControlResponse::KeychainVerified { report } => {
|
||||
println!("ops: {}", report.ops);
|
||||
println!("signatures: {}", report.signatures);
|
||||
println!("accepted_ops: {}", report.accepted_ops);
|
||||
println!("rejected_ops: {}", report.rejected_ops);
|
||||
println!("active_admin_keys: {}", report.active_admin_keys);
|
||||
ControlResponse::KeychainFileSigned {
|
||||
input,
|
||||
out,
|
||||
namespace,
|
||||
signer,
|
||||
note,
|
||||
} => {
|
||||
println!("signed file: {}", input.display());
|
||||
println!(
|
||||
"accepted_head: {}",
|
||||
report
|
||||
.accepted_head
|
||||
"signature: {}",
|
||||
out.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "none".to_owned())
|
||||
);
|
||||
println!("namespace: {namespace}");
|
||||
println!("signer: {signer}");
|
||||
eprintln!("note: {note}");
|
||||
}
|
||||
ControlResponse::KeychainFileVerified {
|
||||
input,
|
||||
signature,
|
||||
namespace,
|
||||
verified,
|
||||
principal,
|
||||
note,
|
||||
} => {
|
||||
println!("file: {}", input.display());
|
||||
println!("signature: {}", signature.display());
|
||||
println!("namespace: {namespace}");
|
||||
println!("principal: {}", principal.as_deref().unwrap_or("none"));
|
||||
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 {
|
||||
input,
|
||||
report,
|
||||
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(|head| head.as_str())
|
||||
.map(|h| h.as_str())
|
||||
.unwrap_or("none")
|
||||
);
|
||||
println!("note: {}", report.note);
|
||||
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);
|
||||
}
|
||||
eprintln!("note: {note}");
|
||||
}
|
||||
ControlResponse::KeychainExplained { subject, lines } => {
|
||||
println!("subject: {subject}");
|
||||
for line in lines {
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
ControlResponse::KeychainVerified { report } => {
|
||||
print_keychain_sigchain_report(&report);
|
||||
}
|
||||
ControlResponse::KeychainSynced {
|
||||
peer_node_id,
|
||||
|
|
|
|||
Loading…
Reference in a new issue