mod acl;
#[cfg(feature = "tee")]
mod attestation;
mod audit;
mod auth;
mod auth_portal;
mod backup;
mod backup_blob;
mod bootstrap;
mod cache;
mod capabilities;
mod config;
mod contexts;
mod did_templates;
#[cfg(feature = "webvh")]
mod did_webvh;
mod health;
pub mod keys;
#[cfg(feature = "webvh")]
mod passkey_vms;
#[cfg(feature = "webvh")]
mod protocol;
mod step_up;
mod vta;
use std::sync::Arc;
use std::time::Duration;
use axum::Router;
use axum::extract::DefaultBodyLimit;
use axum::http::{HeaderName, HeaderValue, Method};
use axum::routing::{get, post};
use tower_governor::GovernorLayer;
use tower_governor::governor::GovernorConfigBuilder;
use tower_http::cors::{AllowOrigin, CorsLayer};
use tower_http::timeout::TimeoutLayer;
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_axum::routes;
use crate::server::AppState;
#[derive(OpenApi)]
#[openapi(
info(
title = "Verifiable Trust Agent (VTA) API",
description = "Key-management, DID-webvh, provisioning, and runtime \
service-management REST surface of a Verifiable Trust Agent.",
version = env!("CARGO_PKG_VERSION"),
),
modifiers(&SecurityAddon),
)]
pub struct ApiDoc;
struct SecurityAddon;
impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
let components = openapi.components.get_or_insert_with(Default::default);
components.add_security_scheme(
"bearer_jwt",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.build(),
),
);
}
}
async fn serve_openapi(api: utoipa::openapi::OpenApi) -> axum::Json<utoipa::openapi::OpenApi> {
axum::Json(api)
}
const MAX_BODY_SIZE: usize = 1024 * 1024;
const UNAUTH_BODY_SIZE: usize = 64 * 1024;
pub(super) const BACKUP_BLOB_BODY_SIZE: usize = 100 * 1024 * 1024;
const UNAUTH_RPS: u64 = 5;
const UNAUTH_BURST: u32 = 10;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
fn apply_unauth_governor(
router: OpenApiRouter<AppState>,
trust_xff: bool,
) -> OpenApiRouter<AppState> {
if trust_xff {
let cfg = Arc::new(
GovernorConfigBuilder::default()
.per_second(UNAUTH_RPS)
.burst_size(UNAUTH_BURST)
.key_extractor(tower_governor::key_extractor::SmartIpKeyExtractor)
.finish()
.expect("governor config values are static and non-zero"),
);
router.layer(GovernorLayer::new(cfg))
} else {
let cfg = Arc::new(
GovernorConfigBuilder::default()
.per_second(UNAUTH_RPS)
.burst_size(UNAUTH_BURST)
.key_extractor(tower_governor::key_extractor::PeerIpKeyExtractor)
.finish()
.expect("governor config values are static and non-zero"),
);
router.layer(GovernorLayer::new(cfg))
}
}
pub fn health_router() -> Router<AppState> {
Router::new().route("/health", get(health::health))
}
pub fn health_router_with_cors(allowed_origins: &[String]) -> Router<AppState> {
let router = health_router();
match build_cors_layer(allowed_origins) {
Some(cors) => router.layer(cors),
None => router,
}
}
fn build_cors_layer(allowed_origins: &[String]) -> Option<CorsLayer> {
if allowed_origins.is_empty() {
return None;
}
let parsed: Vec<HeaderValue> = allowed_origins
.iter()
.filter(|o| !o.is_empty() && *o != "*")
.filter_map(|o| HeaderValue::from_str(o).ok())
.collect();
if parsed.is_empty() {
return None;
}
Some(
CorsLayer::new()
.allow_origin(AllowOrigin::list(parsed))
.allow_methods([Method::GET, Method::POST, Method::DELETE, Method::PATCH])
.allow_headers([
HeaderName::from_static("content-type"),
HeaderName::from_static("authorization"),
HeaderName::from_static("x-backup-token"),
])
.max_age(std::time::Duration::from_secs(60)),
)
}
pub fn router() -> Router<AppState> {
router_with_cors(&[], false)
}
fn build_api_router(trust_xff: bool) -> OpenApiRouter<AppState> {
let unauth = OpenApiRouter::new()
.routes(routes!(bootstrap::request))
.routes(routes!(auth::passkey_login_start))
.routes(routes!(auth::passkey_login_finish))
.routes(routes!(auth::challenge))
.routes(routes!(auth::authenticate))
.routes(routes!(auth::refresh));
#[cfg(feature = "tee")]
let unauth = unauth
.routes(routes!(attestation::status))
.routes(routes!(
attestation::cached_report,
attestation::generate_report
))
.routes(routes!(attestation::did_log));
#[cfg(feature = "webvh")]
let unauth = unauth
.routes(routes!(did_webvh::get_did_log_public_handler));
let unauth = unauth.layer(DefaultBodyLimit::max(UNAUTH_BODY_SIZE));
let unauth = apply_unauth_governor(unauth, trust_xff);
let auth_portal_router =
OpenApiRouter::new().route("/auth/portal", get(auth_portal::portal_handler));
#[cfg(feature = "webvh")]
let auth_provision = OpenApiRouter::new().routes(routes!(bootstrap::provision_integration));
let router = OpenApiRouter::with_openapi(ApiDoc::openapi()).merge(unauth);
#[cfg(feature = "webvh")]
let router = router.merge(auth_provision);
let router = router.merge(auth_portal_router);
let router = router
.routes(routes!(auth::session_list, auth::revoke_sessions_by_did))
.routes(routes!(auth::revoke_session))
.route(
"/api/trust-tasks",
post(crate::trust_tasks::dispatch_trust_task),
)
.routes(routes!(config::get_config, config::update_config))
.routes(routes!(keys::list_keys, keys::create_key))
.routes(routes!(
keys::get_key,
keys::invalidate_key,
keys::rename_key
))
.routes(routes!(keys::get_key_secret))
.routes(routes!(keys::sign_with_key))
.routes(routes!(keys::get_wrapping_key))
.routes(routes!(keys::import_key))
.routes(routes!(keys::list_seeds))
.routes(routes!(keys::rotate_seed))
.routes(routes!(
contexts::list_contexts_handler,
contexts::create_context_handler
))
.routes(routes!(
contexts::get_context_handler,
contexts::update_context_handler,
contexts::delete_context_handler
))
.routes(routes!(contexts::update_context_did_handler))
.routes(routes!(contexts::preview_delete_context_handler))
.routes(routes!(
did_templates::list_handler,
did_templates::create_handler
))
.routes(routes!(
did_templates::get_handler,
did_templates::update_handler,
did_templates::delete_handler
))
.routes(routes!(did_templates::render_handler))
.routes(routes!(
did_templates::list_context_handler,
did_templates::create_context_handler
))
.routes(routes!(
did_templates::get_context_handler,
did_templates::update_context_handler,
did_templates::delete_context_handler
))
.routes(routes!(did_templates::render_context_handler))
.routes(routes!(
step_up::get_step_up_policy,
step_up::put_step_up_policy
))
.routes(routes!(acl::list_acl, acl::create_acl))
.routes(routes!(acl::swap_acl))
.routes(routes!(acl::get_acl, acl::update_acl, acl::delete_acl))
.routes(routes!(audit::list_audit_logs))
.routes(routes!(audit::get_retention, audit::update_retention))
.routes(routes!(
cache::get_cached,
cache::put_cached,
cache::delete_cached
));
#[cfg(feature = "tee")]
let router = router.routes(routes!(
attestation::mnemonic_status,
attestation::mnemonic_export
));
#[cfg(feature = "webvh")]
let router = router
.routes(routes!(protocol::enable_didcomm_handler))
.routes(routes!(protocol::get_didcomm_status_handler))
.routes(routes!(protocol::disable_didcomm_handler))
.routes(routes!(protocol::enable_rest_handler))
.routes(routes!(protocol::update_rest_handler))
.routes(routes!(protocol::disable_rest_handler))
.routes(routes!(protocol::rollback_rest_handler))
.routes(routes!(protocol::enable_webauthn_handler))
.routes(routes!(protocol::update_webauthn_handler))
.routes(routes!(protocol::disable_webauthn_handler))
.routes(routes!(protocol::rollback_webauthn_handler))
.routes(routes!(protocol::list_services_handler))
.routes(routes!(
protocol::list_drain_handler,
protocol::drain_cancel_handler
))
.routes(routes!(protocol::update_didcomm_handler))
.routes(routes!(protocol::rollback_didcomm_handler))
.route(
"/mediators/drain/cancel",
post(protocol::drain_cancel_handler),
)
.routes(routes!(protocol::mediator_report_handler));
#[cfg(feature = "webvh")]
let router = router
.routes(routes!(
did_webvh::list_servers_handler,
did_webvh::add_server_handler
))
.routes(routes!(
did_webvh::update_server_handler,
did_webvh::remove_server_handler
))
.routes(routes!(did_webvh::list_server_domains_handler))
.routes(routes!(
did_webvh::list_dids_handler,
did_webvh::create_did_handler
))
.routes(routes!(
did_webvh::get_did_handler,
did_webvh::delete_did_handler
))
.routes(routes!(did_webvh::get_did_log_handler))
.routes(routes!(did_webvh::register_did_with_server_handler))
.routes(routes!(did_webvh::update_did_handler))
.routes(routes!(did_webvh::rotate_did_keys_handler))
.routes(routes!(passkey_vms::enroll_challenge_handler))
.routes(routes!(
passkey_vms::enroll_submit_handler,
passkey_vms::list_passkeys_handler
))
.routes(routes!(passkey_vms::revoke_passkey_handler));
let router = router
.routes(routes!(vta::restart))
.routes(routes!(vta::metrics))
.routes(routes!(backup::export))
.routes(routes!(backup::import));
let backup_blob_router = OpenApiRouter::new()
.routes(routes!(backup_blob::get_blob, backup_blob::post_blob))
.layer(DefaultBodyLimit::max(BACKUP_BLOB_BODY_SIZE));
let backup_blob_router = apply_unauth_governor(backup_blob_router, trust_xff);
let router = router.merge(backup_blob_router);
router
.route("/health/details", get(health::health_details))
.routes(routes!(capabilities::capabilities))
}
pub fn openapi_spec() -> utoipa::openapi::OpenApi {
build_api_router(false).split_for_parts().1
}
pub fn router_with_cors(allowed_origins: &[String], trust_xff: bool) -> Router<AppState> {
let (router, api) = build_api_router(trust_xff).split_for_parts();
let router = router.route("/openapi.json", get(move || serve_openapi(api.clone())));
let router =
router
.layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
.layer(TimeoutLayer::with_status_code(
axum::http::StatusCode::REQUEST_TIMEOUT,
REQUEST_TIMEOUT,
));
match build_cors_layer(allowed_origins) {
Some(cors) => router.layer(cors),
None => router,
}
}
#[cfg(test)]
mod cors_tests {
use super::*;
#[test]
fn empty_list_disables_cors_entirely() {
assert!(build_cors_layer(&[]).is_none());
}
#[test]
fn explicit_origin_produces_layer() {
let layer = build_cors_layer(&["http://localhost:8000".to_string()]);
assert!(layer.is_some());
}
#[test]
fn invalid_origin_filtered_out_and_empty_result_returns_none() {
let bad_origin = "http://localhost:8000\n".to_string();
assert!(build_cors_layer(&[bad_origin]).is_none());
}
#[test]
fn wildcard_alone_yields_no_layer() {
assert!(
build_cors_layer(&["*".to_string()]).is_none(),
"wildcard must be filtered to None, never partial-applied"
);
}
#[test]
fn wildcard_mixed_with_explicit_origins_drops_wildcard_keeps_others() {
let layer = build_cors_layer(&["*".to_string(), "http://localhost:8000".to_string()]);
assert!(layer.is_some());
}
#[test]
fn empty_origin_string_filtered() {
let layer = build_cors_layer(&["".to_string(), "http://x".to_string()]);
assert!(layer.is_some());
}
#[test]
fn openapi_spec_describes_registered_routes() {
let spec = openapi_spec();
assert_eq!(spec.info.title, "Verifiable Trust Agent (VTA) API");
let schemes = &spec
.components
.as_ref()
.expect("components present once a route contributes a schema")
.security_schemes;
assert!(
schemes.contains_key("bearer_jwt"),
"bearer_jwt security scheme must be registered"
);
let cap = spec
.paths
.paths
.get("/capabilities")
.expect("/capabilities operation must be in the spec");
assert!(
cap.get.is_some(),
"/capabilities must document a GET operation"
);
assert!(
spec.components
.as_ref()
.unwrap()
.schemas
.contains_key("CapabilitiesResponse"),
"CapabilitiesResponse schema must be emitted"
);
}
#[test]
fn openapi_spec_covers_the_route_groups() {
let spec = openapi_spec();
let paths = &spec.paths.paths;
for p in [
"/auth/challenge",
"/keys",
"/keys/{key_id}",
"/contexts",
"/acl",
"/acl/{did}",
"/did-templates",
"/audit/logs",
"/cache/{key}",
"/config",
"/step-up/policy",
"/capabilities",
"/vta/restart",
"/backup/export",
"/backup/blob/{bundle_id}",
"/services/didcomm/enable",
"/services",
"/webvh/dids",
"/webvh/servers",
"/did/verification-methods/passkey",
] {
assert!(paths.contains_key(p), "spec missing documented path {p}");
}
assert!(
paths.len() >= 60,
"expected the documented surface to be >= 60 paths, got {}",
paths.len()
);
}
#[test]
fn health_router_with_cors_builds_both_branches() {
let _with = health_router_with_cors(&["http://localhost:8000".to_string()]);
let _without = health_router_with_cors(&[]);
let _wildcard_only = health_router_with_cors(&["*".to_string()]);
}
}