Add OpenSSH certificate signing path
This commit is contained in:
parent
e145bb47cd
commit
91c65e367d
9 changed files with 160 additions and 13 deletions
|
|
@ -421,6 +421,8 @@ pub enum SshCertCommand {
|
|||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
sign: bool,
|
||||
#[arg(long)]
|
||||
subject: Option<String>,
|
||||
},
|
||||
Import {
|
||||
|
|
@ -736,6 +738,7 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
valid_for,
|
||||
serial,
|
||||
out,
|
||||
sign,
|
||||
subject,
|
||||
} => ControlRequest::SshCertApprove {
|
||||
request_id,
|
||||
|
|
@ -743,6 +746,7 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
valid_for,
|
||||
serial,
|
||||
out,
|
||||
sign,
|
||||
subject,
|
||||
},
|
||||
SshCertCommand::Import {
|
||||
|
|
@ -1201,6 +1205,10 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
}
|
||||
println!("signing_command:");
|
||||
println!("{}", shell_quote_command(&approval.signing_command));
|
||||
println!("signed: {}", approval.signed);
|
||||
if let Some(certificate_id) = approval.certificate_id {
|
||||
println!("certificate_id: {certificate_id}");
|
||||
}
|
||||
println!("note: {}", approval.note);
|
||||
}
|
||||
ControlResponse::SshCertImported { certificate } => {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ pub enum ControlRequest {
|
|||
valid_for: Option<String>,
|
||||
serial: Option<u64>,
|
||||
out: Option<PathBuf>,
|
||||
sign: bool,
|
||||
subject: Option<String>,
|
||||
},
|
||||
SshCertImport {
|
||||
|
|
|
|||
|
|
@ -3117,6 +3117,7 @@ pub fn handle_request(
|
|||
valid_for,
|
||||
serial,
|
||||
out,
|
||||
sign,
|
||||
subject,
|
||||
} => {
|
||||
ensure_subject_authorized(
|
||||
|
|
@ -3152,6 +3153,38 @@ pub fn handle_request(
|
|||
&valid_for,
|
||||
serial,
|
||||
)?;
|
||||
let expected_certificate_path = expected_openssh_cert_path(&public_key_path);
|
||||
let mut signed = false;
|
||||
let mut certificate_id_value = None;
|
||||
let mut note = "request approved; run the signing command on the CA/YubiKey machine, then import the resulting -cert.pub file".to_owned();
|
||||
if sign {
|
||||
geth_ssh_identity::ensure_ssh_keygen_available()?;
|
||||
let output = std::process::Command::new(&signing_command[0])
|
||||
.args(&signing_command[1..])
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
return Err(geth_ssh_identity::SshIdentityError::SshKeygenFailed(
|
||||
String::from_utf8_lossy(&output.stderr).trim().to_owned(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let certificate = std::fs::read_to_string(&expected_certificate_path)?;
|
||||
let record = SshCertificateRecord {
|
||||
id: certificate_id(&certificate),
|
||||
request_id: request.id.clone(),
|
||||
certificate_fingerprint: ssh_public_key_fingerprint(&certificate),
|
||||
certificate,
|
||||
imported_at: UnixMillis(geth_store::now_ms()),
|
||||
};
|
||||
store.insert_ssh_certificate(&stored_from_ssh_certificate(&record))?;
|
||||
store.update_ssh_cert_request_status(
|
||||
request.id.as_str(),
|
||||
SshCertRequestStatus::Signed.as_str(),
|
||||
)?;
|
||||
signed = true;
|
||||
certificate_id_value = Some(record.id);
|
||||
note = "request approved, signed with ssh-keygen, and imported into local certificate metadata".to_owned();
|
||||
}
|
||||
let approval = SshCertApproval {
|
||||
request_id: request.id,
|
||||
approved_by_node: NodeId::new(node.node_id.clone()),
|
||||
|
|
@ -3159,9 +3192,11 @@ pub fn handle_request(
|
|||
key_id: request_id,
|
||||
valid_for,
|
||||
serial,
|
||||
output_path: Some(expected_openssh_cert_path(&public_key_path)),
|
||||
output_path: Some(expected_certificate_path),
|
||||
signing_command,
|
||||
note: "request approved; run the signing command on the CA/YubiKey machine, then import the resulting -cert.pub file".to_owned(),
|
||||
signed,
|
||||
certificate_id: certificate_id_value,
|
||||
note,
|
||||
};
|
||||
Ok(ControlResponse::SshCertApproved { approval })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ pub struct SshCertApproval {
|
|||
pub serial: Option<u64>,
|
||||
pub output_path: Option<String>,
|
||||
pub signing_command: Vec<String>,
|
||||
pub signed: bool,
|
||||
pub certificate_id: Option<SshCertId>,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1392,6 +1392,7 @@ fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
|||
valid_for: Some("+4w".to_owned()),
|
||||
serial: Some(42),
|
||||
out: None,
|
||||
sign: false,
|
||||
subject: None,
|
||||
},
|
||||
)
|
||||
|
|
@ -1591,6 +1592,92 @@ fn ssh_cert_and_revocation_commands_check_subject_capabilities() {
|
|||
.expect("authorized revocation add");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_cert_approve_can_sign_and_import_with_openssh_key() {
|
||||
if Command::new("ssh-keygen").arg("-?").output().is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let paths = geth_config::GethPaths::from_home(home.path());
|
||||
let node = geth_node::init_node(&paths).expect("init node");
|
||||
let ca_key_path = home.path().join("ca_ed25519");
|
||||
let user_key_path = home.path().join("user_ed25519");
|
||||
for key_path in [&ca_key_path, &user_key_path] {
|
||||
let status = Command::new("ssh-keygen")
|
||||
.arg("-q")
|
||||
.arg("-t")
|
||||
.arg("ed25519")
|
||||
.arg("-N")
|
||||
.arg("")
|
||||
.arg("-f")
|
||||
.arg(key_path)
|
||||
.status()
|
||||
.expect("generate ssh key");
|
||||
assert!(status.success());
|
||||
}
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::SshCertRequest {
|
||||
public_key_path: user_key_path.with_extension("pub"),
|
||||
cert_kind: "user".to_owned(),
|
||||
principals: vec!["eric".to_owned()],
|
||||
requested_validity: Some("+1w".to_owned()),
|
||||
renewal_of: None,
|
||||
reason: Some("sign now".to_owned()),
|
||||
subject: None,
|
||||
},
|
||||
)
|
||||
.expect("request cert");
|
||||
let request_id = match response {
|
||||
geth_control::ControlResponse::SshCertRequested { request } => request.id.to_string(),
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
};
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::SshCertApprove {
|
||||
request_id: request_id.clone(),
|
||||
ca_key_path,
|
||||
valid_for: Some("+1w".to_owned()),
|
||||
serial: Some(7),
|
||||
out: None,
|
||||
sign: true,
|
||||
subject: None,
|
||||
},
|
||||
)
|
||||
.expect("approve and sign cert");
|
||||
match response {
|
||||
geth_control::ControlResponse::SshCertApproved { approval } => {
|
||||
assert!(approval.signed);
|
||||
assert!(approval.certificate_id.is_some());
|
||||
assert!(approval.note.contains("signed with ssh-keygen"));
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::SshCertList { subject: None },
|
||||
)
|
||||
.expect("list certs");
|
||||
match response {
|
||||
geth_control::ControlResponse::SshCertList {
|
||||
requests,
|
||||
certificates,
|
||||
} => {
|
||||
assert_eq!(certificates.len(), 1);
|
||||
assert_eq!(certificates[0].request_id.to_string(), request_id);
|
||||
assert_eq!(
|
||||
requests[0].status,
|
||||
geth_ssh_identity::SshCertRequestStatus::Signed
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_revocation_export_can_write_binary_openssh_krl() {
|
||||
if Command::new("ssh-keygen").arg("-?").output().is_err() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue