use super::*;
use crate::runtime::authn::signing_keys as signing_key_domain;
use crate::runtime::core::DataBrokerRuntime;
use crate::runtime::native_catalog::native_model;
use crate::runtime::security::UDB_RS256_KID;
use sqlx::Row;
use super::key_provider::active_key_provider;
pub(super) struct RegistryKey {
pub key_id: String,
pub algorithm: String,
pub public_material: String,
}
fn signing_key_model() -> crate::runtime::native_catalog::NativeModel {
native_model(
"udb.core.authn.entity.v1.SigningKey",
&[
"key_id",
"tenant_id",
"algorithm",
"public_material",
"encrypted_private_material",
"state",
"created_by",
"retired_at",
"retired_by",
"rotation_reason",
],
)
}
fn signing_key_internal_status(operation: impl Into<String>, message: impl Into<String>) -> Status {
crate::runtime::executor_utils::internal_status("authn", operation, message)
}
impl AuthnServiceImpl {
pub(crate) async fn seed_signing_key_registry(&self, runtime: &DataBrokerRuntime) {
self.seed_signing_key_registry_inner(runtime).await;
self.refresh_signing_key_cache(runtime).await;
}
async fn seed_signing_key_registry_inner(&self, runtime: &DataBrokerRuntime) {
let Some(pool) = self.pg_pool.as_ref() else {
return;
};
let Some(public) = self.env_public_pem() else {
return;
};
let m = signing_key_model();
let existing: Result<i64, _> = sqlx::query_scalar(&format!(
"SELECT COUNT(*)::bigint FROM {rel}",
rel = m.relation
))
.fetch_one(pool)
.await;
if !matches!(existing, Ok(0)) {
return;
}
let private_plain = match active_key_provider(&self.security).signing_key_pem() {
Ok(pem) => pem,
Err(err) => {
tracing::warn!(error = %err, "skipping signing-key seed: signing-key provider unavailable");
return;
}
};
let private = match runtime.encrypt_secret_at_rest(&private_plain) {
Ok(sealed) => sealed,
Err(err) => {
tracing::warn!(error = %err, "skipping signing-key seed: cannot seal private material at rest");
return;
}
};
if let Ok(authn_runtime) = self.authn_runtime() {
let context = self.authn_context("", "");
let record = authn_record([
("key_id", LogicalValue::String(UDB_RS256_KID.to_string())),
(
"algorithm",
LogicalValue::String(signing_key_domain::DEFAULT_SIGNING_ALGORITHM.to_string()),
),
("public_material", LogicalValue::String(public.clone())),
(
"encrypted_private_material",
LogicalValue::String(private.clone()),
),
(
"state",
LogicalValue::String(signing_key_domain::STATE_ACTIVE.to_string()),
),
("created_by", LogicalValue::String("env-seed".to_string())),
]);
if let Err(err) = authn_runtime
.native_entity_write_for_service(
"authn",
&context,
"udb.core.authn.entity.v1.SigningKey",
record,
crate::ir::ConflictStrategy::Ignore,
)
.await
{
tracing::warn!(error = %err, "failed to seed signing-key registry from env key");
}
return;
}
let sql = format!(
"INSERT INTO {rel} ({kid}, {alg}, {pubm}, {privm}, {state}, {by}) \
VALUES ($1, $2, $3, $4, $5, 'env-seed') ON CONFLICT ({kid}) DO NOTHING",
rel = m.relation,
kid = m.q("key_id"),
alg = m.q("algorithm"),
pubm = m.q("public_material"),
privm = m.q("encrypted_private_material"),
state = m.q("state"),
by = m.q("created_by"),
);
if let Err(err) = sqlx::query(&sql)
.bind(UDB_RS256_KID)
.bind(signing_key_domain::DEFAULT_SIGNING_ALGORITHM)
.bind(&public)
.bind(&private)
.bind(signing_key_domain::STATE_ACTIVE)
.execute(pool)
.await
{
tracing::warn!(error = %err, "failed to seed signing-key registry from env key");
}
}
fn env_public_pem(&self) -> Option<String> {
self.security.jwt_public_pem()
}
pub(super) async fn jwks_registry_keys(&self) -> Result<Vec<RegistryKey>, Status> {
let Some(pool) = self.pg_pool.as_ref() else {
return Ok(Vec::new());
};
let m = signing_key_model();
let sql = format!(
"SELECT {kid}::TEXT AS key_id, {alg}::TEXT AS algorithm, \
{pubm}::TEXT AS public_material, {state}::TEXT AS state \
FROM {rel} WHERE {state} IN ($1, $2) ORDER BY {state}, {kid}",
kid = m.q("key_id"),
alg = m.q("algorithm"),
pubm = m.q("public_material"),
state = m.q("state"),
rel = m.relation,
);
let rows = match sqlx::query(&sql)
.bind(signing_key_domain::STATE_ACTIVE)
.bind(signing_key_domain::STATE_VERIFYING)
.fetch_all(pool)
.await
{
Ok(rows) => rows,
Err(err) => {
return Err(signing_key_internal_status(
"jwks_registry_read",
format!("signing-key registry read failed: {err}"),
));
}
};
Ok(rows
.into_iter()
.filter_map(|row| {
let state: String = row.try_get("state").ok()?;
if !signing_key_domain::jwks_publishable_state(&state) {
return None;
}
Some(RegistryKey {
key_id: row.try_get("key_id").ok()?,
algorithm: row.try_get("algorithm").ok()?,
public_material: row.try_get("public_material").ok()?,
})
})
.collect())
}
pub(super) async fn load_active_signing_private_key(
&self,
runtime: &DataBrokerRuntime,
) -> Result<Option<(String, String)>, Status> {
let Some(pool) = self.pg_pool.as_ref() else {
return Ok(None);
};
let m = signing_key_model();
let sql = format!(
"SELECT {kid}::TEXT AS key_id, \
{privm}::TEXT AS encrypted_private_material \
FROM {rel} WHERE {state} = $1 ORDER BY {kid} LIMIT 1",
kid = m.q("key_id"),
privm = m.q("encrypted_private_material"),
rel = m.relation,
state = m.q("state"),
);
let row = sqlx::query(&sql)
.bind(signing_key_domain::STATE_ACTIVE)
.fetch_optional(pool)
.await
.map_err(|err| {
signing_key_internal_status(
"active_signing_key_read",
format!("active signing-key read failed: {err}"),
)
})?;
let Some(row) = row else {
return Ok(None);
};
let key_id: String = row.try_get("key_id").unwrap_or_default();
let stored: String = row
.try_get("encrypted_private_material")
.unwrap_or_default();
if key_id.is_empty() || stored.is_empty() {
return Ok(None);
}
let pem = runtime.decrypt_secret_at_rest(&stored).map_err(|err| {
signing_key_internal_status(
"active_signing_key_decrypt",
format!("active signing-key decrypt failed: {err}"),
)
})?;
Ok(Some((key_id, pem)))
}
pub(crate) async fn refresh_signing_key_cache(&self, runtime: &DataBrokerRuntime) {
if self.pg_pool.is_none() {
return;
}
let active = match self.load_active_signing_private_key(runtime).await {
Ok(active) => active,
Err(err) => {
tracing::warn!(error = %err, "signing-key cache refresh: active key load failed; keeping previous snapshot");
return;
}
};
let public_by_kid = match self.jwks_registry_keys().await {
Ok(keys) => keys
.into_iter()
.map(|k| (k.key_id, k.public_material))
.collect(),
Err(err) => {
tracing::warn!(error = %err, "signing-key cache refresh: public-key load failed; keeping previous snapshot");
return;
}
};
crate::runtime::security::install_signing_key_registry_snapshot(
crate::runtime::security::SigningKeyRegistrySnapshot {
active,
public_by_kid,
},
);
}
pub(super) async fn compromise_signing_key_on<'c, E>(
&self,
executor: E,
key_id: &str,
by: &str,
) -> Result<u64, Status>
where
E: sqlx::Executor<'c, Database = sqlx::Postgres>,
{
let m = signing_key_model();
let sql = format!(
"UPDATE {rel} SET {state} = $3, {retired_at} = NOW(), {retired_by} = $2, \
{reason} = $4 \
WHERE {kid} = $1 AND {state} <> $3",
rel = m.relation,
state = m.q("state"),
retired_at = m.q("retired_at"),
retired_by = m.q("retired_by"),
reason = m.q("rotation_reason"),
kid = m.q("key_id"),
);
let res = sqlx::query(&sql)
.bind(key_id)
.bind(by)
.bind(signing_key_domain::STATE_COMPROMISED)
.bind(signing_key_domain::EMERGENCY_COMPROMISE_REASON)
.execute(executor)
.await
.map_err(|err| {
signing_key_internal_status(
"compromise_signing_key",
format!("compromise signing key failed: {err}"),
)
})?;
Ok(res.rows_affected())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::{ErrorDetail, ErrorKind};
use crate::runtime::executor_utils::ERROR_DETAIL_METADATA_KEY;
fn decode_detail(status: &Status) -> ErrorDetail {
let raw = status
.metadata()
.get_bin(ERROR_DETAIL_METADATA_KEY)
.expect("typed detail trailer is present");
crate::runtime::executor_utils::decode_error_detail_from_raw(&raw)
}
fn assert_internal_detail(status: &Status, operation: &str, message: &str) {
assert_eq!(status.code(), tonic::Code::Internal);
assert_eq!(status.message(), message);
let detail = decode_detail(status);
assert_eq!(detail.kind, ErrorKind::Internal as i32);
assert_eq!(detail.backend, "authn");
assert_eq!(detail.operation, operation);
assert!(!detail.retryable);
assert_eq!(detail.retry_after_ms, 0);
assert!(detail.field_violations.is_empty());
}
#[test]
fn signing_key_internal_status_carries_typed_detail() {
let status = signing_key_internal_status(
"active_signing_key_decrypt",
"active signing-key decrypt failed: missing key",
);
assert_internal_detail(
&status,
"active_signing_key_decrypt",
"active signing-key decrypt failed: missing key",
);
}
}