Require signed SSH metadata provenance

This commit is contained in:
Eric Wendland 2026-05-21 01:29:55 +02:00
commit 6e04e786c2
8 changed files with 414 additions and 45 deletions

View file

@ -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)]
pub struct SshCertRequest {
pub id: SshCertRequestId,
@ -130,6 +140,7 @@ pub struct SshCertRequest {
pub reason: Option<String>,
pub status: SshCertRequestStatus,
pub created_at: UnixMillis,
pub provenance: Option<SshRecordProvenance>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -154,6 +165,7 @@ pub struct SshCertificateRecord {
pub certificate: String,
pub certificate_fingerprint: String,
pub imported_at: UnixMillis,
pub provenance: Option<SshRecordProvenance>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -205,6 +217,85 @@ pub struct SshRevocationEntry {
pub reason: Option<String>,
pub created_at: UnixMillis,
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)]
@ -449,6 +540,7 @@ mod tests {
reason: Some("bootstrap".to_owned()),
status: SshCertRequestStatus::Pending,
created_at: UnixMillis(1),
provenance: None,
};
let json = serde_json::to_string(&request).expect("json");
let decoded: SshCertRequest = serde_json::from_str(&json).expect("decode");
@ -469,6 +561,7 @@ mod tests {
reason: None,
status: SshCertRequestStatus::Pending,
created_at: UnixMillis(1),
provenance: None,
};
let command = build_ssh_cert_sign_command(
&request,
@ -492,6 +585,7 @@ mod tests {
reason: None,
created_at: UnixMillis(1),
published: false,
provenance: None,
},
SshRevocationEntry {
id: "ssh-revocation:2".into(),
@ -500,6 +594,7 @@ mod tests {
reason: None,
created_at: UnixMillis(2),
published: false,
provenance: None,
},
SshRevocationEntry {
id: "ssh-revocation:3".into(),
@ -508,6 +603,7 @@ mod tests {
reason: None,
created_at: UnixMillis(3),
published: false,
provenance: None,
},
];
@ -552,6 +648,7 @@ mod tests {
reason: None,
created_at: UnixMillis(1),
published: false,
provenance: None,
};
assert!(openssh_krl_spec_line(&entry).is_err());
@ -586,6 +683,7 @@ mod tests {
reason: Some("test".to_owned()),
created_at: UnixMillis(1),
published: true,
provenance: None,
};
let krl_path = dir.path().join("revoked.krl");
@ -666,6 +764,7 @@ mod tests {
reason: Some("test certificate revocation".to_owned()),
created_at: UnixMillis(1),
published: true,
provenance: None,
};
let krl_path = dir.path().join("revoked-certs.krl");