Import SSH revocation metadata
This commit is contained in:
parent
b9e7c61e67
commit
4c368253e4
11 changed files with 212 additions and 13 deletions
|
|
@ -400,6 +400,11 @@ pub enum SshRevocationCommand {
|
|||
#[arg(long)]
|
||||
ca_public: Option<PathBuf>,
|
||||
},
|
||||
Import {
|
||||
path: PathBuf,
|
||||
#[arg(long, default_value = "jsonl")]
|
||||
format: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
|
|
@ -671,6 +676,9 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
format,
|
||||
ca_public,
|
||||
},
|
||||
SshRevocationCommand::Import { path, format } => {
|
||||
ControlRequest::SshRevocationImport { path, format }
|
||||
}
|
||||
},
|
||||
},
|
||||
Command::Init | Command::Daemon { .. } => bail!("command is handled directly"),
|
||||
|
|
@ -1084,6 +1092,22 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
println!("format: {format}");
|
||||
println!("note: {note}");
|
||||
}
|
||||
ControlResponse::SshRevocationImported {
|
||||
revocations,
|
||||
format,
|
||||
count,
|
||||
note,
|
||||
} => {
|
||||
println!("imported {count} ssh revocations");
|
||||
println!("format: {format}");
|
||||
for revocation in revocations {
|
||||
println!(
|
||||
"{}\t{}\t{}",
|
||||
revocation.id, revocation.kind, revocation.target
|
||||
);
|
||||
}
|
||||
println!("note: {note}");
|
||||
}
|
||||
ControlResponse::DbAdded { db } => {
|
||||
println!("registered db: {}", db.name);
|
||||
println!("id: {}", db.id);
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ pub enum ControlRequest {
|
|||
format: String,
|
||||
ca_public: Option<PathBuf>,
|
||||
},
|
||||
SshRevocationImport {
|
||||
path: PathBuf,
|
||||
format: String,
|
||||
},
|
||||
DbAdd {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
|
|
@ -328,6 +332,12 @@ pub enum ControlResponse {
|
|||
count: usize,
|
||||
note: String,
|
||||
},
|
||||
SshRevocationImported {
|
||||
revocations: Vec<SshRevocationEntry>,
|
||||
format: String,
|
||||
count: usize,
|
||||
note: String,
|
||||
},
|
||||
DbAdded {
|
||||
db: DbResource,
|
||||
},
|
||||
|
|
@ -477,6 +487,15 @@ mod tests {
|
|||
request
|
||||
);
|
||||
|
||||
let request = ControlRequest::SshRevocationImport {
|
||||
path: PathBuf::from("revocations.jsonl"),
|
||||
format: "jsonl".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
|
||||
request
|
||||
);
|
||||
|
||||
let response = ControlResponse::SshRevocationExported {
|
||||
out: PathBuf::from("revocations.krl-spec"),
|
||||
format: "openssh-krl-spec".to_owned(),
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ use geth_secrets::{BearerAccess, ResourceMasterSecret};
|
|||
use geth_ssh_identity::{
|
||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||
SshRevocationEntry, SshRevocationExportFormat, SshRevocationKind, build_ssh_cert_sign_command,
|
||||
cert_request_id, certificate_id, openssh_krl_spec, revocation_id, ssh_public_key_fingerprint,
|
||||
write_openssh_krl,
|
||||
cert_request_id, certificate_id, openssh_krl_spec, parse_openssh_krl_spec, revocation_id,
|
||||
ssh_public_key_fingerprint, write_openssh_krl,
|
||||
};
|
||||
use geth_store::{
|
||||
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredFileConflict,
|
||||
|
|
@ -948,6 +948,53 @@ pub fn handle_request(
|
|||
note,
|
||||
})
|
||||
}
|
||||
ControlRequest::SshRevocationImport { path, format } => {
|
||||
let body = std::fs::read_to_string(&path)?;
|
||||
let created_at = UnixMillis(geth_store::now_ms());
|
||||
let revocations = match format.as_str() {
|
||||
"jsonl" => body
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| serde_json::from_str(line).map_err(NodeError::from))
|
||||
.collect::<Result<Vec<SshRevocationEntry>, NodeError>>()?,
|
||||
"openssh-krl-spec" | "krl-spec" => parse_openssh_krl_spec(&body)?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (kind, target))| {
|
||||
let created_at = UnixMillis(created_at.0 + index as i64);
|
||||
SshRevocationEntry {
|
||||
id: revocation_id(&kind, &target, created_at),
|
||||
kind,
|
||||
target,
|
||||
reason: Some(format!("imported from {}", path.display())),
|
||||
created_at,
|
||||
published: true,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
"openssh-krl" | "krl" => {
|
||||
return Err(NodeError::SshIdentity(
|
||||
geth_ssh_identity::SshIdentityError::BinaryKrlImportUnsupported,
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Err(NodeError::SshIdentity(
|
||||
geth_ssh_identity::SshIdentityError::InvalidRevocationExportFormat(
|
||||
format.clone(),
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
for revocation in &revocations {
|
||||
store.insert_ssh_revocation(&stored_from_ssh_revocation(revocation))?;
|
||||
}
|
||||
Ok(ControlResponse::SshRevocationImported {
|
||||
count: revocations.len(),
|
||||
revocations,
|
||||
format,
|
||||
note: "imported revocation metadata; binary OpenSSH KRL files cannot be enumerated, import JSONL or the KRL spec source instead".to_owned(),
|
||||
})
|
||||
}
|
||||
ControlRequest::DbAdd { name, path } => {
|
||||
geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?;
|
||||
if !path.is_file() {
|
||||
|
|
|
|||
|
|
@ -334,6 +334,33 @@ pub fn openssh_krl_spec(entries: &[SshRevocationEntry]) -> Result<String, SshIde
|
|||
Ok(spec)
|
||||
}
|
||||
|
||||
pub fn parse_openssh_krl_spec(
|
||||
spec: &str,
|
||||
) -> Result<Vec<(SshRevocationKind, String)>, SshIdentityError> {
|
||||
let mut entries = Vec::new();
|
||||
for line in spec.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let Some((directive, target)) = line.split_once(':') else {
|
||||
return Err(SshIdentityError::InvalidKrlSpecLine(line.to_owned()));
|
||||
};
|
||||
let target = target.trim();
|
||||
if target.is_empty() || target.bytes().any(|byte| byte == b'\n' || byte == b'\r') {
|
||||
return Err(SshIdentityError::InvalidRevocationTarget(target.to_owned()));
|
||||
}
|
||||
let kind = match directive.trim() {
|
||||
"key" => SshRevocationKind::PublicKey,
|
||||
"serial" => SshRevocationKind::Serial,
|
||||
"id" => SshRevocationKind::KeyId,
|
||||
other => return Err(SshIdentityError::InvalidKrlSpecLine(other.to_owned())),
|
||||
};
|
||||
entries.push((kind, target.to_owned()));
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn write_openssh_krl(
|
||||
entries: &[SshRevocationEntry],
|
||||
out: &Path,
|
||||
|
|
@ -394,6 +421,10 @@ pub enum SshIdentityError {
|
|||
InvalidRevocationExportFormat(String),
|
||||
#[error("invalid ssh revocation target: {0}")]
|
||||
InvalidRevocationTarget(String),
|
||||
#[error("invalid OpenSSH KRL specification line: {0}")]
|
||||
InvalidKrlSpecLine(String),
|
||||
#[error("binary OpenSSH KRL files cannot be enumerated; import JSONL or the KRL spec source")]
|
||||
BinaryKrlImportUnsupported,
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
|
@ -485,6 +516,31 @@ mod tests {
|
|||
assert!(spec.contains("key: ssh-ed25519 AAAA test\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openssh_krl_spec_import_parses_supported_directives() {
|
||||
let parsed = parse_openssh_krl_spec(
|
||||
r#"
|
||||
# comment
|
||||
serial: 42
|
||||
id: node:laptop
|
||||
key: ssh-ed25519 AAAA test
|
||||
"#,
|
||||
)
|
||||
.expect("parse spec");
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
vec![
|
||||
(SshRevocationKind::Serial, "42".to_owned()),
|
||||
(SshRevocationKind::KeyId, "node:laptop".to_owned()),
|
||||
(
|
||||
SshRevocationKind::PublicKey,
|
||||
"ssh-ed25519 AAAA test".to_owned()
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openssh_krl_spec_rejects_multiline_targets() {
|
||||
let entry = SshRevocationEntry {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ geth-control = { path = "../geth-control" }
|
|||
geth-discovery = { path = "../geth-discovery" }
|
||||
geth-iroh = { path = "../geth-iroh" }
|
||||
geth-node = { path = "../geth-node" }
|
||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||
geth-store = { path = "../geth-store" }
|
||||
geth-types = { path = "../geth-types" }
|
||||
rusqlite.workspace = true
|
||||
|
|
|
|||
|
|
@ -1365,10 +1365,53 @@ fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
|||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
assert!(
|
||||
std::fs::read_to_string(krl_spec_path)
|
||||
std::fs::read_to_string(&krl_spec_path)
|
||||
.expect("read krl spec")
|
||||
.contains("key: ssh:blake3:test")
|
||||
);
|
||||
|
||||
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::SshRevocationImport {
|
||||
path: krl_spec_path,
|
||||
format: "openssh-krl-spec".to_owned(),
|
||||
},
|
||||
)
|
||||
.expect("import krl spec");
|
||||
match response {
|
||||
geth_control::ControlResponse::SshRevocationImported {
|
||||
count,
|
||||
revocations,
|
||||
note,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(
|
||||
revocations[0].kind,
|
||||
geth_ssh_identity::SshRevocationKind::PublicKey
|
||||
);
|
||||
assert!(note.contains("binary OpenSSH KRL files cannot be enumerated"));
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&import_node,
|
||||
geth_control::ControlRequest::SshRevocationImport {
|
||||
path: home.path().join("revocations.jsonl"),
|
||||
format: "jsonl".to_owned(),
|
||||
},
|
||||
)
|
||||
.expect("import jsonl revocations");
|
||||
match response {
|
||||
geth_control::ControlResponse::SshRevocationImported { count, .. } => {
|
||||
assert_eq!(count, 1);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue