Add OpenSSH KRL spec export
This commit is contained in:
parent
02444a7cfd
commit
b102e07204
9 changed files with 235 additions and 25 deletions
|
|
@ -1,5 +1,6 @@
|
|||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
|
||||
use geth_types::{NodeId, SshCertId, SshCertRequestId, SshRevocationId, UnixMillis};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -204,6 +205,43 @@ pub struct SshRevocationEntry {
|
|||
pub published: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SshRevocationExportFormat {
|
||||
Jsonl,
|
||||
OpenSshKrlSpec,
|
||||
}
|
||||
|
||||
impl SshRevocationExportFormat {
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Jsonl => "jsonl",
|
||||
Self::OpenSshKrlSpec => "openssh-krl-spec",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SshRevocationExportFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SshRevocationExportFormat {
|
||||
type Err = SshIdentityError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"jsonl" => Ok(Self::Jsonl),
|
||||
"openssh-krl-spec" | "krl-spec" => Ok(Self::OpenSshKrlSpec),
|
||||
_ => Err(SshIdentityError::InvalidRevocationExportFormat(
|
||||
value.to_owned(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SshCertFlowError {
|
||||
#[error("certificate request has no principals")]
|
||||
|
|
@ -284,6 +322,31 @@ pub fn build_ssh_cert_sign_command(
|
|||
Ok(command)
|
||||
}
|
||||
|
||||
pub fn openssh_krl_spec(entries: &[SshRevocationEntry]) -> Result<String, SshIdentityError> {
|
||||
let mut spec = String::new();
|
||||
for entry in entries {
|
||||
spec.push_str(&openssh_krl_spec_line(entry)?);
|
||||
spec.push('\n');
|
||||
}
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
pub fn openssh_krl_spec_line(entry: &SshRevocationEntry) -> Result<String, SshIdentityError> {
|
||||
let target = entry.target.trim();
|
||||
if target.is_empty() || target.bytes().any(|byte| byte == b'\n' || byte == b'\r') {
|
||||
return Err(SshIdentityError::InvalidRevocationTarget(
|
||||
entry.target.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
let directive = match entry.kind {
|
||||
SshRevocationKind::PublicKey | SshRevocationKind::Certificate => "key",
|
||||
SshRevocationKind::Serial => "serial",
|
||||
SshRevocationKind::KeyId => "id",
|
||||
};
|
||||
Ok(format!("{directive}: {target}"))
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SshIdentityError {
|
||||
#[error("ssh-keygen failed or is unavailable")]
|
||||
|
|
@ -294,6 +357,10 @@ pub enum SshIdentityError {
|
|||
InvalidRequestStatus(String),
|
||||
#[error("invalid ssh revocation kind: {0}")]
|
||||
InvalidRevocationKind(String),
|
||||
#[error("invalid ssh revocation export format: {0}")]
|
||||
InvalidRevocationExportFormat(String),
|
||||
#[error("invalid ssh revocation target: {0}")]
|
||||
InvalidRevocationTarget(String),
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
|
@ -348,4 +415,54 @@ mod tests {
|
|||
assert!(command.contains(&"-h".to_owned()));
|
||||
assert!(command.contains(&"server.local".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openssh_krl_spec_maps_revocation_kinds() {
|
||||
let entries = vec![
|
||||
SshRevocationEntry {
|
||||
id: "ssh-revocation:1".into(),
|
||||
kind: SshRevocationKind::Serial,
|
||||
target: "42".to_owned(),
|
||||
reason: None,
|
||||
created_at: UnixMillis(1),
|
||||
published: false,
|
||||
},
|
||||
SshRevocationEntry {
|
||||
id: "ssh-revocation:2".into(),
|
||||
kind: SshRevocationKind::KeyId,
|
||||
target: "node:laptop".to_owned(),
|
||||
reason: None,
|
||||
created_at: UnixMillis(2),
|
||||
published: false,
|
||||
},
|
||||
SshRevocationEntry {
|
||||
id: "ssh-revocation:3".into(),
|
||||
kind: SshRevocationKind::PublicKey,
|
||||
target: "ssh-ed25519 AAAA test".to_owned(),
|
||||
reason: None,
|
||||
created_at: UnixMillis(3),
|
||||
published: false,
|
||||
},
|
||||
];
|
||||
|
||||
let spec = openssh_krl_spec(&entries).expect("krl spec");
|
||||
|
||||
assert!(spec.contains("serial: 42\n"));
|
||||
assert!(spec.contains("id: node:laptop\n"));
|
||||
assert!(spec.contains("key: ssh-ed25519 AAAA test\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openssh_krl_spec_rejects_multiline_targets() {
|
||||
let entry = SshRevocationEntry {
|
||||
id: "ssh-revocation:1".into(),
|
||||
kind: SshRevocationKind::PublicKey,
|
||||
target: "ssh-ed25519 AAAA\nextra".to_owned(),
|
||||
reason: None,
|
||||
created_at: UnixMillis(1),
|
||||
published: false,
|
||||
};
|
||||
|
||||
assert!(openssh_krl_spec_line(&entry).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue