This guide walks through integrating a 3rd-party application or service with a Verifiable Trust Agent (VTA). By the end you will have a service that:
- Authenticates to a VTA using a credential bundle
- Fetches its DID and private keys for signing or DIDComm
- Caches secrets locally for offline resilience
- Refreshes credentials automatically
- A running VTA instance (local or remote)
- A credential bundle (base64url string) issued by the VTA admin
- Rust 1.94.0+ (or use the REST API directly from any language)
| Term | Description |
|---|---|
| VTA | Verifiable Trust Agent — manages keys, DIDs, and access control |
| Context | A logical namespace within the VTA (e.g., my-app, mediator) |
| Credential bundle | A portable base64url token containing your DID, private key, and VTA endpoint |
DidSecretsBundle |
A JSON bundle containing a DID and its associated private keys |
The VTA admin creates a context for your application and issues a credential:
# Using the PNM CLI (admin)
pnm context provision \
--id my-service \
--name "My Service" \
--admin-label "Service Admin"This outputs a base64url credential string. Store it securely — it grants
access to the my-service context.
[dependencies]
vta-sdk = { version = "0.4", features = ["integration"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tracing = "0.1"
tracing-subscriber = "0.3"The integration feature enables the unified startup module, which
includes the HTTP client, session management, and offline caching support.
The SecretCache trait lets your service persist secrets locally so it can
start even when the VTA is temporarily unreachable. The simplest
implementation writes to a file:
use std::path::PathBuf;
use vta_sdk::did_secrets::DidSecretsBundle;
use vta_sdk::integration::SecretCache;
struct FileCache {
path: PathBuf,
}
impl SecretCache for FileCache {
async fn store(
&self,
bundle: &DidSecretsBundle,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// DidSecretsBundle is plain serde — JSON is the canonical wire form.
// For at-rest secrecy, layer your own encryption (e.g. age, AES-GCM
// keyed off OS-keyring) or front this with a secret manager.
let encoded = serde_json::to_string(bundle)?;
tokio::fs::write(&self.path, encoded).await?;
Ok(())
}
async fn load(
&self,
) -> Result<Option<DidSecretsBundle>, Box<dyn std::error::Error + Send + Sync>> {
match tokio::fs::read_to_string(&self.path).await {
Ok(data) => Ok(Some(serde_json::from_str(&data)?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
}For production, consider encrypting the cached file or using your platform's secret manager (AWS Secrets Manager, GCP Secret Manager, OS keyring, etc.).
use vta_sdk::integration::{startup, VtaServiceConfig, SecretSource};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::init();
let config = VtaServiceConfig {
credential: load_credential()?, // Load from env, file, or secret manager
context: "my-service".into(),
url_override: None, // Use the URL embedded in the credential
};
let cache = FileCache {
path: "/var/lib/my-service/vta-secrets.cache".into(),
};
let result = startup(&config, &cache).await?;
match result.source {
SecretSource::Vta => tracing::info!("Started with fresh secrets from VTA"),
SecretSource::Cache => tracing::warn!("Started with CACHED secrets (VTA unreachable)"),
}
tracing::info!(did = %result.did, secrets = result.bundle.secrets.len());
// Use result.bundle.secrets for DIDComm or signing
// Use result.client (if Some) for additional VTA API calls
Ok(())
}- Authenticates to the VTA using the credential bundle (lightweight REST auth with automatic fallback to DIDComm session auth)
- Fetches the latest
DidSecretsBundlefrom the VTA context - Caches the bundle locally via your
SecretCacheimplementation - Falls back to the cached bundle if the VTA is unreachable
On first run (no cache), the VTA must be reachable. On subsequent runs, the service can start with cached secrets and refresh them later.
The DidSecretsBundle contains private keys that you can use directly:
use vta_sdk::did_key::secret_from_key_response;
// The bundle contains SecretEntry items with key_id and private_key_multibase
for entry in &result.bundle.secrets {
tracing::info!(
key_id = %entry.key_id,
key_type = %entry.key_type,
"Available key"
);
}Or use the VTA as a remote signing oracle (keys never leave the VTA):
if let Some(ref client) = result.client {
let signature = client.sign("my-key-id", b"payload to sign", "EdDSA").await?;
tracing::info!(signature = %signature.signature, "Signed payload");
}If you don't need offline resilience, use the client directly:
use vta_sdk::prelude::*;
// One-line auth from a credential bundle
let client = VtaClient::from_credential(&credential_b64, None).await?;
// Token refresh is automatic — no manual token management needed
// Create keys
let key = client.create_key(
CreateKeyRequest::new(KeyType::Ed25519)
.label("signing-key")
.context("my-app")
).await?;
// List keys
let keys = client.list_keys(None, None, None, None, None).await?;
// Get a key's private material (for local signing)
let secret = client.get_key_secret(&key.key_id).await?;
// Server-side signing (key never leaves VTA)
let sig = client.sign(&key.key_id, b"hello", "EdDSA").await?;
// Fetch all secrets for a context as a portable bundle
let bundle = client.fetch_did_secrets_bundle("my-app").await?;The VTA exposes a standard REST API. Any HTTP client can integrate:
POST /auth/challenge
Content-Type: application/json
{"did": "did:key:z6Mk..."}
→ {"session_id": "...", "data": {"challenge": "..."}}
POST /auth/
Content-Type: text/plain
<DIDComm-packed authenticate message>
→ {"data": {"access_token": "...", "refresh_token": "..."}}
# List keys
GET /keys?context_id=my-app&status=active
Authorization: Bearer <token>
# Create a key
POST /keys
Authorization: Bearer <token>
{"key_type": "Ed25519", "label": "my-key", "context_id": "my-app"}
# Sign a payload
POST /keys/<key_id>/sign
Authorization: Bearer <token>
{"payload": "<base64url>", "algorithm": "EdDSA"}
# Get private key material
GET /keys/<key_id>/secret
Authorization: Bearer <token>
# List contexts
GET /contexts
Authorization: Bearer <token>
# Create a context
POST /contexts
Authorization: Bearer <token>
{"id": "my-app", "name": "My Application"}
See the VTA Service README for the complete API reference.
-
Store credentials securely — Use your platform's secret manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, or OS keyring). Never commit credentials to source control.
-
Encrypt cached secrets — The
SecretCachefile contains private keys. Encrypt it at rest or use a secret manager backend. -
Use context scoping and least-privilege roles — Each application should have its own VTA context with the minimum required role:
- Reader — for services that only need to read keys and config
- Application — for services that need to sign or write to cache
- Admin — only for services that manage keys and DIDs Avoid sharing admin credentials across services.
-
Prefer server-side signing — Use
POST /keys/{id}/signinstead of exporting private keys when possible. This keeps keys inside the VTA's security boundary. -
Monitor token expiry —
VtaClienthandles refresh automatically, but long-running services should monitorclient.token_expires_at()for health checks. -
Handle offline gracefully — The integration module's cache fallback ensures your service can start during VTA outages, but cached keys may be stale (e.g., after a key rotation). Log the
SecretSource::Cachestate prominently and refresh as soon as the VTA is reachable.
┌─────────────────────────────────────┐
│ Your Application │
│ │
│ ┌───────────┐ ┌───────────────┐ │
│ │ vta-sdk │ │ SecretCache │ │
│ │ integration│ │ (your impl) │ │
│ └─────┬─────┘ └──────┬────────┘ │
│ │ │ │
└────────┼───────────────┼────────────┘
│ │
▼ ▼
┌─────────┐ ┌────────────┐
│ VTA │ │ Local disk │
│ Service │ │ / keyring │
└─────────┘ └────────────┘
- On startup,
vta_sdk::integration::startup()authenticates and fetches secrets. - Fresh secrets are cached locally via your
SecretCacheimplementation. - If the VTA is unreachable, cached secrets are loaded instead.
- Your application uses the DID and keys for signing, DIDComm, or other operations.