fix: batch ssh sync imports

This commit is contained in:
Eric Wendland 2026-07-05 23:19:05 +02:00
commit 7c0399b2ef
3 changed files with 321 additions and 17 deletions

View file

@ -11622,10 +11622,24 @@ fn stored_from_ssh_cert_request(request: &SshCertRequest) -> StoredSshCertReques
}
}
#[cfg(test)]
fn insert_ssh_cert_request_if_not_conflicting(
store: &Store,
request: &SshCertRequest,
) -> Result<bool, NodeError> {
match stage_ssh_cert_request_if_not_conflicting(store, request)? {
Some(stored) => {
store.insert_ssh_cert_request(&stored)?;
Ok(true)
}
None => Ok(false),
}
}
fn stage_ssh_cert_request_if_not_conflicting(
store: &Store,
request: &SshCertRequest,
) -> Result<Option<StoredSshCertRequest>, NodeError> {
let stored = stored_from_ssh_cert_request(request);
if let Some(existing) = store.get_ssh_cert_request(&stored.request_id)? {
if existing != stored {
@ -11634,11 +11648,10 @@ fn insert_ssh_cert_request_if_not_conflicting(
"rejected conflicting SSH certificate request during sync"
);
}
return Ok(false);
return Ok(None);
}
verify_ssh_cert_request_provenance(request)?;
store.insert_ssh_cert_request(&stored)?;
Ok(true)
Ok(Some(stored))
}
fn ssh_cert_request_from_stored(stored: StoredSshCertRequest) -> Result<SshCertRequest, NodeError> {
@ -11685,10 +11698,24 @@ fn stored_from_ssh_certificate(certificate: &SshCertificateRecord) -> StoredSshC
}
}
#[cfg(test)]
fn insert_ssh_certificate_if_not_conflicting(
store: &Store,
certificate: &SshCertificateRecord,
) -> Result<bool, NodeError> {
match stage_ssh_certificate_if_not_conflicting(store, certificate)? {
Some(stored) => {
store.insert_ssh_certificate(&stored)?;
Ok(true)
}
None => Ok(false),
}
}
fn stage_ssh_certificate_if_not_conflicting(
store: &Store,
certificate: &SshCertificateRecord,
) -> Result<Option<StoredSshCertificate>, NodeError> {
let stored = stored_from_ssh_certificate(certificate);
if let Some(existing) = store
.list_ssh_certificates()?
@ -11701,11 +11728,10 @@ fn insert_ssh_certificate_if_not_conflicting(
"rejected conflicting SSH certificate during sync"
);
}
return Ok(false);
return Ok(None);
}
verify_ssh_certificate_provenance(certificate)?;
store.insert_ssh_certificate(&stored)?;
Ok(true)
Ok(Some(stored))
}
fn ssh_certificate_from_stored(
@ -11741,10 +11767,24 @@ fn stored_from_ssh_revocation(revocation: &SshRevocationEntry) -> StoredSshRevoc
}
}
#[cfg(test)]
fn insert_ssh_revocation_if_not_conflicting(
store: &Store,
revocation: &SshRevocationEntry,
) -> Result<bool, NodeError> {
match stage_ssh_revocation_if_not_conflicting(store, revocation)? {
Some(stored) => {
store.insert_ssh_revocation(&stored)?;
Ok(true)
}
None => Ok(false),
}
}
fn stage_ssh_revocation_if_not_conflicting(
store: &Store,
revocation: &SshRevocationEntry,
) -> Result<Option<StoredSshRevocation>, NodeError> {
let stored = stored_from_ssh_revocation(revocation);
if let Some(existing) = store
.list_ssh_revocations()?
@ -11757,10 +11797,69 @@ fn insert_ssh_revocation_if_not_conflicting(
"rejected conflicting SSH revocation during sync"
);
}
return Ok(false);
return Ok(None);
}
verify_ssh_revocation_provenance(revocation)?;
store.insert_ssh_revocation(&stored)?;
Ok(Some(stored))
}
fn stage_unique_ssh_cert_request(
staged: &mut Vec<StoredSshCertRequest>,
stored: StoredSshCertRequest,
) -> Result<bool, NodeError> {
if let Some(existing) = staged
.iter()
.find(|existing| existing.request_id == stored.request_id)
{
if existing == &stored {
return Ok(false);
}
return Err(NodeError::IrohPeer(format!(
"conflicting SSH certificate request in sync batch: {}",
stored.request_id
)));
}
staged.push(stored);
Ok(true)
}
fn stage_unique_ssh_certificate(
staged: &mut Vec<StoredSshCertificate>,
stored: StoredSshCertificate,
) -> Result<bool, NodeError> {
if let Some(existing) = staged
.iter()
.find(|existing| existing.cert_id == stored.cert_id)
{
if existing == &stored {
return Ok(false);
}
return Err(NodeError::IrohPeer(format!(
"conflicting SSH certificate in sync batch: {}",
stored.cert_id
)));
}
staged.push(stored);
Ok(true)
}
fn stage_unique_ssh_revocation(
staged: &mut Vec<StoredSshRevocation>,
stored: StoredSshRevocation,
) -> Result<bool, NodeError> {
if let Some(existing) = staged
.iter()
.find(|existing| existing.revocation_id == stored.revocation_id)
{
if existing == &stored {
return Ok(false);
}
return Err(NodeError::IrohPeer(format!(
"conflicting SSH revocation in sync batch: {}",
stored.revocation_id
)));
}
staged.push(stored);
Ok(true)
}
@ -11809,25 +11908,39 @@ fn import_ssh_distribution_log_entries(
let mut requests_imported = 0;
let mut certificates_imported = 0;
let mut revocations_imported = 0;
let mut requests = Vec::new();
let mut certificates = Vec::new();
let mut revocations = Vec::new();
for entry in entries {
match entry {
SshDistributionLogEntry::CertRequest { request, .. } => {
if insert_ssh_cert_request_if_not_conflicting(store, request)? {
if let Some(stored) = stage_ssh_cert_request_if_not_conflicting(store, request)? {
if !stage_unique_ssh_cert_request(&mut requests, stored)? {
continue;
}
requests_imported += 1;
}
}
SshDistributionLogEntry::Certificate { certificate, .. } => {
if insert_ssh_certificate_if_not_conflicting(store, certificate)? {
if let Some(stored) = stage_ssh_certificate_if_not_conflicting(store, certificate)?
{
if !stage_unique_ssh_certificate(&mut certificates, stored)? {
continue;
}
certificates_imported += 1;
}
}
SshDistributionLogEntry::Revocation { revocation, .. } => {
if insert_ssh_revocation_if_not_conflicting(store, revocation)? {
if let Some(stored) = stage_ssh_revocation_if_not_conflicting(store, revocation)? {
if !stage_unique_ssh_revocation(&mut revocations, stored)? {
continue;
}
revocations_imported += 1;
}
}
}
}
store.insert_ssh_distribution_records(&requests, &certificates, &revocations)?;
Ok((
requests_imported,
certificates_imported,
@ -12263,6 +12376,37 @@ mod tests {
Some("original")
);
let mut certificate = SshCertificateRecord {
id: SshCertId::new("ssh-cert:1"),
request_id: request.id.clone(),
certificate: "ssh-ed25519-cert-v01@openssh.com AAAA".to_owned(),
certificate_fingerprint: "SHA256:cert-one".to_owned(),
imported_at: UnixMillis(2),
provenance: None,
};
add_test_ssh_certificate_provenance(&mut certificate);
assert!(
insert_ssh_certificate_if_not_conflicting(&store, &certificate)
.expect("insert certificate")
);
let mut conflicting_certificate = certificate.clone();
conflicting_certificate.certificate = "ssh-ed25519-cert-v01@openssh.com BBBB".to_owned();
conflicting_certificate.provenance = None;
add_test_ssh_certificate_provenance(&mut conflicting_certificate);
assert!(
!insert_ssh_certificate_if_not_conflicting(&store, &conflicting_certificate)
.expect("reject conflicting certificate")
);
assert_eq!(
store
.list_ssh_certificates()
.expect("list certificates")
.first()
.expect("certificate")
.certificate,
"ssh-ed25519-cert-v01@openssh.com AAAA"
);
let mut revocation = SshRevocationEntry {
id: geth_types::SshRevocationId::new("ssh-revocation:1"),
kind: SshRevocationKind::KeyId,
@ -12298,6 +12442,46 @@ mod tests {
);
}
#[test]
fn ssh_sync_import_rejects_batch_conflicts_without_partial_write() {
let store = Store::open_memory().expect("open");
let mut request = SshCertRequest {
id: SshCertRequestId::new("ssh-cert-request:batch-conflict"),
requester_node: NodeId::new("node:right"),
public_key: "ssh-ed25519 AAAA".to_owned(),
public_key_fingerprint: "SHA256:one".to_owned(),
cert_kind: SshCertKind::User,
principals: vec!["eric".to_owned()],
requested_validity: None,
renewal_of: None,
reason: Some("original".to_owned()),
status: SshCertRequestStatus::Pending,
created_at: UnixMillis(1),
provenance: None,
};
add_test_ssh_cert_request_provenance(&mut request);
let mut conflicting_request = request.clone();
conflicting_request.reason = Some("conflict".to_owned());
conflicting_request.provenance = None;
add_test_ssh_cert_request_provenance(&mut conflicting_request);
let result = import_ssh_distribution_log_entries(
&store,
&[
SshDistributionLogEntry::cert_request(request.clone()),
SshDistributionLogEntry::cert_request(conflicting_request),
],
);
assert!(result.is_err());
assert_eq!(
store
.get_ssh_cert_request(request.id.as_str())
.expect("get request"),
None
);
}
fn add_test_ssh_cert_request_provenance(request: &mut SshCertRequest) {
let key = AgentKey::generate();
let signature = key
@ -12316,6 +12500,24 @@ mod tests {
});
}
fn add_test_ssh_certificate_provenance(certificate: &mut SshCertificateRecord) {
let key = AgentKey::generate();
let signature = key
.sign_canonical(
SSH_CERT_ISSUANCE_NAMESPACE,
&ssh_certificate_signing_payload(certificate),
)
.expect("sign certificate");
certificate.provenance = Some(SshRecordProvenance {
namespace: SSH_CERT_ISSUANCE_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),
});
}
fn add_test_ssh_revocation_provenance(revocation: &mut SshRevocationEntry) {
let key = AgentKey::generate();
let signature = key