Add OpenSSH KRL spec export
This commit is contained in:
parent
02444a7cfd
commit
b102e07204
9 changed files with 235 additions and 25 deletions
|
|
@ -134,6 +134,8 @@ Roadmap items should be actionable and checkable:
|
||||||
Bearer access metadata can be created/listed/revoked as resource-scoped auth
|
Bearer access metadata can be created/listed/revoked as resource-scoped auth
|
||||||
ops and must not allow trust graph mutation capabilities. Payload encryption,
|
ops and must not allow trust graph mutation capabilities. Payload encryption,
|
||||||
key envelopes, and bearer challenge-response are still roadmap work.
|
key envelopes, and bearer challenge-response are still roadmap work.
|
||||||
|
- SSH revocations can be exported as JSONL or OpenSSH KRL specification text.
|
||||||
|
Binary KRL generation/import is still roadmap work.
|
||||||
- Signed peer-card LAN discovery payloads, peer auth over Iroh, cr-sqlite,
|
- Signed peer-card LAN discovery payloads, peer auth over Iroh, cr-sqlite,
|
||||||
iroh-docs, iroh-blobs, Automerge sync, real auth enforcement,
|
iroh-docs, iroh-blobs, Automerge sync, real auth enforcement,
|
||||||
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,9 @@ SSH certificate request and renewal flows are managed as geth metadata. A node
|
||||||
can create a certificate request, another machine can approve it and receive an
|
can create a certificate request, another machine can approve it and receive an
|
||||||
explicit `ssh-keygen -s ...` command suitable for a CA key or YubiKey-backed CA,
|
explicit `ssh-keygen -s ...` command suitable for a CA key or YubiKey-backed CA,
|
||||||
and the resulting `-cert.pub` can be imported for distribution. Certificate and
|
and the resulting `-cert.pub` can be imported for distribution. Certificate and
|
||||||
key revocation entries are tracked locally and can be exported as JSONL for
|
key revocation entries are tracked locally and can be exported as JSONL or as an
|
||||||
distribution. Future Iroh replication will distribute these records between
|
OpenSSH KRL specification file for later `ssh-keygen -k` use. Future Iroh
|
||||||
authorized nodes.
|
replication will distribute these records between authorized nodes.
|
||||||
|
|
||||||
## MVP Features
|
## MVP Features
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ The bootstrap implementation provides:
|
||||||
- `geth ssh cert list`
|
- `geth ssh cert list`
|
||||||
- `geth ssh revocation add <kind> <target>`
|
- `geth ssh revocation add <kind> <target>`
|
||||||
- `geth ssh revocation list`
|
- `geth ssh revocation list`
|
||||||
- `geth ssh revocation export --out <path>`
|
- `geth ssh revocation export --out <path> [--format jsonl|openssh-krl-spec]`
|
||||||
|
|
||||||
Other command groups exist as explicit stubs: `pipe` and `ssh`.
|
Other command groups exist as explicit stubs: `pipe` and `ssh`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,8 @@ pub enum SshRevocationCommand {
|
||||||
Export {
|
Export {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
out: PathBuf,
|
out: PathBuf,
|
||||||
|
#[arg(long, default_value = "jsonl")]
|
||||||
|
format: String,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -522,7 +524,9 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
||||||
SshRevocationCommand::List => ControlRequest::SshRevocationList,
|
SshRevocationCommand::List => ControlRequest::SshRevocationList,
|
||||||
SshRevocationCommand::Export { out } => ControlRequest::SshRevocationExport { out },
|
SshRevocationCommand::Export { out, format } => {
|
||||||
|
ControlRequest::SshRevocationExport { out, format }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Command::Init | Command::Daemon { .. } => bail!("command is handled directly"),
|
Command::Init | Command::Daemon { .. } => bail!("command is handled directly"),
|
||||||
|
|
@ -838,8 +842,15 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlResponse::SshRevocationExported { out, count } => {
|
ControlResponse::SshRevocationExported {
|
||||||
|
out,
|
||||||
|
format,
|
||||||
|
count,
|
||||||
|
note,
|
||||||
|
} => {
|
||||||
println!("exported {count} ssh revocations to {}", out.display());
|
println!("exported {count} ssh revocations to {}", out.display());
|
||||||
|
println!("format: {format}");
|
||||||
|
println!("note: {note}");
|
||||||
}
|
}
|
||||||
ControlResponse::DbAdded { db } => {
|
ControlResponse::DbAdded { db } => {
|
||||||
println!("registered db: {}", db.name);
|
println!("registered db: {}", db.name);
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ pub enum ControlRequest {
|
||||||
SshRevocationList,
|
SshRevocationList,
|
||||||
SshRevocationExport {
|
SshRevocationExport {
|
||||||
out: PathBuf,
|
out: PathBuf,
|
||||||
|
format: String,
|
||||||
},
|
},
|
||||||
DbAdd {
|
DbAdd {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -237,7 +238,9 @@ pub enum ControlResponse {
|
||||||
},
|
},
|
||||||
SshRevocationExported {
|
SshRevocationExported {
|
||||||
out: PathBuf,
|
out: PathBuf,
|
||||||
|
format: String,
|
||||||
count: usize,
|
count: usize,
|
||||||
|
note: String,
|
||||||
},
|
},
|
||||||
DbAdded {
|
DbAdded {
|
||||||
db: DbResource,
|
db: DbResource,
|
||||||
|
|
@ -361,5 +364,25 @@ mod tests {
|
||||||
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
|
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
|
||||||
response
|
response
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let request = ControlRequest::SshRevocationExport {
|
||||||
|
out: PathBuf::from("revocations.krl-spec"),
|
||||||
|
format: "openssh-krl-spec".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(),
|
||||||
|
count: 2,
|
||||||
|
note: "OpenSSH KRL specification".to_owned(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
|
||||||
|
response
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ use geth_resource::ResourceDescriptor;
|
||||||
use geth_secrets::{BearerAccess, ResourceMasterSecret};
|
use geth_secrets::{BearerAccess, ResourceMasterSecret};
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||||
SshRevocationEntry, SshRevocationKind, build_ssh_cert_sign_command, cert_request_id,
|
SshRevocationEntry, SshRevocationExportFormat, SshRevocationKind, build_ssh_cert_sign_command,
|
||||||
certificate_id, revocation_id, ssh_public_key_fingerprint,
|
cert_request_id, certificate_id, openssh_krl_spec, revocation_id, ssh_public_key_fingerprint,
|
||||||
};
|
};
|
||||||
use geth_store::{
|
use geth_store::{
|
||||||
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredKeychainOp, StoredKvEntry,
|
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredKeychainOp, StoredKvEntry,
|
||||||
|
|
@ -92,6 +92,8 @@ pub enum NodeError {
|
||||||
MissingSshCertPrincipal,
|
MissingSshCertPrincipal,
|
||||||
#[error("ssh certificate flow error: {0}")]
|
#[error("ssh certificate flow error: {0}")]
|
||||||
SshCertFlow(#[from] geth_ssh_identity::SshCertFlowError),
|
SshCertFlow(#[from] geth_ssh_identity::SshCertFlowError),
|
||||||
|
#[error("ssh identity error: {0}")]
|
||||||
|
SshIdentity(#[from] geth_ssh_identity::SshIdentityError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -668,24 +670,41 @@ pub fn handle_request(
|
||||||
.map(ssh_revocation_from_stored)
|
.map(ssh_revocation_from_stored)
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
}),
|
}),
|
||||||
ControlRequest::SshRevocationExport { out } => {
|
ControlRequest::SshRevocationExport { out, format } => {
|
||||||
let revocations = store
|
let revocations = store
|
||||||
.list_ssh_revocations()?
|
.list_ssh_revocations()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ssh_revocation_from_stored)
|
.map(ssh_revocation_from_stored)
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let format = format
|
||||||
|
.parse::<SshRevocationExportFormat>()
|
||||||
|
.map_err(NodeError::SshIdentity)?;
|
||||||
if let Some(parent) = out.parent() {
|
if let Some(parent) = out.parent() {
|
||||||
std::fs::create_dir_all(parent)?;
|
std::fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
let mut body = String::new();
|
let (body, note) = match format {
|
||||||
for revocation in &revocations {
|
SshRevocationExportFormat::Jsonl => {
|
||||||
body.push_str(&serde_json::to_string(revocation)?);
|
let mut body = String::new();
|
||||||
body.push('\n');
|
for revocation in &revocations {
|
||||||
}
|
body.push_str(&serde_json::to_string(revocation)?);
|
||||||
|
body.push('\n');
|
||||||
|
}
|
||||||
|
(
|
||||||
|
body,
|
||||||
|
"JSONL geth revocation metadata; not an OpenSSH KRL binary".to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SshRevocationExportFormat::OpenSshKrlSpec => (
|
||||||
|
openssh_krl_spec(&revocations)?,
|
||||||
|
"OpenSSH KRL specification; generate a binary KRL with ssh-keygen -k -f <krl> [-s <ca.pub>] <spec>".to_owned(),
|
||||||
|
),
|
||||||
|
};
|
||||||
std::fs::write(&out, body)?;
|
std::fs::write(&out, body)?;
|
||||||
Ok(ControlResponse::SshRevocationExported {
|
Ok(ControlResponse::SshRevocationExported {
|
||||||
out,
|
out,
|
||||||
|
format: format.to_string(),
|
||||||
count: revocations.len(),
|
count: revocations.len(),
|
||||||
|
note,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ControlRequest::DbAdd { name, path } => {
|
ControlRequest::DbAdd { name, path } => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use geth_types::{NodeId, SshCertId, SshCertRequestId, SshRevocationId, UnixMillis};
|
use geth_types::{NodeId, SshCertId, SshCertRequestId, SshRevocationId, UnixMillis};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -204,6 +205,43 @@ pub struct SshRevocationEntry {
|
||||||
pub published: bool,
|
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SshCertFlowError {
|
pub enum SshCertFlowError {
|
||||||
#[error("certificate request has no principals")]
|
#[error("certificate request has no principals")]
|
||||||
|
|
@ -284,6 +322,31 @@ pub fn build_ssh_cert_sign_command(
|
||||||
Ok(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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SshIdentityError {
|
pub enum SshIdentityError {
|
||||||
#[error("ssh-keygen failed or is unavailable")]
|
#[error("ssh-keygen failed or is unavailable")]
|
||||||
|
|
@ -294,6 +357,10 @@ pub enum SshIdentityError {
|
||||||
InvalidRequestStatus(String),
|
InvalidRequestStatus(String),
|
||||||
#[error("invalid ssh revocation kind: {0}")]
|
#[error("invalid ssh revocation kind: {0}")]
|
||||||
InvalidRevocationKind(String),
|
InvalidRevocationKind(String),
|
||||||
|
#[error("invalid ssh revocation export format: {0}")]
|
||||||
|
InvalidRevocationExportFormat(String),
|
||||||
|
#[error("invalid ssh revocation target: {0}")]
|
||||||
|
InvalidRevocationTarget(String),
|
||||||
#[error("io error: {0}")]
|
#[error("io error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
@ -348,4 +415,54 @@ mod tests {
|
||||||
assert!(command.contains(&"-h".to_owned()));
|
assert!(command.contains(&"-h".to_owned()));
|
||||||
assert!(command.contains(&"server.local".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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -863,6 +863,7 @@ fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
||||||
&node,
|
&node,
|
||||||
geth_control::ControlRequest::SshRevocationExport {
|
geth_control::ControlRequest::SshRevocationExport {
|
||||||
out: export_path.clone(),
|
out: export_path.clone(),
|
||||||
|
format: "jsonl".to_owned(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("export revocations");
|
.expect("export revocations");
|
||||||
|
|
@ -871,4 +872,32 @@ fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
||||||
.expect("read revocations")
|
.expect("read revocations")
|
||||||
.contains("lost key")
|
.contains("lost key")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let krl_spec_path = home.path().join("revocations.krl-spec");
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SshRevocationExport {
|
||||||
|
out: krl_spec_path.clone(),
|
||||||
|
format: "openssh-krl-spec".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("export revocation krl spec");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::SshRevocationExported {
|
||||||
|
format,
|
||||||
|
count,
|
||||||
|
note,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(format, "openssh-krl-spec");
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
assert!(note.contains("ssh-keygen -k"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
std::fs::read_to_string(krl_spec_path)
|
||||||
|
.expect("read krl spec")
|
||||||
|
.contains("key: ssh:blake3:test")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,9 @@ roadmap stubs.
|
||||||
|
|
||||||
`geth-ssh-identity` defines SSH trust namespaces plus certificate request,
|
`geth-ssh-identity` defines SSH trust namespaces plus certificate request,
|
||||||
approval, certificate import, and revocation-list data models. The bootstrap
|
approval, certificate import, and revocation-list data models. The bootstrap
|
||||||
persists these flows locally and exports revocations as JSONL. It does not yet
|
persists these flows locally and exports revocations as JSONL or OpenSSH KRL
|
||||||
generate OpenSSH KRL binaries or replicate the lists over Iroh.
|
specification text. It does not yet generate OpenSSH KRL binaries or replicate
|
||||||
|
the lists over Iroh.
|
||||||
|
|
||||||
## Keychain, Auth, And Secrets
|
## Keychain, Auth, And Secrets
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,11 +189,16 @@ resource-scoped capability decisions.
|
||||||
|
|
||||||
- `[~]` SSH certificate and revocation lifecycle.
|
- `[~]` SSH certificate and revocation lifecycle.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `geth ssh cert request/requests/approve/import/list` persist local metadata.
|
- `[x]` `geth ssh cert request/requests/approve/import/list` persist local
|
||||||
- Approval emits an explicit `ssh-keygen -s ...` command for CA/YubiKey use.
|
metadata.
|
||||||
- `geth ssh revocation add/list/export` persists and exports revocations.
|
- `[x]` Approval emits an explicit `ssh-keygen -s ...` command for
|
||||||
- Future completion requires auth checks for request, approve, import, publish,
|
CA/YubiKey use.
|
||||||
and read capabilities.
|
- `[x]` `geth ssh revocation add/list/export` persists and exports
|
||||||
|
revocations.
|
||||||
|
- `[x]` Revocations can be exported as JSONL and OpenSSH KRL specification
|
||||||
|
text.
|
||||||
|
- `[ ]` Future completion requires auth checks for request, approve, import,
|
||||||
|
publish, and read capabilities.
|
||||||
|
|
||||||
## Phase 3: CAS, KV, And Pubsub
|
## Phase 3: CAS, KV, And Pubsub
|
||||||
|
|
||||||
|
|
@ -277,11 +282,14 @@ Goal: add authorized stream-oriented management workflows over Iroh.
|
||||||
- Consumers can list current certs/revocations from local state while offline.
|
- Consumers can list current certs/revocations from local state while offline.
|
||||||
- Conflicting or unsigned records are rejected or quarantined.
|
- Conflicting or unsigned records are rejected or quarantined.
|
||||||
|
|
||||||
- `[ ]` OpenSSH KRL import/export.
|
- `[~]` OpenSSH KRL import/export.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- Revocation records can produce an OpenSSH KRL file.
|
- `[x]` Revocation records can produce an OpenSSH KRL specification file.
|
||||||
- Existing KRL files can be imported into revocation metadata where possible.
|
- `[x]` Tests cover serial, key ID, and public key revocation spec lines.
|
||||||
- Tests cover serial, key ID, public key, and certificate revocations.
|
- `[ ]` Revocation records can produce an OpenSSH binary KRL file.
|
||||||
|
- `[ ]` Existing KRL files can be imported into revocation metadata where
|
||||||
|
possible.
|
||||||
|
- `[ ]` Tests cover binary KRL export/import and certificate revocations.
|
||||||
|
|
||||||
## Phase 5: DB And Documents
|
## Phase 5: DB And Documents
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue