Require signed SSH metadata provenance
This commit is contained in:
parent
c5548e4df9
commit
6e04e786c2
8 changed files with 414 additions and 45 deletions
|
|
@ -218,8 +218,9 @@ Roadmap items should be actionable and checkable:
|
||||||
`resource:ssh:revocations`. The daemon live-syncs known peers using per-peer
|
`resource:ssh:revocations`. The daemon live-syncs known peers using per-peer
|
||||||
cursors; `[sync] live_sync_enabled` and `live_sync_interval_ms` in
|
cursors; `[sync] live_sync_enabled` and `live_sync_interval_ms` in
|
||||||
`config.toml` control that loop. Conflicting records with already-known ids
|
`config.toml` control that loop. Conflicting records with already-known ids
|
||||||
are rejected during sync import; this is pull-only metadata sync, not yet a
|
are rejected during sync import, and new cert-flow/revocation records must
|
||||||
signed CRDT/resource-log replication model.
|
carry valid agent-key signed provenance over canonical payloads. This is
|
||||||
|
pull-only metadata sync, not yet a CRDT/resource-log replication model.
|
||||||
- cr-sqlite apply, iroh-docs, iroh-blobs provider/fetch, Automerge sync,
|
- cr-sqlite apply, iroh-docs, iroh-blobs provider/fetch, Automerge sync,
|
||||||
broader auth enforcement, and Keyhive/BeeKEM-style authorization are future
|
broader auth enforcement, and Keyhive/BeeKEM-style authorization are future
|
||||||
roadmap items unless implemented later.
|
roadmap items unless implemented later.
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1253,6 +1253,7 @@ dependencies = [
|
||||||
"geth-ssh-proxy",
|
"geth-ssh-proxy",
|
||||||
"geth-store",
|
"geth-store",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
|
"hex",
|
||||||
"iroh",
|
"iroh",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ payloads through `ssh-keygen -Y sign` using the
|
||||||
admin/YubiKey-rooted trust. `geth keychain status` reports the number of stored
|
admin/YubiKey-rooted trust. `geth keychain status` reports the number of stored
|
||||||
keychain signatures plus how many currently verify with OpenSSH; rejecting
|
keychain signatures plus how many currently verify with OpenSSH; rejecting
|
||||||
unsigned or invalid replicated keychain ops is still future work.
|
unsigned or invalid replicated keychain ops is still future work.
|
||||||
|
SSH certificate-flow and revocation records carry agent-key signed provenance
|
||||||
|
over canonical payloads, and sync import rejects new unsigned or invalidly
|
||||||
|
signed records.
|
||||||
|
|
||||||
The daemon can also install itself as a user service:
|
The daemon can also install itself as a user service:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-ssh-proxy = { path = "../geth-ssh-proxy" }
|
geth-ssh-proxy = { path = "../geth-ssh-proxy" }
|
||||||
geth-store = { path = "../geth-store" }
|
geth-store = { path = "../geth-store" }
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
|
hex.workspace = true
|
||||||
iroh.workspace = true
|
iroh.workspace = true
|
||||||
swarm-discovery.workspace = true
|
swarm-discovery.workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,13 @@ use geth_pubsub::PubsubMessage;
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
use geth_secrets::{BearerAccess, BearerChallenge, BearerProof, ResourceMasterSecret};
|
use geth_secrets::{BearerAccess, BearerChallenge, BearerProof, ResourceMasterSecret};
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
|
SSH_CERT_ISSUANCE_NAMESPACE, SSH_CERT_REQUEST_NAMESPACE, SSH_REVOCATION_LIST_NAMESPACE,
|
||||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||||
SshRevocationEntry, SshRevocationExportFormat, SshRevocationKind, build_ssh_cert_sign_command,
|
SshRecordProvenance, SshRevocationEntry, SshRevocationExportFormat, SshRevocationKind,
|
||||||
cert_request_id, certificate_id, openssh_krl_spec, parse_openssh_krl_spec, revocation_id,
|
build_ssh_cert_sign_command, cert_request_id, certificate_id, openssh_krl_spec,
|
||||||
ssh_public_key_fingerprint, write_openssh_krl,
|
parse_openssh_krl_spec, revocation_id, ssh_cert_request_signing_payload,
|
||||||
|
ssh_certificate_signing_payload, ssh_public_key_fingerprint, ssh_revocation_signing_payload,
|
||||||
|
write_openssh_krl,
|
||||||
};
|
};
|
||||||
use geth_ssh_proxy::SshProxyConnection;
|
use geth_ssh_proxy::SshProxyConnection;
|
||||||
use geth_store::{
|
use geth_store::{
|
||||||
|
|
@ -3304,7 +3307,7 @@ async fn handle_iroh_control_connection(
|
||||||
.list_ssh_certificates_since(since_ms)?
|
.list_ssh_certificates_since(since_ms)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ssh_certificate_from_stored)
|
.map(ssh_certificate_from_stored)
|
||||||
.collect(),
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(Vec::new(), Vec::new())
|
(Vec::new(), Vec::new())
|
||||||
|
|
@ -5098,7 +5101,7 @@ pub fn handle_request(
|
||||||
.map_err(|_| NodeError::InvalidSshCertKind(cert_kind.clone()))?;
|
.map_err(|_| NodeError::InvalidSshCertKind(cert_kind.clone()))?;
|
||||||
let public_key = std::fs::read_to_string(&public_key_path)?;
|
let public_key = std::fs::read_to_string(&public_key_path)?;
|
||||||
let created_at = UnixMillis(geth_store::now_ms());
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
let request = SshCertRequest {
|
let mut request = SshCertRequest {
|
||||||
id: cert_request_id(
|
id: cert_request_id(
|
||||||
&NodeId::new(node.node_id.clone()),
|
&NodeId::new(node.node_id.clone()),
|
||||||
&public_key,
|
&public_key,
|
||||||
|
|
@ -5115,7 +5118,9 @@ pub fn handle_request(
|
||||||
reason,
|
reason,
|
||||||
status: SshCertRequestStatus::Pending,
|
status: SshCertRequestStatus::Pending,
|
||||||
created_at,
|
created_at,
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
sign_ssh_cert_request(node, &mut request)?;
|
||||||
store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?;
|
store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?;
|
||||||
Ok(ControlResponse::SshCertRequested { request })
|
Ok(ControlResponse::SshCertRequested { request })
|
||||||
}
|
}
|
||||||
|
|
@ -5156,7 +5161,8 @@ pub fn handle_request(
|
||||||
.ok_or_else(|| NodeError::SshCertRequestNotFound(request_id.clone()))?;
|
.ok_or_else(|| NodeError::SshCertRequestNotFound(request_id.clone()))?;
|
||||||
let mut request = ssh_cert_request_from_stored(stored)?;
|
let mut request = ssh_cert_request_from_stored(stored)?;
|
||||||
request.status = SshCertRequestStatus::Approved;
|
request.status = SshCertRequestStatus::Approved;
|
||||||
store.update_ssh_cert_request_status(request.id.as_str(), request.status.as_str())?;
|
sign_ssh_cert_request(node, &mut request)?;
|
||||||
|
store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?;
|
||||||
let public_key_path = out.clone().unwrap_or_else(|| {
|
let public_key_path = out.clone().unwrap_or_else(|| {
|
||||||
node.paths
|
node.paths
|
||||||
.home()
|
.home()
|
||||||
|
|
@ -5193,18 +5199,19 @@ pub fn handle_request(
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
let certificate = std::fs::read_to_string(&expected_certificate_path)?;
|
let certificate = std::fs::read_to_string(&expected_certificate_path)?;
|
||||||
let record = SshCertificateRecord {
|
let mut record = SshCertificateRecord {
|
||||||
id: certificate_id(&certificate),
|
id: certificate_id(&certificate),
|
||||||
request_id: request.id.clone(),
|
request_id: request.id.clone(),
|
||||||
certificate_fingerprint: ssh_public_key_fingerprint(&certificate),
|
certificate_fingerprint: ssh_public_key_fingerprint(&certificate),
|
||||||
certificate,
|
certificate,
|
||||||
imported_at: UnixMillis(geth_store::now_ms()),
|
imported_at: UnixMillis(geth_store::now_ms()),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
sign_ssh_certificate(node, &mut record)?;
|
||||||
store.insert_ssh_certificate(&stored_from_ssh_certificate(&record))?;
|
store.insert_ssh_certificate(&stored_from_ssh_certificate(&record))?;
|
||||||
store.update_ssh_cert_request_status(
|
request.status = SshCertRequestStatus::Signed;
|
||||||
request.id.as_str(),
|
sign_ssh_cert_request(node, &mut request)?;
|
||||||
SshCertRequestStatus::Signed.as_str(),
|
store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?;
|
||||||
)?;
|
|
||||||
signed = true;
|
signed = true;
|
||||||
certificate_id_value = Some(record.id);
|
certificate_id_value = Some(record.id);
|
||||||
note = "request approved, signed with ssh-keygen, and imported into local certificate metadata".to_owned();
|
note = "request approved, signed with ssh-keygen, and imported into local certificate metadata".to_owned();
|
||||||
|
|
@ -5237,18 +5244,22 @@ pub fn handle_request(
|
||||||
"ssh_cert.import",
|
"ssh_cert.import",
|
||||||
)?;
|
)?;
|
||||||
let certificate = std::fs::read_to_string(&cert_path)?;
|
let certificate = std::fs::read_to_string(&cert_path)?;
|
||||||
let record = SshCertificateRecord {
|
let mut record = SshCertificateRecord {
|
||||||
id: certificate_id(&certificate),
|
id: certificate_id(&certificate),
|
||||||
request_id: SshCertRequestId::new(request_id.clone()),
|
request_id: SshCertRequestId::new(request_id.clone()),
|
||||||
certificate_fingerprint: ssh_public_key_fingerprint(&certificate),
|
certificate_fingerprint: ssh_public_key_fingerprint(&certificate),
|
||||||
certificate,
|
certificate,
|
||||||
imported_at: UnixMillis(geth_store::now_ms()),
|
imported_at: UnixMillis(geth_store::now_ms()),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
sign_ssh_certificate(node, &mut record)?;
|
||||||
store.insert_ssh_certificate(&stored_from_ssh_certificate(&record))?;
|
store.insert_ssh_certificate(&stored_from_ssh_certificate(&record))?;
|
||||||
store.update_ssh_cert_request_status(
|
if let Some(stored) = store.get_ssh_cert_request(&request_id)? {
|
||||||
&request_id,
|
let mut request = ssh_cert_request_from_stored(stored)?;
|
||||||
SshCertRequestStatus::Signed.as_str(),
|
request.status = SshCertRequestStatus::Signed;
|
||||||
)?;
|
sign_ssh_cert_request(node, &mut request)?;
|
||||||
|
store.insert_ssh_cert_request(&stored_from_ssh_cert_request(&request))?;
|
||||||
|
}
|
||||||
Ok(ControlResponse::SshCertImported {
|
Ok(ControlResponse::SshCertImported {
|
||||||
certificate: record,
|
certificate: record,
|
||||||
})
|
})
|
||||||
|
|
@ -5271,7 +5282,7 @@ pub fn handle_request(
|
||||||
.list_ssh_certificates()?
|
.list_ssh_certificates()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(ssh_certificate_from_stored)
|
.map(ssh_certificate_from_stored)
|
||||||
.collect(),
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ControlRequest::SshRevocationAdd {
|
ControlRequest::SshRevocationAdd {
|
||||||
|
|
@ -5291,14 +5302,16 @@ pub fn handle_request(
|
||||||
.parse::<SshRevocationKind>()
|
.parse::<SshRevocationKind>()
|
||||||
.map_err(|_| NodeError::InvalidSshRevocationKind(kind.clone()))?;
|
.map_err(|_| NodeError::InvalidSshRevocationKind(kind.clone()))?;
|
||||||
let created_at = UnixMillis(geth_store::now_ms());
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
let revocation = SshRevocationEntry {
|
let mut revocation = SshRevocationEntry {
|
||||||
id: revocation_id(&kind, &target, created_at),
|
id: revocation_id(&kind, &target, created_at),
|
||||||
kind,
|
kind,
|
||||||
target,
|
target,
|
||||||
reason,
|
reason,
|
||||||
created_at,
|
created_at,
|
||||||
published: true,
|
published: true,
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
sign_ssh_revocation(node, &mut revocation)?;
|
||||||
store.insert_ssh_revocation(&stored_from_ssh_revocation(&revocation))?;
|
store.insert_ssh_revocation(&stored_from_ssh_revocation(&revocation))?;
|
||||||
Ok(ControlResponse::SshRevocationAdded { revocation })
|
Ok(ControlResponse::SshRevocationAdded { revocation })
|
||||||
}
|
}
|
||||||
|
|
@ -5390,7 +5403,7 @@ pub fn handle_request(
|
||||||
)?;
|
)?;
|
||||||
let body = std::fs::read_to_string(&path)?;
|
let body = std::fs::read_to_string(&path)?;
|
||||||
let created_at = UnixMillis(geth_store::now_ms());
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
let revocations = match format.as_str() {
|
let mut revocations = match format.as_str() {
|
||||||
"jsonl" => body
|
"jsonl" => body
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| !line.trim().is_empty())
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
|
@ -5408,6 +5421,7 @@ pub fn handle_request(
|
||||||
reason: Some(format!("imported from {}", path.display())),
|
reason: Some(format!("imported from {}", path.display())),
|
||||||
created_at,
|
created_at,
|
||||||
published: true,
|
published: true,
|
||||||
|
provenance: None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
@ -5424,6 +5438,9 @@ pub fn handle_request(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
for revocation in &mut revocations {
|
||||||
|
sign_ssh_revocation(node, revocation)?;
|
||||||
|
}
|
||||||
for revocation in &revocations {
|
for revocation in &revocations {
|
||||||
store.insert_ssh_revocation(&stored_from_ssh_revocation(revocation))?;
|
store.insert_ssh_revocation(&stored_from_ssh_revocation(revocation))?;
|
||||||
}
|
}
|
||||||
|
|
@ -6585,6 +6602,140 @@ fn expected_openssh_cert_path(public_key_path: &Path) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign_ssh_cert_request(node: &LocalNode, request: &mut SshCertRequest) -> Result<(), NodeError> {
|
||||||
|
let key = AgentKey::load(&node.paths.agent_key())?;
|
||||||
|
let signature = key.sign_canonical(
|
||||||
|
SSH_CERT_REQUEST_NAMESPACE,
|
||||||
|
&ssh_cert_request_signing_payload(request),
|
||||||
|
)?;
|
||||||
|
request.provenance = Some(SshRecordProvenance {
|
||||||
|
namespace: SSH_CERT_REQUEST_NAMESPACE.to_owned(),
|
||||||
|
signer_node: NodeId::new(node.node_id.clone()),
|
||||||
|
signer_agent: node.agent_id.clone(),
|
||||||
|
signer_public_key: key.public_key_hex(),
|
||||||
|
signature_hex: hex::encode(signature),
|
||||||
|
signed_at: UnixMillis(geth_store::now_ms()),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_ssh_certificate(
|
||||||
|
node: &LocalNode,
|
||||||
|
certificate: &mut SshCertificateRecord,
|
||||||
|
) -> Result<(), NodeError> {
|
||||||
|
let key = AgentKey::load(&node.paths.agent_key())?;
|
||||||
|
let signature = key.sign_canonical(
|
||||||
|
SSH_CERT_ISSUANCE_NAMESPACE,
|
||||||
|
&ssh_certificate_signing_payload(certificate),
|
||||||
|
)?;
|
||||||
|
certificate.provenance = Some(SshRecordProvenance {
|
||||||
|
namespace: SSH_CERT_ISSUANCE_NAMESPACE.to_owned(),
|
||||||
|
signer_node: NodeId::new(node.node_id.clone()),
|
||||||
|
signer_agent: node.agent_id.clone(),
|
||||||
|
signer_public_key: key.public_key_hex(),
|
||||||
|
signature_hex: hex::encode(signature),
|
||||||
|
signed_at: UnixMillis(geth_store::now_ms()),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_ssh_revocation(
|
||||||
|
node: &LocalNode,
|
||||||
|
revocation: &mut SshRevocationEntry,
|
||||||
|
) -> Result<(), NodeError> {
|
||||||
|
let key = AgentKey::load(&node.paths.agent_key())?;
|
||||||
|
let signature = key.sign_canonical(
|
||||||
|
SSH_REVOCATION_LIST_NAMESPACE,
|
||||||
|
&ssh_revocation_signing_payload(revocation),
|
||||||
|
)?;
|
||||||
|
revocation.provenance = Some(SshRecordProvenance {
|
||||||
|
namespace: SSH_REVOCATION_LIST_NAMESPACE.to_owned(),
|
||||||
|
signer_node: NodeId::new(node.node_id.clone()),
|
||||||
|
signer_agent: node.agent_id.clone(),
|
||||||
|
signer_public_key: key.public_key_hex(),
|
||||||
|
signature_hex: hex::encode(signature),
|
||||||
|
signed_at: UnixMillis(geth_store::now_ms()),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_ssh_cert_request_provenance(request: &SshCertRequest) -> Result<(), NodeError> {
|
||||||
|
let provenance = request.provenance.as_ref().ok_or_else(|| {
|
||||||
|
NodeError::IrohPeer(format!(
|
||||||
|
"SSH cert request {} is missing signed provenance",
|
||||||
|
request.id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if provenance.namespace != SSH_CERT_REQUEST_NAMESPACE {
|
||||||
|
return Err(NodeError::IrohPeer(format!(
|
||||||
|
"SSH cert request {} uses invalid provenance namespace {}",
|
||||||
|
request.id, provenance.namespace
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
verify_record_provenance(
|
||||||
|
&provenance.signer_public_key,
|
||||||
|
SSH_CERT_REQUEST_NAMESPACE,
|
||||||
|
&ssh_cert_request_signing_payload(request),
|
||||||
|
&provenance.signature_hex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_ssh_certificate_provenance(certificate: &SshCertificateRecord) -> Result<(), NodeError> {
|
||||||
|
let provenance = certificate.provenance.as_ref().ok_or_else(|| {
|
||||||
|
NodeError::IrohPeer(format!(
|
||||||
|
"SSH certificate {} is missing signed provenance",
|
||||||
|
certificate.id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if provenance.namespace != SSH_CERT_ISSUANCE_NAMESPACE {
|
||||||
|
return Err(NodeError::IrohPeer(format!(
|
||||||
|
"SSH certificate {} uses invalid provenance namespace {}",
|
||||||
|
certificate.id, provenance.namespace
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
verify_record_provenance(
|
||||||
|
&provenance.signer_public_key,
|
||||||
|
SSH_CERT_ISSUANCE_NAMESPACE,
|
||||||
|
&ssh_certificate_signing_payload(certificate),
|
||||||
|
&provenance.signature_hex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_ssh_revocation_provenance(revocation: &SshRevocationEntry) -> Result<(), NodeError> {
|
||||||
|
let provenance = revocation.provenance.as_ref().ok_or_else(|| {
|
||||||
|
NodeError::IrohPeer(format!(
|
||||||
|
"SSH revocation {} is missing signed provenance",
|
||||||
|
revocation.id
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if provenance.namespace != SSH_REVOCATION_LIST_NAMESPACE {
|
||||||
|
return Err(NodeError::IrohPeer(format!(
|
||||||
|
"SSH revocation {} uses invalid provenance namespace {}",
|
||||||
|
revocation.id, provenance.namespace
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
verify_record_provenance(
|
||||||
|
&provenance.signer_public_key,
|
||||||
|
SSH_REVOCATION_LIST_NAMESPACE,
|
||||||
|
&ssh_revocation_signing_payload(revocation),
|
||||||
|
&provenance.signature_hex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_record_provenance<T: serde::Serialize + ?Sized>(
|
||||||
|
public_key_hex: &str,
|
||||||
|
namespace: &str,
|
||||||
|
payload: &T,
|
||||||
|
signature_hex: &str,
|
||||||
|
) -> Result<(), NodeError> {
|
||||||
|
let public_key =
|
||||||
|
hex::decode(public_key_hex).map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
let signature =
|
||||||
|
hex::decode(signature_hex).map_err(|error| NodeError::IrohPeer(error.to_string()))?;
|
||||||
|
geth_crypto::verify_canonical(&public_key, namespace, payload, &signature)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn stored_from_ssh_cert_request(request: &SshCertRequest) -> StoredSshCertRequest {
|
fn stored_from_ssh_cert_request(request: &SshCertRequest) -> StoredSshCertRequest {
|
||||||
StoredSshCertRequest {
|
StoredSshCertRequest {
|
||||||
request_id: request.id.to_string(),
|
request_id: request.id.to_string(),
|
||||||
|
|
@ -6598,6 +6749,12 @@ fn stored_from_ssh_cert_request(request: &SshCertRequest) -> StoredSshCertReques
|
||||||
reason: request.reason.clone(),
|
reason: request.reason.clone(),
|
||||||
status: request.status.to_string(),
|
status: request.status.to_string(),
|
||||||
created_at_ms: request.created_at.0,
|
created_at_ms: request.created_at.0,
|
||||||
|
provenance_json: request
|
||||||
|
.provenance
|
||||||
|
.as_ref()
|
||||||
|
.map(serde_json::to_string)
|
||||||
|
.transpose()
|
||||||
|
.expect("SSH provenance serializes"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6615,6 +6772,7 @@ fn insert_ssh_cert_request_if_not_conflicting(
|
||||||
}
|
}
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
verify_ssh_cert_request_provenance(request)?;
|
||||||
store.insert_ssh_cert_request(&stored)?;
|
store.insert_ssh_cert_request(&stored)?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
@ -6640,6 +6798,10 @@ fn ssh_cert_request_from_stored(stored: StoredSshCertRequest) -> Result<SshCertR
|
||||||
reason: stored.reason,
|
reason: stored.reason,
|
||||||
status,
|
status,
|
||||||
created_at: UnixMillis(stored.created_at_ms),
|
created_at: UnixMillis(stored.created_at_ms),
|
||||||
|
provenance: stored
|
||||||
|
.provenance_json
|
||||||
|
.map(|json| serde_json::from_str(&json))
|
||||||
|
.transpose()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6650,6 +6812,12 @@ fn stored_from_ssh_certificate(certificate: &SshCertificateRecord) -> StoredSshC
|
||||||
certificate: certificate.certificate.clone(),
|
certificate: certificate.certificate.clone(),
|
||||||
certificate_fingerprint: certificate.certificate_fingerprint.clone(),
|
certificate_fingerprint: certificate.certificate_fingerprint.clone(),
|
||||||
imported_at_ms: certificate.imported_at.0,
|
imported_at_ms: certificate.imported_at.0,
|
||||||
|
provenance_json: certificate
|
||||||
|
.provenance
|
||||||
|
.as_ref()
|
||||||
|
.map(serde_json::to_string)
|
||||||
|
.transpose()
|
||||||
|
.expect("SSH provenance serializes"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6671,18 +6839,25 @@ fn insert_ssh_certificate_if_not_conflicting(
|
||||||
}
|
}
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
verify_ssh_certificate_provenance(certificate)?;
|
||||||
store.insert_ssh_certificate(&stored)?;
|
store.insert_ssh_certificate(&stored)?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_certificate_from_stored(stored: StoredSshCertificate) -> SshCertificateRecord {
|
fn ssh_certificate_from_stored(
|
||||||
SshCertificateRecord {
|
stored: StoredSshCertificate,
|
||||||
|
) -> Result<SshCertificateRecord, NodeError> {
|
||||||
|
Ok(SshCertificateRecord {
|
||||||
id: SshCertId::new(stored.cert_id),
|
id: SshCertId::new(stored.cert_id),
|
||||||
request_id: SshCertRequestId::new(stored.request_id),
|
request_id: SshCertRequestId::new(stored.request_id),
|
||||||
certificate: stored.certificate,
|
certificate: stored.certificate,
|
||||||
certificate_fingerprint: stored.certificate_fingerprint,
|
certificate_fingerprint: stored.certificate_fingerprint,
|
||||||
imported_at: UnixMillis(stored.imported_at_ms),
|
imported_at: UnixMillis(stored.imported_at_ms),
|
||||||
}
|
provenance: stored
|
||||||
|
.provenance_json
|
||||||
|
.map(|json| serde_json::from_str(&json))
|
||||||
|
.transpose()?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stored_from_ssh_revocation(revocation: &SshRevocationEntry) -> StoredSshRevocation {
|
fn stored_from_ssh_revocation(revocation: &SshRevocationEntry) -> StoredSshRevocation {
|
||||||
|
|
@ -6693,6 +6868,12 @@ fn stored_from_ssh_revocation(revocation: &SshRevocationEntry) -> StoredSshRevoc
|
||||||
reason: revocation.reason.clone(),
|
reason: revocation.reason.clone(),
|
||||||
created_at_ms: revocation.created_at.0,
|
created_at_ms: revocation.created_at.0,
|
||||||
published: revocation.published,
|
published: revocation.published,
|
||||||
|
provenance_json: revocation
|
||||||
|
.provenance
|
||||||
|
.as_ref()
|
||||||
|
.map(serde_json::to_string)
|
||||||
|
.transpose()
|
||||||
|
.expect("SSH provenance serializes"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6714,6 +6895,7 @@ fn insert_ssh_revocation_if_not_conflicting(
|
||||||
}
|
}
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
verify_ssh_revocation_provenance(revocation)?;
|
||||||
store.insert_ssh_revocation(&stored)?;
|
store.insert_ssh_revocation(&stored)?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
@ -6732,6 +6914,10 @@ fn ssh_revocation_from_stored(
|
||||||
reason: stored.reason,
|
reason: stored.reason,
|
||||||
created_at: UnixMillis(stored.created_at_ms),
|
created_at: UnixMillis(stored.created_at_ms),
|
||||||
published: stored.published,
|
published: stored.published,
|
||||||
|
provenance: stored
|
||||||
|
.provenance_json
|
||||||
|
.map(|json| serde_json::from_str(&json))
|
||||||
|
.transpose()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6826,7 +7012,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn ssh_sync_import_rejects_conflicting_records() {
|
fn ssh_sync_import_rejects_conflicting_records() {
|
||||||
let store = Store::open_memory().expect("open");
|
let store = Store::open_memory().expect("open");
|
||||||
let request = SshCertRequest {
|
let mut request = SshCertRequest {
|
||||||
id: SshCertRequestId::new("ssh-cert-request:1"),
|
id: SshCertRequestId::new("ssh-cert-request:1"),
|
||||||
requester_node: NodeId::new("node:right"),
|
requester_node: NodeId::new("node:right"),
|
||||||
public_key: "ssh-ed25519 AAAA".to_owned(),
|
public_key: "ssh-ed25519 AAAA".to_owned(),
|
||||||
|
|
@ -6838,7 +7024,9 @@ mod tests {
|
||||||
reason: Some("original".to_owned()),
|
reason: Some("original".to_owned()),
|
||||||
status: SshCertRequestStatus::Pending,
|
status: SshCertRequestStatus::Pending,
|
||||||
created_at: UnixMillis(1),
|
created_at: UnixMillis(1),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
add_test_ssh_cert_request_provenance(&mut request);
|
||||||
assert!(
|
assert!(
|
||||||
insert_ssh_cert_request_if_not_conflicting(&store, &request).expect("insert request")
|
insert_ssh_cert_request_if_not_conflicting(&store, &request).expect("insert request")
|
||||||
);
|
);
|
||||||
|
|
@ -6848,6 +7036,10 @@ mod tests {
|
||||||
!insert_ssh_cert_request_if_not_conflicting(&store, &conflicting_request)
|
!insert_ssh_cert_request_if_not_conflicting(&store, &conflicting_request)
|
||||||
.expect("reject conflicting request")
|
.expect("reject conflicting request")
|
||||||
);
|
);
|
||||||
|
let mut unsigned_request = request.clone();
|
||||||
|
unsigned_request.id = SshCertRequestId::new("ssh-cert-request:unsigned");
|
||||||
|
unsigned_request.provenance = None;
|
||||||
|
assert!(insert_ssh_cert_request_if_not_conflicting(&store, &unsigned_request).is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store
|
store
|
||||||
.get_ssh_cert_request(request.id.as_str())
|
.get_ssh_cert_request(request.id.as_str())
|
||||||
|
|
@ -6858,14 +7050,16 @@ mod tests {
|
||||||
Some("original")
|
Some("original")
|
||||||
);
|
);
|
||||||
|
|
||||||
let revocation = SshRevocationEntry {
|
let mut revocation = SshRevocationEntry {
|
||||||
id: geth_types::SshRevocationId::new("ssh-revocation:1"),
|
id: geth_types::SshRevocationId::new("ssh-revocation:1"),
|
||||||
kind: SshRevocationKind::KeyId,
|
kind: SshRevocationKind::KeyId,
|
||||||
target: "old-key".to_owned(),
|
target: "old-key".to_owned(),
|
||||||
reason: Some("original".to_owned()),
|
reason: Some("original".to_owned()),
|
||||||
created_at: UnixMillis(2),
|
created_at: UnixMillis(2),
|
||||||
published: true,
|
published: true,
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
add_test_ssh_revocation_provenance(&mut revocation);
|
||||||
assert!(
|
assert!(
|
||||||
insert_ssh_revocation_if_not_conflicting(&store, &revocation)
|
insert_ssh_revocation_if_not_conflicting(&store, &revocation)
|
||||||
.expect("insert revocation")
|
.expect("insert revocation")
|
||||||
|
|
@ -6876,6 +7070,10 @@ mod tests {
|
||||||
!insert_ssh_revocation_if_not_conflicting(&store, &conflicting_revocation)
|
!insert_ssh_revocation_if_not_conflicting(&store, &conflicting_revocation)
|
||||||
.expect("reject conflicting revocation")
|
.expect("reject conflicting revocation")
|
||||||
);
|
);
|
||||||
|
let mut unsigned_revocation = revocation.clone();
|
||||||
|
unsigned_revocation.id = geth_types::SshRevocationId::new("ssh-revocation:unsigned");
|
||||||
|
unsigned_revocation.provenance = None;
|
||||||
|
assert!(insert_ssh_revocation_if_not_conflicting(&store, &unsigned_revocation).is_err());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store
|
store
|
||||||
.list_ssh_revocations()
|
.list_ssh_revocations()
|
||||||
|
|
@ -6887,6 +7085,42 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_test_ssh_cert_request_provenance(request: &mut SshCertRequest) {
|
||||||
|
let key = AgentKey::generate();
|
||||||
|
let signature = key
|
||||||
|
.sign_canonical(
|
||||||
|
SSH_CERT_REQUEST_NAMESPACE,
|
||||||
|
&ssh_cert_request_signing_payload(request),
|
||||||
|
)
|
||||||
|
.expect("sign request");
|
||||||
|
request.provenance = Some(SshRecordProvenance {
|
||||||
|
namespace: SSH_CERT_REQUEST_NAMESPACE.to_owned(),
|
||||||
|
signer_node: NodeId::new("node:right"),
|
||||||
|
signer_agent: key.agent_id().to_string(),
|
||||||
|
signer_public_key: key.public_key_hex(),
|
||||||
|
signature_hex: hex::encode(signature),
|
||||||
|
signed_at: UnixMillis(1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_test_ssh_revocation_provenance(revocation: &mut SshRevocationEntry) {
|
||||||
|
let key = AgentKey::generate();
|
||||||
|
let signature = key
|
||||||
|
.sign_canonical(
|
||||||
|
SSH_REVOCATION_LIST_NAMESPACE,
|
||||||
|
&ssh_revocation_signing_payload(revocation),
|
||||||
|
)
|
||||||
|
.expect("sign revocation");
|
||||||
|
revocation.provenance = Some(SshRecordProvenance {
|
||||||
|
namespace: SSH_REVOCATION_LIST_NAMESPACE.to_owned(),
|
||||||
|
signer_node: NodeId::new("node:right"),
|
||||||
|
signer_agent: key.agent_id().to_string(),
|
||||||
|
signer_public_key: key.public_key_hex(),
|
||||||
|
signature_hex: hex::encode(signature),
|
||||||
|
signed_at: UnixMillis(2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_watermarks_include_only_authorized_streams() {
|
fn sync_watermarks_include_only_authorized_streams() {
|
||||||
let store = Store::open_memory().expect("open");
|
let store = Store::open_memory().expect("open");
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,16 @@ impl std::str::FromStr for SshCertRequestStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SshRecordProvenance {
|
||||||
|
pub namespace: String,
|
||||||
|
pub signer_node: NodeId,
|
||||||
|
pub signer_agent: String,
|
||||||
|
pub signer_public_key: String,
|
||||||
|
pub signature_hex: String,
|
||||||
|
pub signed_at: UnixMillis,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct SshCertRequest {
|
pub struct SshCertRequest {
|
||||||
pub id: SshCertRequestId,
|
pub id: SshCertRequestId,
|
||||||
|
|
@ -130,6 +140,7 @@ pub struct SshCertRequest {
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
pub status: SshCertRequestStatus,
|
pub status: SshCertRequestStatus,
|
||||||
pub created_at: UnixMillis,
|
pub created_at: UnixMillis,
|
||||||
|
pub provenance: Option<SshRecordProvenance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -154,6 +165,7 @@ pub struct SshCertificateRecord {
|
||||||
pub certificate: String,
|
pub certificate: String,
|
||||||
pub certificate_fingerprint: String,
|
pub certificate_fingerprint: String,
|
||||||
pub imported_at: UnixMillis,
|
pub imported_at: UnixMillis,
|
||||||
|
pub provenance: Option<SshRecordProvenance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -205,6 +217,85 @@ pub struct SshRevocationEntry {
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
pub created_at: UnixMillis,
|
pub created_at: UnixMillis,
|
||||||
pub published: bool,
|
pub published: bool,
|
||||||
|
pub provenance: Option<SshRecordProvenance>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SshCertRequestSigningPayload {
|
||||||
|
pub id: SshCertRequestId,
|
||||||
|
pub requester_node: NodeId,
|
||||||
|
pub public_key: String,
|
||||||
|
pub public_key_fingerprint: String,
|
||||||
|
pub cert_kind: SshCertKind,
|
||||||
|
pub principals: Vec<String>,
|
||||||
|
pub requested_validity: Option<String>,
|
||||||
|
pub renewal_of: Option<SshCertId>,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub status: SshCertRequestStatus,
|
||||||
|
pub created_at: UnixMillis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SshCertificateSigningPayload {
|
||||||
|
pub id: SshCertId,
|
||||||
|
pub request_id: SshCertRequestId,
|
||||||
|
pub certificate: String,
|
||||||
|
pub certificate_fingerprint: String,
|
||||||
|
pub imported_at: UnixMillis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SshRevocationSigningPayload {
|
||||||
|
pub id: SshRevocationId,
|
||||||
|
pub kind: SshRevocationKind,
|
||||||
|
pub target: String,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub created_at: UnixMillis,
|
||||||
|
pub published: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn ssh_cert_request_signing_payload(request: &SshCertRequest) -> SshCertRequestSigningPayload {
|
||||||
|
SshCertRequestSigningPayload {
|
||||||
|
id: request.id.clone(),
|
||||||
|
requester_node: request.requester_node.clone(),
|
||||||
|
public_key: request.public_key.clone(),
|
||||||
|
public_key_fingerprint: request.public_key_fingerprint.clone(),
|
||||||
|
cert_kind: request.cert_kind.clone(),
|
||||||
|
principals: request.principals.clone(),
|
||||||
|
requested_validity: request.requested_validity.clone(),
|
||||||
|
renewal_of: request.renewal_of.clone(),
|
||||||
|
reason: request.reason.clone(),
|
||||||
|
status: request.status.clone(),
|
||||||
|
created_at: request.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn ssh_certificate_signing_payload(
|
||||||
|
certificate: &SshCertificateRecord,
|
||||||
|
) -> SshCertificateSigningPayload {
|
||||||
|
SshCertificateSigningPayload {
|
||||||
|
id: certificate.id.clone(),
|
||||||
|
request_id: certificate.request_id.clone(),
|
||||||
|
certificate: certificate.certificate.clone(),
|
||||||
|
certificate_fingerprint: certificate.certificate_fingerprint.clone(),
|
||||||
|
imported_at: certificate.imported_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn ssh_revocation_signing_payload(
|
||||||
|
revocation: &SshRevocationEntry,
|
||||||
|
) -> SshRevocationSigningPayload {
|
||||||
|
SshRevocationSigningPayload {
|
||||||
|
id: revocation.id.clone(),
|
||||||
|
kind: revocation.kind.clone(),
|
||||||
|
target: revocation.target.clone(),
|
||||||
|
reason: revocation.reason.clone(),
|
||||||
|
created_at: revocation.created_at,
|
||||||
|
published: revocation.published,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|
@ -449,6 +540,7 @@ mod tests {
|
||||||
reason: Some("bootstrap".to_owned()),
|
reason: Some("bootstrap".to_owned()),
|
||||||
status: SshCertRequestStatus::Pending,
|
status: SshCertRequestStatus::Pending,
|
||||||
created_at: UnixMillis(1),
|
created_at: UnixMillis(1),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&request).expect("json");
|
let json = serde_json::to_string(&request).expect("json");
|
||||||
let decoded: SshCertRequest = serde_json::from_str(&json).expect("decode");
|
let decoded: SshCertRequest = serde_json::from_str(&json).expect("decode");
|
||||||
|
|
@ -469,6 +561,7 @@ mod tests {
|
||||||
reason: None,
|
reason: None,
|
||||||
status: SshCertRequestStatus::Pending,
|
status: SshCertRequestStatus::Pending,
|
||||||
created_at: UnixMillis(1),
|
created_at: UnixMillis(1),
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
let command = build_ssh_cert_sign_command(
|
let command = build_ssh_cert_sign_command(
|
||||||
&request,
|
&request,
|
||||||
|
|
@ -492,6 +585,7 @@ mod tests {
|
||||||
reason: None,
|
reason: None,
|
||||||
created_at: UnixMillis(1),
|
created_at: UnixMillis(1),
|
||||||
published: false,
|
published: false,
|
||||||
|
provenance: None,
|
||||||
},
|
},
|
||||||
SshRevocationEntry {
|
SshRevocationEntry {
|
||||||
id: "ssh-revocation:2".into(),
|
id: "ssh-revocation:2".into(),
|
||||||
|
|
@ -500,6 +594,7 @@ mod tests {
|
||||||
reason: None,
|
reason: None,
|
||||||
created_at: UnixMillis(2),
|
created_at: UnixMillis(2),
|
||||||
published: false,
|
published: false,
|
||||||
|
provenance: None,
|
||||||
},
|
},
|
||||||
SshRevocationEntry {
|
SshRevocationEntry {
|
||||||
id: "ssh-revocation:3".into(),
|
id: "ssh-revocation:3".into(),
|
||||||
|
|
@ -508,6 +603,7 @@ mod tests {
|
||||||
reason: None,
|
reason: None,
|
||||||
created_at: UnixMillis(3),
|
created_at: UnixMillis(3),
|
||||||
published: false,
|
published: false,
|
||||||
|
provenance: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -552,6 +648,7 @@ mod tests {
|
||||||
reason: None,
|
reason: None,
|
||||||
created_at: UnixMillis(1),
|
created_at: UnixMillis(1),
|
||||||
published: false,
|
published: false,
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(openssh_krl_spec_line(&entry).is_err());
|
assert!(openssh_krl_spec_line(&entry).is_err());
|
||||||
|
|
@ -586,6 +683,7 @@ mod tests {
|
||||||
reason: Some("test".to_owned()),
|
reason: Some("test".to_owned()),
|
||||||
created_at: UnixMillis(1),
|
created_at: UnixMillis(1),
|
||||||
published: true,
|
published: true,
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
let krl_path = dir.path().join("revoked.krl");
|
let krl_path = dir.path().join("revoked.krl");
|
||||||
|
|
||||||
|
|
@ -666,6 +764,7 @@ mod tests {
|
||||||
reason: Some("test certificate revocation".to_owned()),
|
reason: Some("test certificate revocation".to_owned()),
|
||||||
created_at: UnixMillis(1),
|
created_at: UnixMillis(1),
|
||||||
published: true,
|
published: true,
|
||||||
|
provenance: None,
|
||||||
};
|
};
|
||||||
let krl_path = dir.path().join("revoked-certs.krl");
|
let krl_path = dir.path().join("revoked-certs.krl");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,14 +186,16 @@ impl Store {
|
||||||
renewal_of TEXT,
|
renewal_of TEXT,
|
||||||
reason TEXT,
|
reason TEXT,
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
created_at_ms INTEGER NOT NULL
|
created_at_ms INTEGER NOT NULL,
|
||||||
|
provenance_json TEXT
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS ssh_certificates (
|
CREATE TABLE IF NOT EXISTS ssh_certificates (
|
||||||
cert_id TEXT PRIMARY KEY,
|
cert_id TEXT PRIMARY KEY,
|
||||||
request_id TEXT NOT NULL,
|
request_id TEXT NOT NULL,
|
||||||
certificate TEXT NOT NULL,
|
certificate TEXT NOT NULL,
|
||||||
certificate_fingerprint TEXT NOT NULL,
|
certificate_fingerprint TEXT NOT NULL,
|
||||||
imported_at_ms INTEGER NOT NULL
|
imported_at_ms INTEGER NOT NULL,
|
||||||
|
provenance_json TEXT
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS ssh_revocations (
|
CREATE TABLE IF NOT EXISTS ssh_revocations (
|
||||||
revocation_id TEXT PRIMARY KEY,
|
revocation_id TEXT PRIMARY KEY,
|
||||||
|
|
@ -201,7 +203,8 @@ impl Store {
|
||||||
target TEXT NOT NULL,
|
target TEXT NOT NULL,
|
||||||
reason TEXT,
|
reason TEXT,
|
||||||
created_at_ms INTEGER NOT NULL,
|
created_at_ms INTEGER NOT NULL,
|
||||||
published INTEGER NOT NULL DEFAULT 0
|
published INTEGER NOT NULL DEFAULT 0,
|
||||||
|
provenance_json TEXT
|
||||||
);
|
);
|
||||||
INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '1');
|
INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '1');
|
||||||
"#,
|
"#,
|
||||||
|
|
@ -211,6 +214,9 @@ impl Store {
|
||||||
"signer_public_key",
|
"signer_public_key",
|
||||||
"TEXT NOT NULL DEFAULT ''",
|
"TEXT NOT NULL DEFAULT ''",
|
||||||
)?;
|
)?;
|
||||||
|
self.add_column_if_missing("ssh_cert_requests", "provenance_json", "TEXT")?;
|
||||||
|
self.add_column_if_missing("ssh_certificates", "provenance_json", "TEXT")?;
|
||||||
|
self.add_column_if_missing("ssh_revocations", "provenance_json", "TEXT")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1001,8 +1007,9 @@ impl Store {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
r#"INSERT OR REPLACE INTO ssh_cert_requests(
|
r#"INSERT OR REPLACE INTO ssh_cert_requests(
|
||||||
request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
||||||
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
|
principals_json, requested_validity, renewal_of, reason, status, created_at_ms,
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"#,
|
provenance_json
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)"#,
|
||||||
params![
|
params![
|
||||||
request.request_id,
|
request.request_id,
|
||||||
request.requester_node,
|
request.requester_node,
|
||||||
|
|
@ -1014,7 +1021,8 @@ impl Store {
|
||||||
request.renewal_of,
|
request.renewal_of,
|
||||||
request.reason,
|
request.reason,
|
||||||
request.status,
|
request.status,
|
||||||
request.created_at_ms
|
request.created_at_ms,
|
||||||
|
request.provenance_json
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -1026,7 +1034,8 @@ impl Store {
|
||||||
) -> Result<Option<StoredSshCertRequest>, StoreError> {
|
) -> Result<Option<StoredSshCertRequest>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
||||||
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
|
principals_json, requested_validity, renewal_of, reason, status, created_at_ms,
|
||||||
|
provenance_json
|
||||||
FROM ssh_cert_requests WHERE request_id = ?1"#,
|
FROM ssh_cert_requests WHERE request_id = ?1"#,
|
||||||
)?;
|
)?;
|
||||||
let mut rows = stmt.query(params![request_id])?;
|
let mut rows = stmt.query(params![request_id])?;
|
||||||
|
|
@ -1052,7 +1061,8 @@ impl Store {
|
||||||
pub fn list_ssh_cert_requests(&self) -> Result<Vec<StoredSshCertRequest>, StoreError> {
|
pub fn list_ssh_cert_requests(&self) -> Result<Vec<StoredSshCertRequest>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
||||||
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
|
principals_json, requested_validity, renewal_of, reason, status, created_at_ms,
|
||||||
|
provenance_json
|
||||||
FROM ssh_cert_requests ORDER BY created_at_ms, request_id"#,
|
FROM ssh_cert_requests ORDER BY created_at_ms, request_id"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map([], stored_ssh_cert_request_from_row)?;
|
let rows = stmt.query_map([], stored_ssh_cert_request_from_row)?;
|
||||||
|
|
@ -1066,7 +1076,8 @@ impl Store {
|
||||||
) -> Result<Vec<StoredSshCertRequest>, StoreError> {
|
) -> Result<Vec<StoredSshCertRequest>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
r#"SELECT request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
|
||||||
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
|
principals_json, requested_validity, renewal_of, reason, status, created_at_ms,
|
||||||
|
provenance_json
|
||||||
FROM ssh_cert_requests WHERE created_at_ms >= ?1 ORDER BY created_at_ms, request_id"#,
|
FROM ssh_cert_requests WHERE created_at_ms >= ?1 ORDER BY created_at_ms, request_id"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map(params![since_ms], stored_ssh_cert_request_from_row)?;
|
let rows = stmt.query_map(params![since_ms], stored_ssh_cert_request_from_row)?;
|
||||||
|
|
@ -1080,14 +1091,16 @@ impl Store {
|
||||||
) -> Result<(), StoreError> {
|
) -> Result<(), StoreError> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
r#"INSERT OR REPLACE INTO ssh_certificates(
|
r#"INSERT OR REPLACE INTO ssh_certificates(
|
||||||
cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
|
cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms,
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5)"#,
|
provenance_json
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
||||||
params![
|
params![
|
||||||
certificate.cert_id,
|
certificate.cert_id,
|
||||||
certificate.request_id,
|
certificate.request_id,
|
||||||
certificate.certificate,
|
certificate.certificate,
|
||||||
certificate.certificate_fingerprint,
|
certificate.certificate_fingerprint,
|
||||||
certificate.imported_at_ms
|
certificate.imported_at_ms,
|
||||||
|
certificate.provenance_json
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -1096,6 +1109,7 @@ impl Store {
|
||||||
pub fn list_ssh_certificates(&self) -> Result<Vec<StoredSshCertificate>, StoreError> {
|
pub fn list_ssh_certificates(&self) -> Result<Vec<StoredSshCertificate>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
|
r#"SELECT cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
|
||||||
|
, provenance_json
|
||||||
FROM ssh_certificates ORDER BY imported_at_ms, cert_id"#,
|
FROM ssh_certificates ORDER BY imported_at_ms, cert_id"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let rows = stmt.query_map([], |row| {
|
||||||
|
|
@ -1105,6 +1119,7 @@ impl Store {
|
||||||
certificate: row.get(2)?,
|
certificate: row.get(2)?,
|
||||||
certificate_fingerprint: row.get(3)?,
|
certificate_fingerprint: row.get(3)?,
|
||||||
imported_at_ms: row.get(4)?,
|
imported_at_ms: row.get(4)?,
|
||||||
|
provenance_json: row.get(5)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect::<Result<Vec<_>, _>>()
|
rows.collect::<Result<Vec<_>, _>>()
|
||||||
|
|
@ -1117,6 +1132,7 @@ impl Store {
|
||||||
) -> Result<Vec<StoredSshCertificate>, StoreError> {
|
) -> Result<Vec<StoredSshCertificate>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
|
r#"SELECT cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
|
||||||
|
, provenance_json
|
||||||
FROM ssh_certificates WHERE imported_at_ms >= ?1 ORDER BY imported_at_ms, cert_id"#,
|
FROM ssh_certificates WHERE imported_at_ms >= ?1 ORDER BY imported_at_ms, cert_id"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map(params![since_ms], |row| {
|
let rows = stmt.query_map(params![since_ms], |row| {
|
||||||
|
|
@ -1126,6 +1142,7 @@ impl Store {
|
||||||
certificate: row.get(2)?,
|
certificate: row.get(2)?,
|
||||||
certificate_fingerprint: row.get(3)?,
|
certificate_fingerprint: row.get(3)?,
|
||||||
imported_at_ms: row.get(4)?,
|
imported_at_ms: row.get(4)?,
|
||||||
|
provenance_json: row.get(5)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect::<Result<Vec<_>, _>>()
|
rows.collect::<Result<Vec<_>, _>>()
|
||||||
|
|
@ -1138,15 +1155,16 @@ impl Store {
|
||||||
) -> Result<(), StoreError> {
|
) -> Result<(), StoreError> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
r#"INSERT OR REPLACE INTO ssh_revocations(
|
r#"INSERT OR REPLACE INTO ssh_revocations(
|
||||||
revocation_id, kind, target, reason, created_at_ms, published
|
revocation_id, kind, target, reason, created_at_ms, published, provenance_json
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"#,
|
||||||
params![
|
params![
|
||||||
revocation.revocation_id,
|
revocation.revocation_id,
|
||||||
revocation.kind,
|
revocation.kind,
|
||||||
revocation.target,
|
revocation.target,
|
||||||
revocation.reason,
|
revocation.reason,
|
||||||
revocation.created_at_ms,
|
revocation.created_at_ms,
|
||||||
if revocation.published { 1_i64 } else { 0_i64 }
|
if revocation.published { 1_i64 } else { 0_i64 },
|
||||||
|
revocation.provenance_json
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -1155,6 +1173,7 @@ impl Store {
|
||||||
pub fn list_ssh_revocations(&self) -> Result<Vec<StoredSshRevocation>, StoreError> {
|
pub fn list_ssh_revocations(&self) -> Result<Vec<StoredSshRevocation>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT revocation_id, kind, target, reason, created_at_ms, published
|
r#"SELECT revocation_id, kind, target, reason, created_at_ms, published
|
||||||
|
, provenance_json
|
||||||
FROM ssh_revocations ORDER BY created_at_ms, revocation_id"#,
|
FROM ssh_revocations ORDER BY created_at_ms, revocation_id"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let rows = stmt.query_map([], |row| {
|
||||||
|
|
@ -1165,6 +1184,7 @@ impl Store {
|
||||||
reason: row.get(3)?,
|
reason: row.get(3)?,
|
||||||
created_at_ms: row.get(4)?,
|
created_at_ms: row.get(4)?,
|
||||||
published: row.get::<_, i64>(5)? != 0,
|
published: row.get::<_, i64>(5)? != 0,
|
||||||
|
provenance_json: row.get(6)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect::<Result<Vec<_>, _>>()
|
rows.collect::<Result<Vec<_>, _>>()
|
||||||
|
|
@ -1177,6 +1197,7 @@ impl Store {
|
||||||
) -> Result<Vec<StoredSshRevocation>, StoreError> {
|
) -> Result<Vec<StoredSshRevocation>, StoreError> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
r#"SELECT revocation_id, kind, target, reason, created_at_ms, published
|
r#"SELECT revocation_id, kind, target, reason, created_at_ms, published
|
||||||
|
, provenance_json
|
||||||
FROM ssh_revocations WHERE created_at_ms >= ?1 ORDER BY created_at_ms, revocation_id"#,
|
FROM ssh_revocations WHERE created_at_ms >= ?1 ORDER BY created_at_ms, revocation_id"#,
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map(params![since_ms], |row| {
|
let rows = stmt.query_map(params![since_ms], |row| {
|
||||||
|
|
@ -1187,6 +1208,7 @@ impl Store {
|
||||||
reason: row.get(3)?,
|
reason: row.get(3)?,
|
||||||
created_at_ms: row.get(4)?,
|
created_at_ms: row.get(4)?,
|
||||||
published: row.get::<_, i64>(5)? != 0,
|
published: row.get::<_, i64>(5)? != 0,
|
||||||
|
provenance_json: row.get(6)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect::<Result<Vec<_>, _>>()
|
rows.collect::<Result<Vec<_>, _>>()
|
||||||
|
|
@ -1213,6 +1235,7 @@ fn stored_ssh_cert_request_from_row(
|
||||||
reason: row.get(8)?,
|
reason: row.get(8)?,
|
||||||
status: row.get(9)?,
|
status: row.get(9)?,
|
||||||
created_at_ms: row.get(10)?,
|
created_at_ms: row.get(10)?,
|
||||||
|
provenance_json: row.get(11)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1388,6 +1411,7 @@ pub struct StoredSshCertRequest {
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub created_at_ms: i64,
|
pub created_at_ms: i64,
|
||||||
|
pub provenance_json: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -1397,6 +1421,7 @@ pub struct StoredSshCertificate {
|
||||||
pub certificate: String,
|
pub certificate: String,
|
||||||
pub certificate_fingerprint: String,
|
pub certificate_fingerprint: String,
|
||||||
pub imported_at_ms: i64,
|
pub imported_at_ms: i64,
|
||||||
|
pub provenance_json: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
@ -1407,6 +1432,7 @@ pub struct StoredSshRevocation {
|
||||||
pub reason: Option<String>,
|
pub reason: Option<String>,
|
||||||
pub created_at_ms: i64,
|
pub created_at_ms: i64,
|
||||||
pub published: bool,
|
pub published: bool,
|
||||||
|
pub provenance_json: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -1445,6 +1471,7 @@ mod tests {
|
||||||
reason: Some("renewal".to_owned()),
|
reason: Some("renewal".to_owned()),
|
||||||
status: "pending".to_owned(),
|
status: "pending".to_owned(),
|
||||||
created_at_ms: 1,
|
created_at_ms: 1,
|
||||||
|
provenance_json: Some(r#"{"test":true}"#.to_owned()),
|
||||||
};
|
};
|
||||||
store
|
store
|
||||||
.insert_ssh_cert_request(&request)
|
.insert_ssh_cert_request(&request)
|
||||||
|
|
@ -1463,6 +1490,7 @@ mod tests {
|
||||||
reason: Some("lost key".to_owned()),
|
reason: Some("lost key".to_owned()),
|
||||||
created_at_ms: 2,
|
created_at_ms: 2,
|
||||||
published: true,
|
published: true,
|
||||||
|
provenance_json: Some(r#"{"test":true}"#.to_owned()),
|
||||||
};
|
};
|
||||||
store
|
store
|
||||||
.insert_ssh_revocation(&revocation)
|
.insert_ssh_revocation(&revocation)
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,10 @@ resource-scoped capability decisions.
|
||||||
revocation records with ids that already exist locally.
|
revocation records with ids that already exist locally.
|
||||||
- `[x]` Tests verify conflicting SSH cert request and revocation records do
|
- `[x]` Tests verify conflicting SSH cert request and revocation records do
|
||||||
not overwrite local metadata.
|
not overwrite local metadata.
|
||||||
- `[ ]` Future completion requires all accepted SSH cert/revocation records
|
- `[x]` Accepted SSH cert/revocation records carry agent-key signed
|
||||||
to carry signed provenance and reduce cleanly before replication.
|
provenance over deterministic canonical payloads.
|
||||||
|
- `[x]` Sync import rejects unsigned or invalidly signed cert-flow and
|
||||||
|
revocation records that are not already-known conflicting ids.
|
||||||
|
|
||||||
## Phase 3: CAS, KV, And Pubsub
|
## Phase 3: CAS, KV, And Pubsub
|
||||||
|
|
||||||
|
|
@ -429,8 +431,8 @@ Goal: add authorized stream-oriented management workflows over Iroh.
|
||||||
- `[ ]` Replace pull-only metadata sync with a resource log or CRDT model.
|
- `[ ]` Replace pull-only metadata sync with a resource log or CRDT model.
|
||||||
- `[x]` Conflicting records with already-known ids are rejected during import
|
- `[x]` Conflicting records with already-known ids are rejected during import
|
||||||
rather than replacing local metadata.
|
rather than replacing local metadata.
|
||||||
- `[ ]` Unsigned records are rejected or quarantined once signed provenance is
|
- `[x]` Unsigned records are rejected during sync import once signed
|
||||||
part of the metadata format.
|
provenance is part of the metadata format.
|
||||||
|
|
||||||
- `[x]` OpenSSH KRL import/export.
|
- `[x]` OpenSSH KRL import/export.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue