Vectis protects data throughout its lifecycle. The HTTP API exposes operations to create key material, validate keys, publish public keys, sign message hashes, exchange protected messages between Vectis instances, and encrypt/decrypt internal messages.
The vectis CLI is an HTTP client for the runtime API, except for vectis init, vectis serve, vectis apikey create, vectis config sign, and vectis config list, which are local commands.
- Default base URL:
http://127.0.0.1:3000 - Requests with a body use
Content-Type: application/json. - Request bodies are limited to 2 MiB (
2,097,152bytes) by the internalINTERNAL_HTTP_MAX_SIZEpolicy. Larger bodies are rejected before authentication, authorization, storage, or cryptographic processing. - Timestamps are encoded as Unix epoch seconds in a string.
kidvalues are hex strings derived with Vectis' internal hash (INTERNAL_KEYS_HASH, currentlyBLAKE2b(256)), so they are normally 64 hex characters.- Binary fields (
ctx,nonce, signatures, public keys) are encoded as hex. - Every HTTP response includes
X-Request-Id, a 32-character hex request id also present in operational logs. - Public errors use this shape:
{
"error": "invalid request"
}An oversized request body returns 413 Payload Too Large:
{
"error": "request body exceeds maximum allowed size"
}Only v1 is supported. Any other version is rejected explicitly; there is no silent downgrade.
The signed config file (config.json, with routes, remote_routes, and permissions
and optional fpe_profiles sections) carries a single top-level version that is part of the signed content.
Signed tokens and protected messages intentionally carry version at two levels:
- Envelope (
version): used for dispatch — it lets a receiver decide how to interpret thepayloadbefore parsing or trusting it, which keeps room for future versions whose payload shape may differ. - Payload (
payload.version): used for integrity — it is inside the signed bytes, so signed content cannot be reinterpreted under a different version.
Verification requires version == payload.version; a mismatch is rejected. This binds the two
copies and defends against version-confusion / downgrade attempts. This is not accidental
duplication: the envelope copy negotiates, the payload copy authenticates.
Signatures cover the canonical JSON (sorted keys, compact, UTF-8) of the payload, so
payload.version is protected by the signature.
Protected endpoints require:
X-API-Key: <VECTIS_APIKEY>VECTIS_APIKEY is the client secret sent in the X-API-Key header. vectis init and vectis apikey create generate it from Botan's cryptographic random number generator. The server validates it against VECTIS_APIKEY_HASH, which is derived with the init API auth key.
VECTIS_APIKEY and VECTIS_APIKEY_HASH are the root API key pair. Root can access every protected endpoint. Additional clients can be authorized through the permissions section of the signed config; clients with admin can also access protected administrative endpoints, including POST /config/reload.
Endpoints requiring auth:
POST /keysGET /keys/propertiesGET /keys/properties/{kid}POST /keys/reloadPOST /lifecycle/{kid}POST /config/reloadGET /routesGET /remote-routesGET /permissionsGET /metricsGET /self-test/initGET /self-test/keys/{kid}POST /sign/{kid}POST /message/{sender_kid}POST /message/decryptPOST /message/internal/encrypt/{kid}POST /message/internal/decryptPOST /fpe/encrypt/{kid}POST /fpe/encrypt/batch/{kid}POST /fpe/decryptPOST /fpe/decrypt/batchPOST /token/encode/{kid}POST /token/encode/batch/{kid}POST /token/decodePOST /token/decode/batchPOST /mac/{kid}POST /mac/batch/{kid}POST /mac/verifyPOST /mac/verify/batchPOST /commit/{kid}POST /commit/batch/{kid}POST /commit/verifyPOST /commit/verify/batchPOST /shares/split/{kid}POST /shares/combinePOST /index/{kid}POST /index/batch/{kid}POST /index/verifyPOST /index/verify/batchPOST /mask/{kid}POST /mask/batch/{kid}POST /time/attest
Endpoints without auth:
GET /healthz/startupGET /healthz/liveGET /healthz/readyGET /keysGET /pub/{kid}POST /sign/verificationPOST /message
Hash:
BLAKE2b(160),BLAKE2b(224),BLAKE2b(256),BLAKE2b(384),BLAKE2b(512)SHA-224,SHA-256,SHA-384,SHA-512,SHA-512-256SHA-3(224),SHA-3(256),SHA-3(384),SHA-3(512)Whirlpool
Symmetric:
ChaCha20Poly1305AES-128/GCMAES-192/GCMAES-256/GCM
EdDSA:
Ed25519Ed448
XECDH:
X25519X448
ML-DSA:
ML-DSA-44ML-DSA-65ML-DSA-87
ML-KEM:
ML-KEM-512ML-KEM-768ML-KEM-1024
Startup probe. Reports when the HTTP service state was initialized.
Response:
{
"status": "started",
"timestamp": "1782058090"
}Liveness probe. Does not perform I/O.
Response:
{
"status": "ok"
}Readiness probe. Performs a lightweight storage check and reports current in-memory state counts. It does not reload keys or routes.
Response:
{
"status": "ready",
"unsealed": true,
"storage": "ok",
"keys_loaded": 3,
"routes_loaded": 1
}Requests a complete on-demand time attestation using authenticated NTS and a
verified Roughtime response. It requires the global time-attest permission
with kid: "*" and has no request body. It is not part of readiness.
Both sources must be available or the endpoint returns 502 without partial
results. A 200 with server_clock.acceptable: false means both measurements
were valid but did not meet the signed policy.
Prometheus metrics in the text exposition format (text/plain; version=0.0.4). Requires auth with root, admin, or the metrics permission. Enabled by VECTIS_METRICS_ENABLED (default true); returns 404 when disabled after auth succeeds. Labels are low cardinality and carry no sensitive data. Current labels are limited to stable dimensions such as method, endpoint route template, status, outcome, operation, and result.
Exposed metrics:
http_requests_total{method,endpoint,status}http_request_duration_seconds{method,endpoint}(histogram)auth_total{outcome}(allowordeny)vectis_unsealedvectis_keys_loadedvectis_routes_loadedvectis_remote_routes_loadedvectis_permission_clientsvectis_fpe_profiles_loadedvectis_tokenization_profiles_loadedvectis_mac_profiles_loadedvectis_masking_profiles_loadedvectis_commitment_profiles_loadedvectis_sharing_profiles_loadedvectis_permission_total{result}(allowordeny)vectis_config_reload_total{result}(success,stale, orfailed)vectis_config_last_reload_timestamp_seconds{result}(success,stale, orfailed)vectis_keys_reload_total{result}(successorfailed)vectis_message_total{operation,result}(send,receive, ordecrypt;success,denied, orfailed)vectis_crypto_operation_total{operation,result}(sign,verify,encrypt,decrypt,fpe_encrypt,fpe_decrypt,fpe_encrypt_batch,fpe_decrypt_batch,token_encode,token_decode,token_encode_batch,token_decode_batch,mac_create,mac_verify,mac_create_batch,mac_verify_batch,commit_create,commit_verify,commit_create_batch,commit_verify_batch,share_split,share_combine,index_create,index_verify,index_create_batch,index_verify_batch,mask, ormask_batch;successorfailed)
Validates the key material generated by vectis init.
Requires auth.
Response:
{
"timestamp": "1782058090",
"aad": "version=v1;hostname=localhost;type=init-keys;cipher=AES-256/GCM",
"hash": {
"variant": "BLAKE2b(256)",
"value_hex": "..."
},
"symmetric": {
"variant": "ChaCha20Poly1305",
"valid": true
},
"eddsa": {
"variant": "Ed25519",
"valid": true
},
"xecdh": {
"variant": "X25519",
"valid": true
},
"ml-dsa": {
"variant": "ML-DSA-44",
"valid": true
},
"ml-kem": {
"variant": "ML-KEM-512",
"valid": true
}
}Validates a key loaded from storage into memory. It does not expose private keys.
Requires auth.
Response:
{
"timestamp": "1782058090",
"aad": "...",
"hash": {
"variant": "SHA-256",
"value_hex": "..."
},
"symmetric": {
"variant": "AES-256/GCM",
"valid": true
},
"eddsa": {
"variant": "Ed25519",
"valid": true
},
"xecdh": {
"variant": "X25519",
"valid": true
},
"ml-dsa": {
"variant": "ML-DSA-44",
"valid": true
},
"ml-kem": {
"variant": "ML-KEM-512",
"valid": true
}
}Creates an operational key set, encrypts it with the internal symmetric key created by init, stores it, and loads it into memory.
Requires auth.
Request:
{
"tag": "ACME Corp.",
"profile": "hybrid-high-assurance-v1"
}All fields are optional:
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | No | Human-readable label for the key. Must not contain ; or = (the value is embedded in the key's info/AAD string, formatted as version=v1;...;tag=<tag>;...). If missing, Vectis uses a timestamp. |
profile |
string | No | Crypto profile used as the base algorithm policy. If missing, Vectis uses VECTIS_DEFAULT_CRYPTO_PROFILE. |
hash_algorithm |
string | No | Individual hash override. Accepted only when VECTIS_CRYPTO_POLICY=allow-overrides. |
symmetric_algorithm |
string | No | Individual symmetric algorithm override. Accepted only when VECTIS_CRYPTO_POLICY=allow-overrides. |
eddsa_algorithm |
string | No | Individual EdDSA override. Accepted only when VECTIS_CRYPTO_POLICY=allow-overrides. |
xecdh_algorithm |
string | No | Individual XECDH override. Accepted only when VECTIS_CRYPTO_POLICY=allow-overrides. |
ml_dsa_variant |
string | No | Individual ML-DSA override. Accepted only when VECTIS_CRYPTO_POLICY=allow-overrides. |
ml_kem_variant |
string | No | Individual ML-KEM override. Accepted only when VECTIS_CRYPTO_POLICY=allow-overrides. |
When VECTIS_CRYPTO_POLICY=profile-only, Vectis rejects all individual algorithm fields and accepts only tag and profile.
Supported profiles:
hybrid-performance-v1:BLAKE2b(256),ChaCha20Poly1305,Ed25519,X25519,ML-DSA-44,ML-KEM-512hybrid-standard-v1:SHA-3(256),AES-128/GCM,Ed25519,X25519,ML-DSA-44,ML-KEM-512hybrid-high-assurance-v1:SHA-3(384),AES-192/GCM,Ed25519,X25519,ML-DSA-65,ML-KEM-768hybrid-long-term-v1:SHA-3(512),AES-256/GCM,Ed448,X448,ML-DSA-87,ML-KEM-1024
Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed"
}Lists keys currently loaded in memory. This endpoint does not require auth.
This public discovery endpoint intentionally omits decrypted lifecycle properties.
Response:
{
"keys": [
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"info": "version=v1;hostname=localhost;type=ops-keys;cipher=AES-192/GCM;tag=ACME Corp.;profile=hybrid-high-assurance-v1;timestamp=1782058090"
}
]
}Administrative refresh operation. Reloads the local in-memory key state from storage, decrypting the keys and properties this node can load, then returns the refreshed state with properties.
This endpoint uses POST because it changes server memory state.
Requires auth.
Response:
{
"keys": [
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"info": "version=v1;hostname=localhost;type=ops-keys;cipher=AES-192/GCM;tag=ACME Corp.;profile=hybrid-high-assurance-v1;timestamp=1782058090",
"properties_info": "version=v1;hostname=localhost;type=ops-key-properties;cipher=AES-192/GCM;kid=f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed;tag=ACME Corp.;profile=hybrid-high-assurance-v1;timestamp=1782058090",
"properties": {
"version": 1,
"profile": "hybrid-high-assurance-v1",
"tag": "ACME Corp.",
"created_at": "1782058090",
"lifecycle": {
"status": "active",
"reason": "initial creation",
"changed_at": "1782058090"
},
"access": null
}
}
]
}Lists keys currently loaded in memory with decrypted lifecycle properties.
Requires auth.
GET /keys remains public and does not expose properties.
info is the AAD used for the encrypted operational key material. properties_info is the AAD used for the encrypted properties payload.
Response:
{
"keys": [
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"info": "version=v1;hostname=localhost;type=ops-keys;cipher=AES-192/GCM;tag=payments-prod;profile=hybrid-high-assurance-v1;timestamp=1782058090",
"properties_info": "version=v1;hostname=localhost;type=ops-key-properties;cipher=AES-192/GCM;kid=f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed;tag=payments-prod;profile=hybrid-high-assurance-v1;timestamp=1782058090",
"properties": {
"version": 1,
"profile": "hybrid-high-assurance-v1",
"tag": "payments-prod",
"created_at": "1782058090",
"lifecycle": {
"status": "active",
"reason": "initial creation",
"changed_at": "1782058090"
},
"access": null
}
}
]
}Returns decrypted lifecycle properties for one key. If the key is not currently loaded in memory, Vectis attempts to load and decrypt it from storage first.
Requires auth.
Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"info": "version=v1;hostname=localhost;type=ops-keys;cipher=AES-192/GCM;tag=payments-prod;profile=hybrid-high-assurance-v1;timestamp=1782058090",
"properties_info": "version=v1;hostname=localhost;type=ops-key-properties;cipher=AES-192/GCM;kid=f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed;tag=payments-prod;profile=hybrid-high-assurance-v1;timestamp=1782058090",
"properties": {
"version": 1,
"profile": "hybrid-high-assurance-v1",
"tag": "payments-prod",
"created_at": "1782058090",
"lifecycle": {
"status": "active",
"reason": "initial creation",
"changed_at": "1782058090"
},
"access": null
}
}Updates encrypted lifecycle metadata for an operational key. Lifecycle status is enforced by cryptographic operations.
Requires auth.
Request:
{
"status": "disabled",
"reason": "maintenance window"
}Allowed status values:
activedisabledretiredcompromiseddestroyed
reason is required, must not contain control characters, and is limited to
128 characters.
Lifecycle behavior:
active: normal use.disabled: blocked for all cryptographic operations.retired: allowed only for decrypt and verification; blocked for new encryption/signing/sending operations and/pub.compromised: blocked for all cryptographic operations.destroyed: logically destroyed; administrative metadata is retained, but cryptographic operations are blocked.
Allowed transitions:
active->disabled,retired,compromised,destroyeddisabled->activeretired-> no transitionscompromised-> no transitionsdestroyed-> no transitions
Transitions to the same status are rejected.
Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"lifecycle": {
"status": "disabled",
"reason": "maintenance window",
"changed_at": "1782059000"
}
}Administrative refresh operation. Reloads the unified signed config into memory.
Requires auth with root or an admin client.
If config.json is missing, Vectis reloads to empty config sections. If the config exists but is invalid, unsigned, has an invalid signature, references an unloaded KID where a loaded KID is required, or contains invalid section data, the request fails and the previous in-memory config remains active. If config.json has changed but config_sign.json still signs older content, reload keeps the previous in-memory config and returns a warning.
Response:
{
"status": "reloaded",
"warning": "config.json has changes not covered by config_sign.json — run 'vectis config sign' first",
"routes_loaded": 1,
"remote_routes_loaded": 1,
"clients_loaded": 1,
"fpe_profiles_loaded": 1,
"tokenization_profiles_loaded": 1,
"mac_profiles_loaded": 1,
"commitment_profiles_loaded": 1,
"sharing_profiles_loaded": 1,
"masking_profiles_loaded": 1
}warning is optional and appears only when the current config.json is not covered by the current config_sign.json.
Lists the final app routes currently loaded in memory. It does not read config.json.
Requires auth.
Response:
{
"routes": [
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"name": "clinical-app-a",
"final_app_addr": "127.0.0.1:3999",
"final_app_path": "/message"
}
]
}Administrative endpoint. Lists authorized remote Vectis routes currently loaded in memory.
Requires auth.
Response:
{
"routes": [
{
"remote_kid": "b01cbe33187916f0f1367d07bc986d71bb0d91d7047ccd790c13fc9d85fe7259",
"name": "site-b",
"remote_addr": "vectis-b.example.com:443",
"allowed_local_kids": ["*"],
"status": "active"
}
]
}Permissions are the permissions section of the unified signed config file:
VECTIS_CONFIG_PATH=config.json
VECTIS_CONFIG_SIGN_PATH=config_sign.jsonvectis config sign signs VECTIS_CONFIG_PATH locally with init keys and writes VECTIS_CONFIG_SIGN_PATH. The config file has routes, remote_routes, and permissions sections under a top-level version.
Recommended admin permission:
{
"kid": "*",
"actions": ["admin"]
}If any permission entry contains admin, Vectis treats the whole client as admin and ignores kid plus any other actions for that client.
Allowed actions:
adminkeyslifecycleself-testsignmessagefpe-encryptfpe-decrypttoken-encodetoken-decodemac-createmac-verifyindex-createindex-verifymaskmetricstime-attest
Permission mapping:
| Permission | Endpoints |
|---|---|
admin |
POST /keys, POST /keys/reload, GET /keys/properties, POST /config/reload, GET /routes, GET /remote-routes, GET /permissions, GET /self-test/init, GET /metrics |
keys |
GET /keys/properties/{kid} |
lifecycle |
POST /lifecycle/{kid} |
self-test |
GET /self-test/keys/{kid} |
sign |
POST /sign/{kid} |
message |
POST /message/{sender_kid}, POST /message/decrypt, POST /message/internal/encrypt/{kid}, POST /message/internal/decrypt |
fpe-encrypt |
POST /fpe/encrypt/{kid}, POST /fpe/encrypt/batch/{kid} |
fpe-decrypt |
POST /fpe/decrypt, POST /fpe/decrypt/batch |
token-encode |
POST /token/encode/{kid}, POST /token/encode/batch/{kid} |
token-decode |
POST /token/decode, POST /token/decode/batch |
mac-create |
POST /mac/{kid}, POST /mac/batch/{kid} |
mac-verify |
POST /mac/verify, POST /mac/verify/batch |
commit-create |
POST /commit/{kid}, POST /commit/batch/{kid} |
commit-verify |
POST /commit/verify, POST /commit/verify/batch |
share-split |
POST /shares/split/{kid} |
share-combine |
POST /shares/combine |
index-create |
POST /index/{kid}, POST /index/batch/{kid} |
index-verify |
POST /index/verify, POST /index/verify/batch |
mask |
POST /mask/{kid}, POST /mask/batch/{kid} |
metrics |
GET /metrics with kid: "*" |
time-attest |
POST /time/attest with kid: "*" |
metrics and time-attest are global actions: their grants must use kid: "*", and neither endpoint accepts a KID. Root always passes permission checks. Routes operations require root or admin; there is no granular routes action. FPE, tokenization, MAC, blind-index, commitment, sharing, and masking actions require explicit KID-scoped grants; kid: "*" is rejected for those actions.
Example time-attestation grant:
{
"client": "clock-monitor",
"apikey_hash": "<VECTIS_APIKEY_HASH>",
"status": "active",
"permissions": [
{ "kid": "*", "actions": ["time-attest"] }
]
}Permissions file shape:
{
"version": "v1",
"clients": [
{
"client": "client-a",
"apikey_hash": "<VECTIS_APIKEY_HASH>",
"status": "active",
"permissions": [
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"actions": ["sign", "message"]
}
]
}
]
}Administrative endpoint. Lists the effective API key permissions currently loaded in memory. It does not read config.json directly and never returns apikey_hash.
Requires root or admin.
Response:
{
"clients": [
{
"client": "clinic-app",
"admin": false,
"permissions": [
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"actions": ["message"]
}
]
},
{
"client": "ops-admin",
"admin": true,
"permissions": [
{
"kid": "*",
"actions": ["admin"]
}
]
}
]
}Admin clients are shown with the effective permission kid: "*", actions: ["admin"] because Vectis ignores kid-scoped grants once a client has admin.
Reload permissions by reloading the unified config with POST /config/reload.
Returns public keys only. This endpoint does not require auth.
Lifecycle behavior:
active: public keys are returned.disabled,retired,compromised,destroyed: request is rejected.
Response:
{
"info": "version=v1;hostname=localhost;type=ops-keys;cipher=AES-192/GCM;tag=ACME Corp.;profile=hybrid-high-assurance-v1;timestamp=1782058090",
"keys": {
"eddsa": {
"alg": "Ed25519",
"public_key_der_hex": "..."
},
"xecdh": {
"alg": "X25519",
"public_key_hex": "..."
},
"ml-dsa": {
"alg": "ML-DSA-44",
"public_key_der_hex": "..."
},
"ml-kem": {
"alg": "ML-KEM-512",
"public_key_der_hex": "..."
}
}
}Signs a message hash with EdDSA and ML-DSA.
Requires auth.
Request:
{
"message_hash": {
"alg": "BLAKE2b(256)",
"hex": "..."
}
}message_hash.hex must have the correct length for message_hash.alg.
Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"signature": "<base64url(header)>.<base64url(payload)>.<base64url(eddsa)>.<base64url(ml-dsa)>"
}The canonical compact header is { "version": "vectis-signature-v1" }. Both
signatures cover the exact ASCII bytes header_b64.payload_b64.
Verifies a token emitted by POST /sign/{kid}. This endpoint does not require auth.
The signer key is resolved locally first; if the token's kid is not a local key, Vectis resolves the signer's public keys from a trusted peer in the signed config (an active remote_routes entry with public_keys for that kid), enabling cross-instance verification. If the kid is neither local nor a known peer, verification fails.
Request: the complete JSON returned by POST /sign/{kid}.
Valid response:
{
"status": {
"eddsa": "ok",
"ml-dsa": "ok"
},
"valid": "ok"
}Response with invalid signatures:
{
"status": {
"eddsa": "not_checked",
"ml-dsa": "fail"
},
"valid": "fail"
}Verification validates all four Base64URL segments, resolves the KID from the
request, then verifies ML-DSA before EdDSA. eddsa: "not_checked" means
ML-DSA failed, so EdDSA was deliberately not evaluated. The authenticated
payload KID must match the request KID.
Sends a protected message from this Vectis instance to another Vectis instance.
Requires auth.
Request:
{
"recipient_kid": "b01cbe33187916f0f1367d07bc986d71bb0d91d7047ccd790c13fc9d85fe7259",
"message": "hello vectis"
}Flow:
- Validate
sender_kid,recipient_kid, andmessage. - Resolve
recipient_kidthrough the signed configremote_routessection. - Load the recipient's public keys from the
public_keysof the resolvedremote_routesentry. The signed config is the only source of peer public keys; Vectis never fetches/pubat runtime. - Create a hybrid secret with XECDH + ML-KEM.
- Derive
message_keywith HKDF. - Encrypt the message.
- Sign the payload with EdDSA and ML-DSA.
- Send the envelope to
POST /messageon the recipient.
Response:
{
"message": {
"valid": true
},
"symmetric": {
"variant": "AES-256/GCM",
"valid": true
},
"eddsa": {
"variant": "Ed25519",
"valid": true
},
"xecdh": {
"variant": "X25519",
"valid": true
},
"ml-dsa": {
"variant": "ML-DSA-44",
"valid": true
},
"ml-kem": {
"variant": "ML-KEM-512",
"valid": true
}
}Relevant public errors:
{
"error": "recipient route has no registered public keys in the signed config"
}{
"error": "internal server error final app can't be reached"
}Receives a protected message from another Vectis instance. This endpoint does not require auth because authenticity is validated with EdDSA and ML-DSA signatures. The sender kid must match an active remote_routes entry with registered public_keys in the signed config; messages from unregistered senders are rejected with 403.
Request:
{
"version": "v1",
"payload": {
"version": "v1",
"type": "protected-message",
"created_at": "1782058090",
"sender": {
"host": "127.0.0.1:3000",
"kid": "..."
},
"recipient": {
"kid": "..."
},
"kem": {
"alg": "X25519+ML-KEM-512",
"xecdh_ephemeral_public": "...",
"ml_kem_ciphertext": "...",
"ml_kem_salt": "...",
"hkdf_salt": "..."
},
"cipher": {
"alg": "AES-256/GCM",
"nonce": "...",
"aad": "...",
"ct": "..."
}
},
"signatures": {
"eddsa": {
"alg": "Ed25519",
"sig": "..."
},
"ml-dsa": {
"alg": "ML-DSA-44",
"sig": "..."
}
}
}Response:
{
"status": "ok",
"sender_kid": "...",
"recipient_kid": "...",
"local_cipher": {
"alg": "AES-256/GCM",
"nonce": "...",
"aad": "...",
"ct": "..."
}
}After receiving a message, Vectis re-encrypts the plaintext with the local symmetric key for recipient_kid and delivers it to the configured final app.
Decrypts a local message received by the final app.
Requires auth.
Request:
{
"sender_host": "127.0.0.1:3000",
"sender_kid": "...",
"timestamp": "1782058090",
"message": {
"ctx": "...",
"nonce": "...",
"aad": "version=v1;type=stored-protected-message;sender_kid=...;recipient_kid=...;source_created_at=1782058090;cipher_alg=AES-256/GCM",
"variant": "AES-256/GCM"
}
}Response:
{
"plaintext": "hello vectis"
}These endpoints encrypt and decrypt internal messages with the symmetric key associated with a kid. They are meant for local data protection without running the network exchange flow between Vectis instances.
Requires auth.
Request:
{
"plaintext": "hello vectis"
}Response:
{
"timestamp": "1782058090",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"message": {
"ctx": "...",
"nonce": "...",
"aad": "version=v1;type=internal-message;kid=...;timestamp=1782058090;cipher_alg=AES-256/GCM",
"variant": "AES-256/GCM"
}
}Requires auth.
Request: the JSON returned by POST /message/internal/encrypt/{kid}.
Response:
{
"plaintext": "hello vectis"
}FPE is a local field operation. It preserves an alphabet and length range defined by a signed config profile. It is deterministic for the same key, profile, tweak AAD, and plaintext. It does not authenticate data and does not replace AEAD message encryption.
FPE profiles live in config.json under fpe_profiles. Requests cannot provide alphabet, tweak_aad, min_len, max_len, or fpe_version; those values come only from signed config.
All FPE requests include a client-defined ref. It is required, non-empty, at most 128 characters, and echoed in the response. Batch requests require every item ref to be unique within the request.
The FPE key is derived from the operational key's symmetric key:
INTERNAL_KEYS_HKDF(
input_key_material = ops_symmetric_key,
salt = "vectis:fpe:ff1:v1",
info = "profile=<profile_name>;kid=<kid>;fpe_version=<fpe_version>",
length = 32
)
Requires auth and fpe-encrypt permission for the path kid. The key must be active.
Request:
{
"ref": "reg1",
"profile": "patient-id-decimal-v1",
"plaintext": "123456"
}Response:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-decimal-v1",
"ciphertext": "839201"
}The response intentionally does not include fpe_version.
Requires auth and fpe-encrypt permission for the path kid. The key must be active.
The batch uses one profile and is all-or-nothing. items must contain between 1 and 128 entries. Each item must include a unique ref. Response order matches request order. If any item is invalid, the whole request fails and no partial items are returned.
Request:
{
"profile": "patient-id-decimal-v1",
"items": [
{ "ref": "reg1", "plaintext": "123456" },
{ "ref": "reg2", "plaintext": "654321" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-decimal-v1",
"items": [
{ "ref": "reg1", "ciphertext": "839201" },
{ "ref": "reg2", "ciphertext": "102938" }
]
}Requires auth and fpe-decrypt permission for the request kid. The key may be active or retired.
Request:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-decimal-v1",
"ciphertext": "839201"
}Response:
{
"ref": "reg1",
"plaintext": "123456"
}Requires auth and fpe-decrypt permission for the request kid. The key may be active or retired.
The batch uses one profile and is all-or-nothing. items must contain between 1 and 128 entries. Each item must include a unique ref. Response order matches request order. If any item is invalid, the whole request fails and no partial items are returned.
Request:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-decimal-v1",
"items": [
{ "ref": "reg1", "ciphertext": "839201" },
{ "ref": "reg2", "ciphertext": "102938" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-decimal-v1",
"items": [
{ "ref": "reg1", "plaintext": "123456" },
{ "ref": "reg2", "plaintext": "654321" }
]
}Tokenization is a local reversible random-token operation. It returns a visible random token and stores the original plaintext plus optional metadata encrypted in storage. The database only sees kid, hashid, and encrypted data; it never sees plaintext, metadata, profile name as a column, or the visible token.
Tokenization profiles live in config.json under tokenization_profiles. Requests cannot provide token_prefix, token_len, max_plaintext_len, or one_time; those values come only from signed config. Vectis uses the fixed internal tokenization scheme token-random-v1.
All tokenization requests include a client-defined ref. It is required, non-empty, at most 128 characters, and echoed in the response. Batch requests require every item ref to be unique within the request. For POST /token/decode/batch, profiles with one_time: true also require each token to be unique; a duplicate fails before lookup or consumption with batch item N failed: token batch contains duplicated token. Profiles with one_time: false may decode the same token more than once in a batch when every item has a distinct ref.
hash_key and data_key are derived from the operational key's symmetric key with INTERNAL_KEYS_HKDF and are prepared when config is loaded. The derivation binds the profile name, KID, and fixed internal tokenization scheme token-random-v1. tokens.data AAD also binds that internal scheme. Tokens are random and are not deterministic for the same plaintext.
Visible tokens have the form <token_prefix>_<base64url-no-pad random bytes>. token_len is the number of random bytes before base64url encoding, and decode validates both the configured prefix and decoded byte length before looking up the token.
Encode metadata is optional, must be a JSON object when present, and its compact serialized JSON representation must be at most 128 characters.
Batch tokenization preserves item order and is all-or-nothing. If any item fails validation, lookup, encryption, decryption, or storage insert, the response is a single error and no partial items are returned. POST /token/encode/batch/{kid} writes all token rows in one storage transaction. When one_time is true, decode consumes all batch tokens in one storage transaction only after every item decrypts successfully. A token lost to a concurrent consume returns batch item N failed: token not found, using its original input position. The maximum batch size is INTERNAL_TOKEN_BATCH (128).
Requires auth and token-encode permission for the path kid. The key must be active.
Request:
{
"ref": "reg1",
"profile": "patient-id-token-v1",
"plaintext": "123456",
"metadata": { "tenant": "acme" }
}Response:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-token-v1",
"token": "tok_patient_vGqyEeXKcKz5QK1jwBQTyQ"
}Requires auth and token-decode permission for the request kid. The key may be active or retired.
Request:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-token-v1",
"token": "tok_patient_vGqyEeXKcKz5QK1jwBQTyQ"
}Response:
{
"ref": "reg1",
"plaintext": "123456",
"metadata": { "tenant": "acme" }
}Requires auth and token-encode permission for the path kid. The key must be active.
Request:
{
"profile": "patient-id-token-v1",
"items": [
{ "ref": "reg1", "plaintext": "123456", "metadata": { "tenant": "acme" } },
{ "ref": "reg2", "plaintext": "654321" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-token-v1",
"items": [
{ "ref": "reg1", "token": "tok_patient_vGqyEeXKcKz5QK1jwBQTyQ" },
{ "ref": "reg2", "token": "tok_patient_j43sAUddCPYAaHwvA6Yoww" }
]
}Requires auth and token-decode permission for the request kid. The key may be active or retired.
Request:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-token-v1",
"items": [
{ "ref": "reg1", "token": "tok_patient_vGqyEeXKcKz5QK1jwBQTyQ" },
{ "ref": "reg2", "token": "tok_patient_j43sAUddCPYAaHwvA6Yoww" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "patient-id-token-v1",
"items": [
{ "ref": "reg1", "plaintext": "123456", "metadata": { "tenant": "acme" } },
{ "ref": "reg2", "plaintext": "654321" }
]
}MAC profiles live in signed config under mac_profiles. Requests select a
profile by name; kid and context come from signed config and must match the
request KID. Context uses key=value;key=value labels and is limited to 128
characters. MAC create requires an active key. MAC verify allows active or
retired keys.
All MAC requests include a client-defined ref. It is required, non-empty, at
most 128 characters, and echoed in the response.
If the operational key hash algorithm is SHA-3(N), Vectis uses KMAC-N and
returns an N-bit digest; otherwise it uses HMAC with the operational key hash
algorithm. In both cases Vectis derives a MAC key from the operational
symmetric key and applies the signed context.
Requires auth and mac-create permission for the path KID.
Request:
{
"ref": "reg1",
"profile": "pan-blind-index-v1",
"plaintext": "4111111111111111"
}Response:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-blind-index-v1",
"algorithm": "KMAC-256",
"digest": "hex..."
}Requires auth and mac-verify permission for the request body KID.
Request:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-blind-index-v1",
"plaintext": "4111111111111111",
"digest": "hex..."
}Response:
{
"ref": "reg1",
"valid": true
}Requires auth and mac-create permission for the path KID.
The batch uses one profile and is all-or-nothing. items must contain between
1 and 128 entries. Each item must include a unique ref. Response order
matches request order. If any item is invalid, the whole request fails and no
partial items are returned.
Request:
{
"profile": "pan-blind-index-v1",
"items": [
{ "ref": "reg1", "plaintext": "4111111111111111" },
{ "ref": "reg2", "plaintext": "5555555555554444" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-blind-index-v1",
"algorithm": "KMAC-256",
"items": [
{ "ref": "reg1", "digest": "hex..." },
{ "ref": "reg2", "digest": "hex..." }
]
}Requires auth and mac-verify permission for the request body KID.
The batch uses one profile and is all-or-nothing for request validation and
execution errors. A digest mismatch is not an error; it returns valid: false
for that item.
Request:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-blind-index-v1",
"items": [
{ "ref": "reg1", "plaintext": "4111111111111111", "digest": "hex..." },
{ "ref": "reg2", "plaintext": "5555555555554444", "digest": "hex..." }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-blind-index-v1",
"items": [
{ "ref": "reg1", "valid": true },
{ "ref": "reg2", "valid": false }
]
}Commitment profiles live in signed config under commitment_profiles. They use
the same MAC algorithm resolution as mac_profiles: SHA-3 operational keys use
KMAC with the corresponding output size, and other keys use HMAC with the
operational key hash algorithm. Commitments are keyed and verified by Vectis;
they are stateless and are not stored.
Unlike MAC or blind indexes, commitment create generates a random opening.
Two commitments for the same plaintext are expected to differ when their
openings differ. Verify recomputes the commitment from plaintext, opening,
profile, KID, and signed context.
Requires auth and commit-create permission for the path KID. Create requires
an active key.
Request:
{
"ref": "reg1",
"profile": "pan-commitment-v1",
"plaintext": "4111111111111111"
}Response:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-commitment-v1",
"algorithm": "KMAC-256",
"commitment": "hex...",
"opening": "base64url..."
}Requires auth and commit-verify permission for the request body KID. Verify
allows active or retired keys. A well-formed but incorrect opening or
commitment returns valid: false; malformed opening or non-hex commitment
returns 400.
Request:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-commitment-v1",
"plaintext": "4111111111111111",
"opening": "base64url...",
"commitment": "hex..."
}Response:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-commitment-v1",
"valid": true
}Requires auth and commit-create permission for the path KID. The batch uses
one profile and is all-or-nothing. items must contain between 1 and 128
entries, and every ref must be unique.
Request:
{
"profile": "pan-commitment-v1",
"items": [
{ "ref": "row1", "plaintext": "4111111111111111" },
{ "ref": "row2", "plaintext": "5555555555554444" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-commitment-v1",
"algorithm": "KMAC-256",
"items": [
{ "ref": "row1", "commitment": "hex...", "opening": "base64url..." },
{ "ref": "row2", "commitment": "hex...", "opening": "base64url..." }
]
}Requires auth and commit-verify permission for the request body KID. Request
validation errors fail the whole batch; well-formed mismatches return
valid: false per item.
Request:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-commitment-v1",
"items": [
{ "ref": "row1", "plaintext": "4111111111111111", "opening": "base64url...", "commitment": "hex..." },
{ "ref": "row2", "plaintext": "5555555555554444", "opening": "base64url...", "commitment": "hex..." }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-commitment-v1",
"items": [
{ "ref": "row1", "valid": true },
{ "ref": "row2", "valid": false }
]
}Masking is a local display-only transform. It does not encrypt, tokenize,
persist, or derive keys. A signed masking_profiles[] entry controls how many
characters are visible at the start and end of the value, and which single
character masks the middle. The profile KID is used for permission and lifecycle
checks.
Requires auth and mask permission for the path KID. The key must be active or
retired.
Request:
{ "ref": "row1", "profile": "pan-display-v1", "plaintext": "4111111111111111" }Response:
{ "ref": "row1", "kid": "...", "profile": "pan-display-v1", "masked": "************1111" }Uses one profile for every item. Each item requires a unique ref; the batch is
all-or-nothing and is limited to INTERNAL_MASK_BATCH (128).
Request:
{
"profile": "pan-display-v1",
"items": [
{ "ref": "row1", "plaintext": "4111111111111111" }
]
}Response:
{
"kid": "...",
"profile": "pan-display-v1",
"items": [
{ "ref": "row1", "masked": "************1111" }
]
}Blind indexes reuse signed mac_profiles. /mac computes a deterministic
digest; /index computes the same deterministic digest and stores or verifies
membership in the local indexes table. Storage keeps only kid and digest;
it never stores profile, plaintext, metadata, or client ref.
Use a MAC profile context with a clear indexing purpose, for example
tenant=mx;field=pan;purpose=index;version=1.
Requires auth and index-create permission for the path KID. Create requires
an active key. Re-creating the same index is idempotent.
Request:
{
"ref": "reg1",
"profile": "pan-index-v1",
"plaintext": "4111111111111111"
}Response:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-index-v1",
"index": "hex..."
}Requires auth and index-verify permission for the request body KID. Verify allows
active or retired keys.
Request:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-index-v1",
"plaintext": "4111111111111111"
}Response:
{
"ref": "reg1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-index-v1",
"matched": true,
"index": "hex..."
}Requires auth and index-create permission for the path KID. The batch uses
one profile and is all-or-nothing. items must contain between 1 and 128
entries, and every ref must be unique. The storage write is transactional.
Request:
{
"profile": "pan-index-v1",
"items": [
{ "ref": "row1", "plaintext": "4111111111111111" },
{ "ref": "row2", "plaintext": "5555555555554444" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-index-v1",
"items": [
{ "ref": "row1", "index": "hex..." },
{ "ref": "row2", "index": "hex..." }
]
}Requires auth and index-verify permission for the request body KID. Request errors
fail the whole batch; a missing index returns matched: false for that item.
Request:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-index-v1",
"items": [
{ "ref": "row1", "plaintext": "4111111111111111" },
{ "ref": "row2", "plaintext": "5555555555554444" }
]
}Response:
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"profile": "pan-index-v1",
"items": [
{ "ref": "row1", "matched": true, "index": "hex..." },
{ "ref": "row2", "matched": false, "index": "hex..." }
]
}When POST /message receives and validates a protected message, it delivers this JSON to the final app:
{
"sender_host": "127.0.0.1:3000",
"sender_kid": "...",
"timestamp": "1782058090",
"message": {
"ctx": "...",
"nonce": "...",
"aad": "...",
"variant": "AES-256/GCM"
}
}The final app can call POST /message/decrypt to recover the plaintext.
Secret sharing uses signed sharing_profiles and emits self-contained,
authenticated vectis-sss-v1 envelopes. Vectis keeps no shares or plaintext in
storage. Each split creates a public random set_id; combine accepts any
threshold-sized subset from that set. Share tags bind the profile, KID,
threshold, total shares, set ID, index, and payload before interpolation.
POST /shares/split/{kid} requires share-split permission and an active KID:
{"profile":"customer-secret-3of5-v1","plaintext":"secret-value"}It returns kid, profile, threshold, set_id, and shares. POST /shares/combine requires share-combine permission and accepts the KID in the
request body:
{"kid":"<kid>","profile":"customer-secret-3of5-v1","shares":["vectis-sss-v1.<share>"]}Combine permits active or retired KIDs. It rejects malformed, altered, mixed, duplicate-index, or insufficient shares. There are no batch endpoints in v1.
Vectis loads a single signed config file (config.json) with routes, remote_routes, permissions, optional profile arrays, and an optional time_attestation singleton plus a top-level version. It is loaded when vectis serve starts and can be reloaded at runtime with POST /config/reload. Create/update its signature with vectis config sign; inspect it with vectis config list.
time_attestation is optional. provider is a configuration name (non-empty,
without control characters, maximum 128 characters); v1 supports only
cloudflare. Missing fields use compiled Cloudflare defaults.
It may override provider, nts_server, roughtime_server,
roughtime_public_key, max_clock_skew_ms, max_round_trip_ms, and
max_roughtime_radius_ms. The Roughtime public key is a signed trust anchor.
{
"time_attestation": {
"provider": "cloudflare",
"nts_server": "time.cloudflare.com",
"roughtime_server": "roughtime.cloudflare.com:2003",
"roughtime_public_key": "0GD7c3yP8xEc4Zl2zeuN2SlLvDVVocjsPSL8/Rl/7zg=",
"max_clock_skew_ms": 1000,
"max_round_trip_ms": 2000,
"max_roughtime_radius_ms": 2000
}
}Default paths:
VECTIS_CONFIG_PATH=config.json
VECTIS_CONFIG_SIGN_PATH=config_sign.jsonSize limits:
config.json: 8 MiB maximum.config_sign.json: 1 MiB maximum.
Vectis checks these limits before parsing, canonicalizing, signing, or verifying config material.
Expected file shape:
{
"version": "v1",
"routes": [
{
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"name": "clinical-app-a",
"final_app_addr": "127.0.0.1:3999",
"final_app_path": "/message"
}
],
"remote_routes": [
{
"remote_kid": "b01cbe33187916f0f1367d07bc986d71bb0d91d7047ccd790c13fc9d85fe7259",
"name": "site-b",
"remote_addr": "vectis-b.example.com:443",
"allowed_local_kids": ["*"],
"status": "active",
"public_keys": {
"eddsa": { "alg": "Ed25519", "public_key_der_hex": "3043..." },
"xecdh": { "alg": "X25519", "public_key_hex": "a1b2..." },
"ml-dsa": { "alg": "ML-DSA-44", "public_key_der_hex": "3082..." },
"ml-kem": { "alg": "ML-KEM-512", "public_key_der_hex": "3082..." }
}
}
],
"permissions": [
{
"client": "clinic-app",
"apikey_hash": "f80e3d53ecb4c086f6a4f76792df30fe70fcf383c8aaff09bce65340a9360e3e",
"status": "active",
"permissions": [{ "kid": "f55f086e...", "actions": ["message"] }]
}
],
"fpe_profiles": [
{
"name": "patient-id-decimal-v1",
"fpe_version": "fpe-ff1-2025",
"alphabet": "0123456789",
"min_len": 6,
"max_len": 32,
"tweak_aad": "tenant=acme;field=patient_id;version=1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed"
}
],
"tokenization_profiles": [
{
"name": "patient-id-token-v1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"token_prefix": "tok_patient",
"token_len": 32,
"max_plaintext_len": 1024,
"one_time": false
}
],
"mac_profiles": [
{
"name": "pan-blind-index-v1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"context": "tenant=mx;field=pan;purpose=blind-index;version=1"
}
],
"commitment_profiles": [
{
"name": "pan-commitment-v1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"context": "tenant=mx;field=pan;purpose=commitment;version=1",
"max_plaintext_len": 128,
"opening_len": 32
}
],
"sharing_profiles": [
{
"name": "customer-secret-3of5-v1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"threshold": 3,
"shares": 5,
"max_secret_len": 4096,
"context": "tenant=acme;purpose=customer-secret-sharing;version=1"
}
],
"masking_profiles": [
{
"name": "pan-display-v1",
"kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
"visible_first": 0,
"visible_last": 4,
"mask_char": "*",
"min_len": 12,
"max_len": 19
}
]
}Top level:
| Field | Required | Value | Meaning |
|---|---|---|---|
version |
yes | "v1" |
Config schema version; unknown versions are rejected. |
routes |
yes (may be []) |
array | Final app delivery routes per local kid. |
remote_routes |
yes (may be []) |
array | Authorized remote Vectis peers. |
permissions |
yes (may be []) |
array | Non-root API key clients and their allowed actions. |
fpe_profiles |
no (defaults to []) |
array | Signed local FPE field profiles. |
tokenization_profiles |
no (defaults to []) |
array | Signed local reversible tokenization profiles. |
mac_profiles |
no (defaults to []) |
array | Signed local MAC profiles. |
commitment_profiles |
no (defaults to []) |
array | Signed local keyed commitment profiles. |
sharing_profiles |
no (defaults to []) |
array | Signed local authenticated Shamir secret sharing profiles. |
masking_profiles |
no (defaults to []) |
array | Signed local display masking profiles. |
routes[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
kid |
yes | 64-hex kid, must be a loaded local key | Local operational key this route applies to. |
name |
yes | non-empty text | Human-readable route name. Metadata only. |
final_app_addr |
yes | host:port |
Where to deliver decrypted messages for this kid. |
final_app_path |
yes | path starting with / |
Delivery path (e.g. /message). |
remote_routes[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
remote_kid |
yes | 64-hex kid (the remote peer's key) | Identifies the remote peer. |
name |
yes | text | Human label. |
remote_addr |
yes | host:port |
Address of the remote Vectis instance. |
allowed_local_kids |
yes | non-empty array; ["*"] or explicit loaded kids (no mixing) |
Which local sender KIDs may use this route. |
status |
yes | active | disabled |
Disabled routes load but cannot send. |
public_keys |
no | object (see below) | Peer's trusted public keys. Required for /message send/receive with that peer and for cross-instance verification; entries without it are routing metadata only. |
public_keys object (optional; the keys object from the peer's GET /pub/{kid}): eddsa/ml-dsa/ml-kem each { "alg", "public_key_der_hex" }, and xecdh { "alg", "public_key_hex" }. When present, Vectis validates the material before loading it: DER public keys must be loadable by Botan, X25519/X448 raw public keys must be exactly 32/56 bytes, and ML-KEM public keys must be loadable and usable for encapsulation.
permissions[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
client |
yes | text, unique | Client label. |
apikey_hash |
yes | 64 hex (32 bytes) | Server-side verifier for this client's X-API-Key. |
status |
yes | active | disabled | revoked |
Only active clients are authorized. |
permissions |
yes | array of { "kid", "actions" } |
Per-kid grants. actions ⊆ admin, keys, lifecycle, self-test, sign, message, fpe-encrypt, fpe-decrypt, token-encode, token-decode, mac-create, mac-verify, commit-create, commit-verify, share-split, share-combine, index-create, index-verify, mask, metrics, time-attest. kid: "*" is required for global actions admin, metrics, and time-attest; crypto profile actions require explicit KIDs. An admin action grants all endpoints and ignores kid-scoped grants. |
fpe_profiles[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
name |
yes | unique non-empty text | Profile selected by FPE requests. |
fpe_version |
yes | fpe-ff1-2025 |
FF1 profile version and HKDF binding value. |
alphabet |
yes | unique characters, no control chars | Domain alphabet for plaintext and ciphertext. |
min_len |
yes | integer >= 6 | Minimum accepted field length. |
max_len |
yes | integer >= min_len |
Maximum accepted field length. |
tweak_aad |
yes | key=value;key=value, max 128 chars |
Literal cryptographic tweak context from signed config. Keys must be unique and use [A-Za-z0-9_.-]+. |
kid |
yes | loaded local KID | Operational key whose symmetric key derives the FPE key. |
tokenization_profiles[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
name |
yes | unique non-empty text | Profile selected by tokenization requests. |
kid |
yes | loaded local KID | Operational key whose symmetric key derives tokenization keys. |
token_prefix |
yes | non-empty visible token prefix, max 16 chars, no whitespace/control chars, no ; or = |
Prefix used in returned tokens. |
token_len |
yes | integer >= 32 | Random bytes generated before base64url-no-pad encoding; decode requires this exact decoded byte length. |
max_plaintext_len |
yes | integer 1..1024 | Maximum plaintext length accepted by encode. |
one_time |
yes | boolean | When true, a successful decode consumes the token. The signed profile currently loaded by Vectis controls this policy. |
mac_profiles[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
name |
yes | unique AAD-safe text, max 128 chars | Profile selected by MAC requests. |
kid |
yes | loaded local KID | Operational key whose symmetric key derives the MAC key. |
context |
yes | key=value;key=value, max 128 chars |
Signed MAC domain context. Keys must be unique and use [A-Za-z0-9_.-]+. |
commitment_profiles[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
name |
yes | unique AAD-safe text, max 128 chars | Profile selected by commitment requests. |
kid |
yes | loaded local KID | Operational key whose symmetric key derives the commitment key. |
context |
yes | key=value;key=value, max 128 chars |
Signed commitment domain context. Keys must be unique and use [A-Za-z0-9_.-]+. |
max_plaintext_len |
yes | integer 1..1024 | Maximum plaintext length accepted by commitment create/verify. |
opening_len |
yes | integer 32..64 | Random opening bytes generated before base64url-no-pad encoding. |
sharing_profiles[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
name |
yes | unique AAD-safe text, max 128 chars | Profile selected by sharing requests. |
kid |
yes | loaded local KID | Operational key whose symmetric key derives share authentication material. |
threshold |
yes | integer 2..=shares |
Minimum shares required for reconstruction. |
shares |
yes | integer threshold..32 |
Total shares emitted by split. |
max_secret_len |
yes | integer 1..4096 | Maximum UTF-8 plaintext length in bytes. |
context |
yes | key=value;key=value, max 128 chars |
Signed sharing domain context. |
masking_profiles[] entries:
| Field | Required | Value | Meaning |
|---|---|---|---|
name |
yes | unique AAD-safe text, max 128 chars | Profile selected by masking requests. |
kid |
yes | loaded local KID | Operational key used for permission and lifecycle checks. |
visible_first |
yes | integer >= 0 | Number of leading characters to show. |
visible_last |
yes | integer >= 0 | Number of trailing characters to show. |
mask_char |
yes | exactly one non-control character | Character used to mask the middle. |
min_len |
yes | integer >= 1 | Minimum plaintext length accepted. |
max_len |
yes | integer >= min_len, <= 1024 |
Maximum plaintext length accepted. |
Routing behavior:
- Resolve
recipient_kidin the in-memory routes state. - If a route exists, deliver to that route's
final_app_addrandfinal_app_path. - If no route exists for the
kid, deliver to the defaultVECTIS_FINAL_APP_ADDRandVECTIS_FINAL_APP_PATH. - A manual route is loaded only if its
kidexists in the keys currently loaded in memory. - During startup, a missing config starts with empty sections and uses the default final app fallback.
- During startup, an existing invalid config, bad signature, or section referencing an unloaded
kidis fatal. - During reload, a missing config reloads to empty sections; an invalid config, corrupt signature, or a section referencing an unloaded
kidreturns an error and keeps the previous in-memory config. Ifconfig.jsonchanged butconfig_sign.jsonsigns older content, reload returnsstatus: "reloaded"with a warning and keeps the previous in-memory config.
The config file is operational configuration. Vectis does not create it automatically and POST /keys does not modify it.
POST /message/{sender_kid} never accepts a destination host from the request body; it resolves recipient_kid through the signed remote_routes section. A route can allow specific local sender KIDs, or allowed_local_kids: ["*"] for any loaded local KID. The wildcard cannot be mixed with explicit KIDs. Disabled routes are loaded and listed, but cannot be used to send messages.
Each remote_routes entry may carry an optional public_keys object — the full public key set of that peer, exactly as returned by the remote's GET /pub/{kid}. It is trusted because the operator signs the config, and it is the only source of peer public keys: Vectis never fetches keys from a remote /pub endpoint at runtime.
POST /message/{sender_kid}requires the recipient route to carrypublic_keys; a route without them is routing-only and sending returns403.POST /messagerequires the senderkidto match an activeremote_routesentry withpublic_keys; messages from unregistered senders return403.POST /sign/verificationcan verify timestamp tokens whose signerkidis not local, resolving the signer's public keys from the matching activeremote_routesentry.- Invalid or non-operational public key material rejects config load/reload. Startup fails if the config exists and is invalid; runtime reload keeps the previous in-memory config.
Main variables:
VECTIS_PUBLIC_ADDR: public address used assender.hostin protected messages.VECTIS_MODE: central transport mode.devuses HTTP everywhere;produses HTTPS for the local server, Vectis-to-Vectis requests, and final app delivery.VECTIS_TLS_CERT_PATH,VECTIS_TLS_KEY_PATH: PEM certificate and private key required whenVECTIS_MODE=prod.VECTIS_TLS_SKIP_VERIFY: disables outbound HTTPS certificate verification.VECTIS_FINAL_APP_ADDR: final app host:port.VECTIS_FINAL_APP_PATH: final app delivery path.VECTIS_CONFIG_PATH: unified signed config file path (routes, remote routes, permissions), relative to the working directory unless absolute.VECTIS_CONFIG_SIGN_PATH: signature token forVECTIS_CONFIG_PATH, created byvectis config sign.VECTIS_APIKEY: client-side HTTP auth key sent asX-API-Key.VECTIS_APIKEY_HASH: server-side HMAC value used to verifyX-API-Keywithout storing the API key in plaintext.VECTIS_STORAGE: storage backend,sqliteorpostgres.VECTIS_SQLITE_PATH: SQLite storage path.VECTIS_POSTGRES_DSN: PostgreSQL connection string used whenVECTIS_STORAGE=postgres.VECTIS_DEFAULT_CRYPTO_PROFILE: default crypto profile forPOST /keys.VECTIS_CRYPTO_POLICY: controls whetherPOST /keysaccepts request-level algorithm overrides.VECTIS_HASH,VECTIS_SYMMETRIC,VECTIS_EDDSA,VECTIS_XECDH,VECTIS_ML_DSA_VARIANT,VECTIS_ML_KEM_VARIANT: validated legacy/dev algorithm settings; profiles are the source of key-generation defaults.VECTIS_LOG_LEVEL,VECTIS_LOG_DIR,VECTIS_LOG_FILE: operational logging configuration.VECTIS_AUDIT_LOG_FILE: append-only local audit JSONL stream containing hash-chained security records and hybrid-signed checkpoints. Verify it withvectis audit verify --file <path>usingVECTIS_INIT_PUBLIC_KEYS_FILE; preserve checkpoints and the public verification file outside the node to detect complete replacement or truncation.
Internal defaults for init key material:
INTERNAL_KEYS_HASH:BLAKE2b(256)INTERNAL_KEYS_HKDF:HKDF(BLAKE2b(256))INTERNAL_KEYS_HMAC:HMAC(BLAKE2b(256))INTERNAL_KEYS_EDDSA_ALGORITHM:Ed25519INTERNAL_KEYS_XECDH_ALGORITHM:X25519INTERNAL_KEYS_ML_DSA_VARIANT:ML-DSA-44INTERNAL_KEYS_ML_KEM_VARIANT:ML-KEM-512
Runtime CLI commands call the HTTP API:
CLI output defaults to YAML for readability. Add --output json to HTTP client commands and to vectis apikey create to print pretty JSON instead. This does not apply to vectis init.
| CLI command | HTTP operation | Auth |
|---|---|---|
vectis apikey create |
Local API key generation | No HTTP |
vectis health startup |
GET /healthz/startup |
No |
vectis health live |
GET /healthz/live |
No |
vectis health ready |
GET /healthz/ready |
No |
vectis test init |
GET /self-test/init |
Yes |
vectis test <kid> |
GET /self-test/keys/{kid} |
Yes |
vectis keys create |
POST /keys |
Yes |
vectis keys list |
GET /keys |
No |
vectis keys properties |
GET /keys/properties |
Yes |
vectis keys properties <kid> |
GET /keys/properties/{kid} |
Yes |
vectis keys reload |
POST /keys/reload |
Yes |
vectis lifecycle <kid> |
POST /lifecycle/{kid} |
Yes |
vectis routes list |
GET /routes |
Yes |
vectis remote-routes list |
GET /remote-routes |
Yes |
vectis permissions list |
GET /permissions |
Yes |
vectis config sign |
Local config_sign.json update |
No HTTP |
vectis config list |
Prints local config.json |
No HTTP |
vectis config routes ... |
Lists or edits local config.json routes |
No HTTP |
vectis config remote-routes ... |
Lists or edits local config.json remote routes; add and identity/address updates fetch peer public keys from /pub/{kid} |
No HTTP for most local edits; remote /pub for key import |
vectis config permissions ... |
Lists or edits local config.json permissions |
No HTTP |
vectis config fpe ... |
Lists or edits local config.json FPE profiles |
No HTTP |
vectis config reload |
POST /config/reload |
Yes |
vectis pub <kid> |
GET /pub/{kid} |
No |
vectis sign <kid> |
POST /sign/{kid} |
Yes |
vectis sign verify |
POST /sign/verification |
No |
vectis message send <sender_kid> |
POST /message/{sender_kid} |
Yes |
vectis message receive |
POST /message |
No |
vectis message decrypt |
POST /message/decrypt |
Yes |
vectis message internal encrypt <kid> |
POST /message/internal/encrypt/{kid} |
Yes |
vectis message internal decrypt |
POST /message/internal/decrypt |
Yes |
Local commands:
vectis init: creates encryptedVECTIS_INIT_KEYS_FILE, defaultinit.json, printsVECTIS_UNSEAL_KEY,VECTIS_APIKEY, andVECTIS_APIKEY_HASH. It refuses to overwrite an existing init keys file; delete it manually before reinitializing.vectis apikey create: decryptsVECTIS_INIT_KEYS_FILE, derives the internal API auth key, prints a newVECTIS_APIKEYand matchingVECTIS_APIKEY_HASH, and does not write files.vectis serve: validatesVECTIS_INIT_KEYS_FILE, loads storage/config into memory, and starts the HTTP service. Unseal key resolution order isVECTIS_UNSEAL_KEY,VECTIS_UNSEAL_KEY_FILEwith default.unseal_key, then hidden prompt.vectis config sign: readsVECTIS_CONFIG_PATH, signs its canonical JSON with init EdDSA and init ML-DSA, and writesVECTIS_CONFIG_SIGN_PATH.vectis config list: printsVECTIS_CONFIG_PATHlocally.
When config.json exists, Vectis requires a matching config signature before loading it.
Default paths:
VECTIS_CONFIG_PATH=config.json
VECTIS_CONFIG_SIGN_PATH=config_sign.jsonCreate or update the signature:
vectis config signconfig_sign.json stores a Vectis compact hybrid signature wrapper:
{
"signature": "base64url(header).base64url(payload).base64url(eddsa).base64url(ml-dsa)"
}The canonical header is {"version":"vectis-signature-v1"}. The canonical
payload contains version, type, created_at, info, kid, serial, and
message_hash; it remains separate from the timestamp token format returned by
POST /sign/{kid}.
Validation rules:
payload.typemust bevectis-config.payload.kidmust beinit-keys.payload.infomust beversion=v1;type=vectis-config.payload.message_hashmust match canonicalconfig.jsonusingINTERNAL_KEYS_HASH.- ML-DSA and then EdDSA must verify over the exact encoded
header.payloadbytes with init public keys before Vectis parses the signed header or payload. - The compact signature uses four non-empty base64url-without-padding segments and is limited to 64 KiB.
- The signature is not bound to the local filesystem path. Moving
config.jsonandconfig_sign.jsontogether is supported. - Older JSON timestamp-token envelopes are not accepted; run
vectis config signafter upgrading. - Startup with missing
config.jsonuses empty sections (default routing, only root authorized). - Startup with invalid existing config or invalid config signature fails.
- A reload endpoint rejects invalid signatures and keeps the previous in-memory config.