use std::sync::Arc;
use std::time::Duration;
use affinidi_did_resolver_cache_sdk::{DIDCacheClient, config::DIDCacheConfigBuilder};
use affinidi_tdk::common::TDKSharedState;
use affinidi_tdk::common::config::TDKConfig;
use affinidi_tdk::messaging::ATM;
use affinidi_tdk::messaging::config::ATMConfig;
use affinidi_tdk::secrets_resolver::{SecretsResolver, ThreadedSecretsResolver, secrets::Secret};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
use crate::auth::AuthState;
use crate::auth::jwt::JwtKeys;
use crate::auth::session::cleanup_expired_sessions;
use crate::config::{AppConfig, AuthConfig};
use crate::error::AppError;
use crate::install::{InstallTokenSigner, InstallTokenStore};
use crate::keys::seed_store::SecretStore;
use crate::messaging;
use crate::routes;
use crate::setup::VtcKeyBundle;
use crate::store::{KeyspaceHandle, Store};
use crate::supervisor::{SupervisorKind, detect_supervisor};
use tokio::sync::{RwLock, watch};
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tracing::{debug, error, info, warn};
use vti_common::audit::{AuditKeyStore, AuditWriter};
use vti_common::auth::passkey::{PasskeyState, build_webauthn};
use webauthn_rs::Webauthn;
use zeroize::Zeroizing;
const DEFAULT_ENROLLMENT_TTL_SECS: u64 = 60 * 60;
#[derive(Clone)]
#[allow(dead_code)]
pub struct AppState {
pub sessions_ks: KeyspaceHandle,
pub acl_ks: KeyspaceHandle,
pub community_ks: KeyspaceHandle,
pub config_ks: KeyspaceHandle,
pub passkey_ks: KeyspaceHandle,
pub install_ks: KeyspaceHandle,
pub members_ks: KeyspaceHandle,
pub join_requests_ks: KeyspaceHandle,
pub policies_ks: KeyspaceHandle,
pub active_policies_ks: KeyspaceHandle,
pub status_lists_ks: KeyspaceHandle,
pub registry_records_ks: KeyspaceHandle,
pub sync_queue_ks: KeyspaceHandle,
pub sync_cursor_ks: KeyspaceHandle,
pub relationships_ks: KeyspaceHandle,
pub relationships_by_did_ks: KeyspaceHandle,
pub endorsement_types_ks: KeyspaceHandle,
pub endorsements_ks: KeyspaceHandle,
pub audit_ks: KeyspaceHandle,
pub audit_key_ks: KeyspaceHandle,
pub registry_client: Option<Arc<dyn crate::registry::TrustRegistryClient>>,
pub registry_health: crate::registry::RegistryHealth,
pub config: Arc<RwLock<AppConfig>>,
pub did_resolver: Option<DIDCacheClient>,
pub secrets_resolver: Option<Arc<ThreadedSecretsResolver>>,
pub jwt_keys: Option<Arc<JwtKeys>>,
pub atm: Option<ATM>,
pub webauthn: Option<Arc<Webauthn>>,
pub public_url: Option<String>,
pub install_signer: Option<Arc<InstallTokenSigner>>,
pub credential_signer: Option<Arc<crate::credentials::LocalSigner>>,
pub install_store: InstallTokenStore,
pub audit_writer: Option<AuditWriter>,
pub shutdown_tx: watch::Sender<bool>,
pub supervisor: Option<SupervisorKind>,
}
impl AuthState for AppState {
fn jwt_keys(&self) -> Option<&Arc<JwtKeys>> {
self.jwt_keys.as_ref()
}
fn sessions_ks(&self) -> &KeyspaceHandle {
&self.sessions_ks
}
}
impl PasskeyState for AppState {
fn webauthn(&self) -> Option<&Arc<Webauthn>> {
self.webauthn.as_ref()
}
fn acl_ks(&self) -> &KeyspaceHandle {
&self.acl_ks
}
fn access_token_expiry(&self) -> u64 {
self.config
.try_read()
.map(|c| c.auth.access_token_expiry)
.unwrap_or(AuthConfig::default().access_token_expiry)
}
fn refresh_token_expiry(&self) -> u64 {
self.config
.try_read()
.map(|c| c.auth.refresh_token_expiry)
.unwrap_or(AuthConfig::default().refresh_token_expiry)
}
fn public_url(&self) -> Option<&str> {
self.public_url.as_deref()
}
fn enrollment_ttl(&self) -> u64 {
DEFAULT_ENROLLMENT_TTL_SECS
}
}
pub async fn run(
config: AppConfig,
store: Store,
secret_store: Box<dyn SecretStore>,
) -> Result<(), AppError> {
let sessions_ks = store.keyspace("sessions")?;
let acl_ks = store.keyspace("acl")?;
let community_ks = store.keyspace("community")?;
let config_ks = store.keyspace("config")?;
let passkey_ks = store.keyspace("passkey")?;
let install_ks = store.keyspace("install")?;
let install_store = InstallTokenStore::new(install_ks.clone());
let members_ks = store.keyspace("members")?;
let join_requests_ks = store.keyspace("join_requests")?;
let policies_ks = store.keyspace("policies")?;
let active_policies_ks = store.keyspace("active_policies")?;
let status_lists_ks = store.keyspace("status_lists")?;
let registry_records_ks = store.keyspace("registry_records")?;
let sync_queue_ks = store.keyspace("sync_queue")?;
let sync_cursor_ks = store.keyspace("sync_cursor")?;
let relationships_ks = store.keyspace("relationships")?;
let relationships_by_did_ks = store.keyspace("relationships_by_did")?;
let endorsement_types_ks = store.keyspace("endorsement_types")?;
let endorsements_ks = store.keyspace("endorsements")?;
let audit_ks = store.keyspace("audit")?;
let audit_key_ks = store.keyspace("audit_key")?;
match crate::policy::default::install_defaults(&policies_ks, &active_policies_ks).await {
Ok(0) => debug!("default policies: every purpose already has an active row"),
Ok(n) => info!("installed {n} default policy(ies) at boot"),
Err(e) => warn!("failed to install default policies: {e}"),
}
if let Some(public_url) = config.public_url.as_deref() {
for purpose in [
affinidi_status_list::StatusPurpose::Revocation,
affinidi_status_list::StatusPurpose::Suspension,
] {
let url = format!("{public_url}/v1/status-lists/{purpose}");
match crate::status_list::ensure_initial(&status_lists_ks, purpose, url).await {
Ok(_) => debug!(?purpose, "status list initialised"),
Err(e) => warn!(?purpose, "failed to initialise status list: {e}"),
}
}
} else {
warn!(
"public_url not configured — status lists deferred; \
VMC issuance + GET /v1/status-lists/* return 503 until set"
);
}
let registry_health = crate::registry::RegistryHealth::new();
let registry_client: Option<Arc<dyn crate::registry::TrustRegistryClient>> = match config
.registry
.url
.as_deref()
{
Some(url) => {
let cfg = crate::registry::upstream::UpstreamConfig {
base_url: url.to_string(),
http_timeout: std::time::Duration::from_secs(config.registry.http_timeout_seconds),
authority_did: config.vtc_did.clone(),
};
match crate::registry::UpstreamRegistryClient::new(cfg) {
Ok(c) => Some(Arc::new(c) as Arc<dyn crate::registry::TrustRegistryClient>),
Err(e) => {
warn!(error = ?e, "failed to construct trust-registry client; running with registry features disabled");
None
}
}
}
None => {
debug!(
"trust-registry url not configured — registry features disabled; \
registry_status reads 'degraded'"
);
None
}
};
let (
did_resolver,
secrets_resolver,
jwt_keys,
atm,
install_signer,
audit_writer,
credential_signer,
) = init_auth(
&config,
&*secret_store,
audit_ks.clone(),
audit_key_ks.clone(),
)
.await;
let public_url = config.public_url.clone();
let webauthn = match &public_url {
Some(url) => match build_webauthn(url) {
Ok(w) => Some(Arc::new(w)),
Err(e) => {
warn!(
"failed to build WebAuthn relying party from public_url '{url}': {e} — passkey routes disabled"
);
None
}
},
None => {
debug!("public_url not configured — WebAuthn / passkey routes disabled");
None
}
};
let addr = format!("{}:{}", config.server.host, config.server.port);
let std_listener = std::net::TcpListener::bind(&addr).map_err(AppError::Io)?;
std_listener.set_nonblocking(true).map_err(AppError::Io)?;
info!("server listening addr={addr}");
let (shutdown_tx, shutdown_rx) = watch::channel(false);
tokio::spawn({
let shutdown_tx = shutdown_tx.clone();
async move {
shutdown_signal().await;
let _ = shutdown_tx.send(true);
}
});
let didcomm_config = config.clone();
let didcomm_secrets = secrets_resolver.clone();
let didcomm_vtc_did = config.vtc_did.clone();
let storage_sessions_ks = sessions_ks.clone();
let storage_auth_config = config.auth.clone();
let has_auth = jwt_keys.is_some();
let state = AppState {
sessions_ks,
acl_ks,
community_ks,
config_ks,
passkey_ks,
install_ks,
members_ks,
join_requests_ks,
policies_ks,
active_policies_ks,
status_lists_ks: status_lists_ks.clone(),
registry_records_ks,
sync_queue_ks,
sync_cursor_ks,
relationships_ks,
relationships_by_did_ks,
endorsement_types_ks,
endorsements_ks,
audit_ks,
audit_key_ks,
registry_client: registry_client.clone(),
registry_health: registry_health.clone(),
config: Arc::new(RwLock::new(config)),
did_resolver,
secrets_resolver,
jwt_keys,
atm,
webauthn,
public_url,
install_signer,
credential_signer: credential_signer.clone(),
install_store,
audit_writer,
shutdown_tx: shutdown_tx.clone(),
supervisor: detect_supervisor(),
};
if let Err(e) = heal_missing_admin_entries(&state).await {
warn!(error = %e, "admin-entry heal scan failed");
}
let boot_cfg = state.config.read().await.clone();
if let Some(vtc_did) = boot_cfg.vtc_did.clone()
&& crate::community::load_profile(&state.community_ks)
.await
.ok()
.flatten()
.is_none()
{
let profile = crate::community::CommunityProfile::new(&vtc_did, "");
match crate::community::store_profile(&state.community_ks, &profile).await {
Ok(()) => info!(
%vtc_did,
"initialised default community profile at boot (heal)",
),
Err(e) => warn!(error = %e, "failed to initialise default community profile at boot"),
}
}
if let (Some(client), Some(vtc_did)) =
(state.registry_client.as_ref(), boot_cfg.vtc_did.clone())
{
match client.health().await {
Ok(()) => {
state
.registry_health
.record_success(state.audit_writer.as_ref(), &vtc_did)
.await;
info!("trust-registry health probe passed at boot");
}
Err(e) => {
state
.registry_health
.record_failure(format!("{e}"), state.audit_writer.as_ref(), &vtc_did)
.await;
warn!(error = %e, "trust-registry health probe failed at boot — running with registry_status=degraded");
}
}
}
let probe_interval_secs = boot_cfg.registry.health_probe_interval_seconds;
let probe_did_opt = boot_cfg.vtc_did.clone();
if registry_client.is_some() && probe_interval_secs > 0 && probe_did_opt.is_some() {
let probe_client = registry_client.clone().expect("checked is_some");
let probe_health = registry_health.clone();
let probe_audit = state.audit_writer.clone();
let probe_did = probe_did_opt.clone().expect("checked is_some");
let mut probe_shutdown = shutdown_rx.clone();
tokio::spawn(async move {
let mut timer = tokio::time::interval(Duration::from_secs(probe_interval_secs));
timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
timer.tick().await;
loop {
tokio::select! {
_ = timer.tick() => {
match probe_client.health().await {
Ok(()) => {
probe_health
.record_success(probe_audit.as_ref(), &probe_did)
.await;
}
Err(e) => {
probe_health
.record_failure(
format!("{e}"),
probe_audit.as_ref(),
&probe_did,
)
.await;
}
}
}
_ = probe_shutdown.changed() => {
debug!("trust-registry health probe task shutting down");
return;
}
}
}
});
}
let syncer_actor_did_opt = boot_cfg.vtc_did.clone();
if let (Some(client), Some(actor_did)) =
(state.registry_client.clone(), syncer_actor_did_opt.clone())
{
let rtbf_batch_window_hours = boot_cfg.registry.rtbf_batch_window_hours;
let syncer = crate::registry::MembershipSyncer::new(
state.audit_ks.clone(),
state.sync_queue_ks.clone(),
state.sync_cursor_ks.clone(),
state.registry_records_ks.clone(),
state.policies_ks.clone(),
state.active_policies_ks.clone(),
client,
state.registry_health.clone(),
state.audit_writer.clone(),
actor_did,
)
.with_rtbf_batch_window_hours(rtbf_batch_window_hours);
let syncer_shutdown = shutdown_rx.clone();
tokio::spawn(async move {
syncer.run(syncer_shutdown).await;
});
}
if let (Some(pending), Some(writer)) = (
state
.install_store
.take_pending_emergency()
.await
.ok()
.flatten(),
state.audit_writer.as_ref(),
) {
warn!(
operator_hostname = %pending.operator_hostname,
invoked_at = %pending.invoked_at,
"EMERGENCY BOOTSTRAP was invoked since the daemon last ran — auditing now",
);
if let Err(e) = writer
.write(
"did:key:vtc-emergency",
None,
vti_common::audit::AuditEvent::EmergencyBootstrapInvoked(
vti_common::audit::EmergencyBootstrapData {
operator_hostname: pending.operator_hostname,
invoked_at: pending.invoked_at,
},
),
)
.await
{
error!(error = %e, "failed to emit EmergencyBootstrapInvoked envelope");
}
}
let rest_cors = boot_cfg.cors.clone();
let rest_routing = boot_cfg.routing.clone();
#[cfg(feature = "admin-ui")]
if let Some(writer) = state.audit_writer.as_ref() {
let mode = boot_cfg.admin_ui.mode.clone();
let info = crate::admin_ui::AdminUiInfo::from_embedded(&mode);
let _ = writer
.write(
"daemon",
None,
vti_common::audit::AuditEvent::AdminUiServed(
vti_common::audit::AdminUiServedData {
index_sha256: (*info.index_sha256).clone(),
file_count: info.file_count,
mode: (*info.mode).clone(),
},
),
)
.await;
}
let mut rest_shutdown_rx = shutdown_rx.clone();
let rest_state = state.clone();
let rest_handle = std::thread::Builder::new()
.name("vtc-rest".into())
.spawn(move || {
run_rest_thread(
std_listener,
rest_state,
rest_cors,
rest_routing,
&mut rest_shutdown_rx,
)
})
.map_err(|e| AppError::Internal(format!("failed to spawn REST thread: {e}")))?;
let mut didcomm_shutdown_rx = shutdown_rx.clone();
let didcomm_state = state.clone();
let didcomm_handle = std::thread::Builder::new()
.name("vtc-didcomm".into())
.spawn(move || {
run_didcomm_thread(
didcomm_config,
didcomm_secrets,
didcomm_vtc_did,
didcomm_state,
&mut didcomm_shutdown_rx,
)
})
.map_err(|e| AppError::Internal(format!("failed to spawn DIDComm thread: {e}")))?;
let mut storage_shutdown_rx = shutdown_rx.clone();
let storage_handle = std::thread::Builder::new()
.name("vtc-storage".into())
.spawn(move || {
run_storage_thread(
store,
storage_sessions_ks,
storage_auth_config,
has_auth,
&mut storage_shutdown_rx,
)
})
.map_err(|e| AppError::Internal(format!("failed to spawn storage thread: {e}")))?;
let (rest_result, didcomm_result) = tokio::join!(
tokio::task::spawn_blocking(move || rest_handle.join()),
tokio::task::spawn_blocking(move || didcomm_handle.join()),
);
let mut any_panic = false;
match rest_result {
Ok(Ok(())) => info!("REST thread stopped"),
Ok(Err(_panic)) => {
error!("REST thread panicked");
any_panic = true;
}
Err(e) => {
error!("failed to join REST thread: {e}");
any_panic = true;
}
}
match didcomm_result {
Ok(Ok(())) => info!("DIDComm thread stopped"),
Ok(Err(_panic)) => {
error!("DIDComm thread panicked");
any_panic = true;
}
Err(e) => {
error!("failed to join DIDComm thread: {e}");
any_panic = true;
}
}
if any_panic {
let _ = shutdown_tx.send(true);
}
match storage_handle.join() {
Ok(()) => info!("storage thread stopped"),
Err(_panic) => {
error!("storage thread panicked");
any_panic = true;
}
}
if any_panic {
return Err(AppError::Internal("one or more threads panicked".into()));
}
info!("server shut down");
Ok(())
}
async fn heal_missing_admin_entries(state: &AppState) -> Result<(), AppError> {
use chrono::Utc;
use vti_common::acl::list_acl_entries;
use vti_common::auth::passkey::store::get_passkey_user_by_did;
use crate::acl::admin::{AdminEntry, RegisteredPasskey, get_admin_entry, store_admin_entry};
let admins = list_acl_entries(&state.acl_ks).await?;
let mut healed = 0usize;
for acl_entry in admins {
if acl_entry.role != vti_common::acl::Role::Admin {
continue;
}
if get_admin_entry(&state.passkey_ks, &acl_entry.did)
.await?
.is_some()
{
continue;
}
let Some(pk_user) = get_passkey_user_by_did(&state.passkey_ks, &acl_entry.did).await?
else {
continue;
};
let now = Utc::now();
let passkeys: Vec<RegisteredPasskey> = pk_user
.credentials
.iter()
.map(|cred| {
let cred_id_hex = hex::encode(<_ as AsRef<[u8]>>::as_ref(cred.cred_id()));
RegisteredPasskey {
credential_id: cred_id_hex,
label: "install".into(),
transports: Vec::new(),
registered_at: now,
last_used_at: None,
}
})
.collect();
if passkeys.is_empty() {
continue;
}
let entry = AdminEntry {
did: acl_entry.did.clone(),
passkeys,
extensions: serde_json::Value::Null,
created_at: now,
};
store_admin_entry(&state.passkey_ks, &entry).await?;
info!(did = %acl_entry.did, "synthesised missing AdminEntry from PasskeyUser (heal)");
healed += 1;
}
if healed > 0 {
info!(count = healed, "admin-entry heal scan completed");
}
Ok(())
}
fn run_storage_thread(
store: Store,
sessions_ks: KeyspaceHandle,
auth_config: AuthConfig,
has_auth: bool,
shutdown_rx: &mut watch::Receiver<bool>,
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build storage runtime");
rt.block_on(async {
info!("storage thread started");
if has_auth {
let interval = Duration::from_secs(auth_config.session_cleanup_interval);
let mut timer = tokio::time::interval(interval);
timer.tick().await;
loop {
tokio::select! {
_ = timer.tick() => {
if let Err(e) = cleanup_expired_sessions(&sessions_ks, auth_config.challenge_ttl).await {
warn!("session cleanup error: {e}");
}
}
_ = shutdown_rx.changed() => {
info!("storage thread shutting down");
break;
}
}
}
} else {
let _ = shutdown_rx.changed().await;
info!("storage thread shutting down");
}
if let Err(e) = store.persist().await {
error!("failed to persist store on shutdown: {e}");
} else {
info!("store persisted");
}
});
}
fn build_cors_layer(cors: &crate::config::CorsConfig) -> CorsLayer {
use axum::http::Method;
use axum::http::header::{
ACCESS_CONTROL_ALLOW_HEADERS, AUTHORIZATION, CONTENT_TYPE, HeaderName, HeaderValue,
};
if cors.allowed_origins.is_empty() {
return CorsLayer::new();
}
let allowed_origins: Vec<HeaderValue> = cors
.allowed_origins
.iter()
.filter_map(|o| o.parse::<HeaderValue>().ok())
.collect();
let allowed_methods = vec![
Method::GET,
Method::POST,
Method::PUT,
Method::PATCH,
Method::DELETE,
Method::OPTIONS,
];
let allowed_headers: Vec<HeaderName> = vec![
AUTHORIZATION,
CONTENT_TYPE,
ACCESS_CONTROL_ALLOW_HEADERS,
HeaderName::from_static("trust-task"),
HeaderName::from_static("idempotency-key"),
];
CorsLayer::new()
.allow_origin(allowed_origins)
.allow_methods(allowed_methods)
.allow_headers(allowed_headers)
.allow_credentials(true)
}
fn run_rest_thread(
std_listener: std::net::TcpListener,
state: AppState,
cors: crate::config::CorsConfig,
routing: crate::config::RoutingConfig,
shutdown_rx: &mut watch::Receiver<bool>,
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build REST runtime");
rt.block_on(async {
info!("REST thread started");
let listener = tokio::net::TcpListener::from_std(std_listener)
.expect("failed to convert std TcpListener to tokio TcpListener");
let cors_layer = build_cors_layer(&cors);
let host_map = crate::routing::host_dispatch::HostMap::from_routing(&routing);
let host_layer =
axum::middleware::from_fn_with_state(host_map, crate::routing::host_dispatch::enforce);
let csrf_layer = axum::middleware::from_fn(crate::routing::csrf::enforce);
#[cfg(feature = "website")]
let website_state = build_website_state(&state.config).await;
let trust_xff = state.config.read().await.server.trust_xff;
#[cfg(feature = "website")]
let app = routes::router_with_xff(&routing, website_state, trust_xff)
.with_state(state)
.layer(csrf_layer)
.layer(host_layer)
.layer(cors_layer)
.layer(TraceLayer::new_for_http());
#[cfg(not(feature = "website"))]
let app = routes::router_with_xff(&routing, trust_xff)
.with_state(state)
.layer(csrf_layer)
.layer(host_layer)
.layer(cors_layer)
.layer(TraceLayer::new_for_http());
let shutdown_rx = shutdown_rx.clone();
axum::serve(
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(async move {
let mut rx = shutdown_rx;
let _ = rx.changed().await;
})
.await
.expect("axum serve failed");
info!("REST thread shutting down");
});
}
fn run_didcomm_thread(
config: AppConfig,
secrets_resolver: Option<Arc<ThreadedSecretsResolver>>,
vtc_did: Option<String>,
state: AppState,
shutdown_rx: &mut watch::Receiver<bool>,
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build DIDComm runtime");
rt.block_on(async {
info!("DIDComm thread started");
let (sr, did) = match (&secrets_resolver, &vtc_did) {
(Some(sr), Some(did)) => (sr, did.as_str()),
_ => {
info!("DIDComm not configured — thread idle");
let _ = shutdown_rx.changed().await;
info!("DIDComm thread shutting down (idle)");
return;
}
};
messaging::run_didcomm_service(&config, sr, did, state, shutdown_rx).await;
info!("DIDComm thread shutting down");
});
}
async fn init_auth(
config: &AppConfig,
secret_store: &dyn SecretStore,
audit_ks: KeyspaceHandle,
audit_key_ks: KeyspaceHandle,
) -> (
Option<DIDCacheClient>,
Option<Arc<ThreadedSecretsResolver>>,
Option<Arc<JwtKeys>>,
Option<ATM>,
Option<Arc<InstallTokenSigner>>,
Option<AuditWriter>,
Option<Arc<crate::credentials::LocalSigner>>,
) {
let vtc_did = match &config.vtc_did {
Some(did) => did.clone(),
None => {
warn!("vtc_did not configured — auth endpoints will not work (run setup first)");
return (None, None, None, None, None, None, None);
}
};
let stored = match secret_store.get().await {
Ok(Some(s)) => s,
Ok(None) => {
warn!("no key material found — auth endpoints will not work (run setup first)");
return (None, None, None, None, None, None, None);
}
Err(e) => {
warn!("failed to load key material: {e} — auth endpoints will not work");
return (None, None, None, None, None, None, None);
}
};
let (ed25519_bytes, x25519_bytes) = match decode_secret_store_value(&vtc_did, &stored) {
Ok(pair) => pair,
Err(msg) => {
warn!("{msg}");
return (None, None, None, None, None, None, None);
}
};
let credential_signer = Some(Arc::new(
crate::credentials::LocalSigner::from_ed25519_seed(vtc_did.clone(), &ed25519_bytes),
));
let install_signer = match InstallTokenSigner::from_master_seed(&*ed25519_bytes) {
Ok(s) => Some(Arc::new(s)),
Err(e) => {
warn!("failed to derive install token signer: {e} — install routes disabled");
None
}
};
let audit_writer = {
let key_store = AuditKeyStore::new(audit_key_ks);
match key_store.ensure_initial(&*ed25519_bytes).await {
Ok(_) => Some(AuditWriter::new(audit_ks, key_store)),
Err(e) => {
warn!("failed to derive initial audit key: {e} — audit-emitting routes disabled");
None
}
}
};
let did_resolver = match DIDCacheClient::new(DIDCacheConfigBuilder::default().build()).await {
Ok(r) => r,
Err(e) => {
warn!("failed to create DID resolver: {e} — auth endpoints will not work");
return (
None,
None,
None,
None,
install_signer,
audit_writer,
credential_signer,
);
}
};
let (secrets_resolver, _handle) = ThreadedSecretsResolver::new(None).await;
let mut signing_secret = Secret::generate_ed25519(None, Some(&*ed25519_bytes));
signing_secret.id = format!("{vtc_did}#key-0");
secrets_resolver.insert(signing_secret).await;
match Secret::generate_x25519(None, Some(&*x25519_bytes)) {
Ok(mut ka_secret) => {
ka_secret.id = format!("{vtc_did}#key-1");
secrets_resolver.insert(ka_secret).await;
}
Err(e) => warn!("failed to create VTC key-agreement secret: {e}"),
}
let jwt_keys = match &config.auth.jwt_signing_key {
Some(b64) => match decode_jwt_key(b64) {
Ok(k) => k,
Err(e) => {
warn!("failed to load JWT signing key: {e} — auth endpoints will not work");
return (
Some(did_resolver),
Some(Arc::new(secrets_resolver)),
None,
None,
install_signer,
audit_writer,
credential_signer,
);
}
},
None => {
warn!(
"auth.jwt_signing_key not configured — auth endpoints will not work (run setup first)"
);
return (
Some(did_resolver),
Some(Arc::new(secrets_resolver)),
None,
None,
install_signer,
audit_writer,
credential_signer,
);
}
};
let secrets_resolver = Arc::new(secrets_resolver);
let atm = {
let tdk_config = TDKConfig::builder()
.with_did_resolver(did_resolver.clone())
.with_secrets_resolver((*secrets_resolver).clone())
.with_load_environment(false)
.build();
match tdk_config {
Ok(cfg) => match TDKSharedState::new(cfg).await {
Ok(tdk) => match ATMConfig::builder().build() {
Ok(atm_cfg) => match ATM::new(atm_cfg, Arc::new(tdk)).await {
Ok(a) => Some(a),
Err(e) => {
warn!("failed to create ATM for auth unpack: {e}");
None
}
},
Err(e) => {
warn!("failed to build ATMConfig: {e}");
None
}
},
Err(e) => {
warn!("failed to create TDK shared state: {e}");
None
}
},
Err(e) => {
warn!("failed to build TDK config: {e}");
None
}
}
};
info!("auth initialized for DID {vtc_did}");
(
Some(did_resolver),
Some(secrets_resolver),
Some(Arc::new(jwt_keys)),
atm,
install_signer,
audit_writer,
credential_signer,
)
}
fn decode_jwt_key(b64: &str) -> Result<JwtKeys, AppError> {
let bytes = BASE64
.decode(b64)
.map_err(|e| AppError::Config(format!("invalid jwt_signing_key base64: {e}")))?;
let key_bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| AppError::Config("jwt_signing_key must be exactly 32 bytes".into()))?;
let keys = JwtKeys::from_ed25519_bytes(&key_bytes, "VTC")?;
debug!("JWT signing key decoded successfully");
Ok(keys)
}
#[cfg(feature = "website")]
async fn build_website_state(
config: &Arc<RwLock<crate::config::AppConfig>>,
) -> Option<crate::website::WebsiteState> {
let cfg = config.read().await;
let root_dir = cfg.website.root_dir.as_ref()?;
let root = match crate::website::WebsiteRoot::new(root_dir, &cfg.website.deploy_mode) {
Ok(r) => r,
Err(e) => {
warn!("website.deploy_mode invalid ({e}); falling back to 503 placeholder");
return None;
}
};
let cache = crate::website::cache::WebsiteCache::new(cfg.website.live_cache_ttl_seconds);
info!(
root = %root_dir.display(),
mode = %cfg.website.deploy_mode,
"public-website handler enabled",
);
Some(crate::website::WebsiteState {
root,
cache,
executable_blocklist: cfg.website.executable_blocklist.clone(),
cache_control: cfg.website.cache_control.clone(),
csp_override_file: cfg.website.csp_override_file.clone(),
})
}
async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => info!("received SIGINT"),
() = terminate => info!("received SIGTERM"),
}
}
fn decode_secret_store_value(
vtc_did: &str,
stored: &[u8],
) -> Result<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>), String> {
if stored.len() == 64 {
let mut ed = Zeroizing::new([0u8; 32]);
let mut x = Zeroizing::new([0u8; 32]);
ed.copy_from_slice(&stored[..32]);
x.copy_from_slice(&stored[32..]);
return Ok((ed, x));
}
let bundle = VtcKeyBundle::from_secret_store_bytes(stored)
.map_err(|e| format!("secret store payload not a VtcKeyBundle: {e}"))?;
if bundle.integration_did != vtc_did {
return Err(format!(
"VtcKeyBundle DID '{}' does not match config.vtc_did '{}' — refusing to init auth",
bundle.integration_did, vtc_did
));
}
let ed = bundle
.ed25519_private_bytes()
.map_err(|e| format!("bundle Ed25519 decode: {e}"))?;
let x = bundle
.x25519_private_bytes()
.map_err(|e| format!("bundle X25519 decode: {e}"))?;
Ok((ed, x))
}