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

@ -186,14 +186,16 @@ impl Store {
renewal_of TEXT,
reason TEXT,
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 (
cert_id TEXT PRIMARY KEY,
request_id TEXT NOT NULL,
certificate 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 (
revocation_id TEXT PRIMARY KEY,
@ -201,7 +203,8 @@ impl Store {
target TEXT NOT NULL,
reason TEXT,
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');
"#,
@ -211,6 +214,9 @@ impl Store {
"signer_public_key",
"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(())
}
@ -1001,8 +1007,9 @@ impl Store {
self.conn.execute(
r#"INSERT OR REPLACE INTO ssh_cert_requests(
request_id, requester_node, public_key, public_key_fingerprint, cert_kind,
principals_json, requested_validity, renewal_of, reason, status, created_at_ms
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)"#,
principals_json, requested_validity, renewal_of, reason, status, created_at_ms,
provenance_json
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)"#,
params![
request.request_id,
request.requester_node,
@ -1014,7 +1021,8 @@ impl Store {
request.renewal_of,
request.reason,
request.status,
request.created_at_ms
request.created_at_ms,
request.provenance_json
],
)?;
Ok(())
@ -1026,7 +1034,8 @@ impl Store {
) -> Result<Option<StoredSshCertRequest>, StoreError> {
let mut stmt = self.conn.prepare(
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"#,
)?;
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> {
let mut stmt = self.conn.prepare(
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"#,
)?;
let rows = stmt.query_map([], stored_ssh_cert_request_from_row)?;
@ -1066,7 +1076,8 @@ impl Store {
) -> Result<Vec<StoredSshCertRequest>, StoreError> {
let mut stmt = self.conn.prepare(
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"#,
)?;
let rows = stmt.query_map(params![since_ms], stored_ssh_cert_request_from_row)?;
@ -1080,14 +1091,16 @@ impl Store {
) -> Result<(), StoreError> {
self.conn.execute(
r#"INSERT OR REPLACE INTO ssh_certificates(
cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
) VALUES (?1, ?2, ?3, ?4, ?5)"#,
cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms,
provenance_json
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
params![
certificate.cert_id,
certificate.request_id,
certificate.certificate,
certificate.certificate_fingerprint,
certificate.imported_at_ms
certificate.imported_at_ms,
certificate.provenance_json
],
)?;
Ok(())
@ -1096,6 +1109,7 @@ impl Store {
pub fn list_ssh_certificates(&self) -> Result<Vec<StoredSshCertificate>, StoreError> {
let mut stmt = self.conn.prepare(
r#"SELECT cert_id, request_id, certificate, certificate_fingerprint, imported_at_ms
, provenance_json
FROM ssh_certificates ORDER BY imported_at_ms, cert_id"#,
)?;
let rows = stmt.query_map([], |row| {
@ -1105,6 +1119,7 @@ impl Store {
certificate: row.get(2)?,
certificate_fingerprint: row.get(3)?,
imported_at_ms: row.get(4)?,
provenance_json: row.get(5)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>()
@ -1117,6 +1132,7 @@ impl Store {
) -> Result<Vec<StoredSshCertificate>, StoreError> {
let mut stmt = self.conn.prepare(
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"#,
)?;
let rows = stmt.query_map(params![since_ms], |row| {
@ -1126,6 +1142,7 @@ impl Store {
certificate: row.get(2)?,
certificate_fingerprint: row.get(3)?,
imported_at_ms: row.get(4)?,
provenance_json: row.get(5)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>()
@ -1138,15 +1155,16 @@ impl Store {
) -> Result<(), StoreError> {
self.conn.execute(
r#"INSERT OR REPLACE INTO ssh_revocations(
revocation_id, kind, target, reason, created_at_ms, published
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
revocation_id, kind, target, reason, created_at_ms, published, provenance_json
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"#,
params![
revocation.revocation_id,
revocation.kind,
revocation.target,
revocation.reason,
revocation.created_at_ms,
if revocation.published { 1_i64 } else { 0_i64 }
if revocation.published { 1_i64 } else { 0_i64 },
revocation.provenance_json
],
)?;
Ok(())
@ -1155,6 +1173,7 @@ impl Store {
pub fn list_ssh_revocations(&self) -> Result<Vec<StoredSshRevocation>, StoreError> {
let mut stmt = self.conn.prepare(
r#"SELECT revocation_id, kind, target, reason, created_at_ms, published
, provenance_json
FROM ssh_revocations ORDER BY created_at_ms, revocation_id"#,
)?;
let rows = stmt.query_map([], |row| {
@ -1165,6 +1184,7 @@ impl Store {
reason: row.get(3)?,
created_at_ms: row.get(4)?,
published: row.get::<_, i64>(5)? != 0,
provenance_json: row.get(6)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>()
@ -1177,6 +1197,7 @@ impl Store {
) -> Result<Vec<StoredSshRevocation>, StoreError> {
let mut stmt = self.conn.prepare(
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"#,
)?;
let rows = stmt.query_map(params![since_ms], |row| {
@ -1187,6 +1208,7 @@ impl Store {
reason: row.get(3)?,
created_at_ms: row.get(4)?,
published: row.get::<_, i64>(5)? != 0,
provenance_json: row.get(6)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>()
@ -1213,6 +1235,7 @@ fn stored_ssh_cert_request_from_row(
reason: row.get(8)?,
status: row.get(9)?,
created_at_ms: row.get(10)?,
provenance_json: row.get(11)?,
})
}
@ -1388,6 +1411,7 @@ pub struct StoredSshCertRequest {
pub reason: Option<String>,
pub status: String,
pub created_at_ms: i64,
pub provenance_json: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -1397,6 +1421,7 @@ pub struct StoredSshCertificate {
pub certificate: String,
pub certificate_fingerprint: String,
pub imported_at_ms: i64,
pub provenance_json: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -1407,6 +1432,7 @@ pub struct StoredSshRevocation {
pub reason: Option<String>,
pub created_at_ms: i64,
pub published: bool,
pub provenance_json: Option<String>,
}
#[must_use]
@ -1445,6 +1471,7 @@ mod tests {
reason: Some("renewal".to_owned()),
status: "pending".to_owned(),
created_at_ms: 1,
provenance_json: Some(r#"{"test":true}"#.to_owned()),
};
store
.insert_ssh_cert_request(&request)
@ -1463,6 +1490,7 @@ mod tests {
reason: Some("lost key".to_owned()),
created_at_ms: 2,
published: true,
provenance_json: Some(r#"{"test":true}"#.to_owned()),
};
store
.insert_ssh_revocation(&revocation)