Skip to content

Latest commit

 

History

History
2229 lines (1770 loc) · 69.3 KB

File metadata and controls

2229 lines (1770 loc) · 69.3 KB

Vectis HTTP API

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.

Conventions

  • 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,152 bytes) by the internal INTERNAL_HTTP_MAX_SIZE policy. Larger bodies are rejected before authentication, authorization, storage, or cryptographic processing.
  • Timestamps are encoded as Unix epoch seconds in a string.
  • kid values are hex strings derived with Vectis' internal hash (INTERNAL_KEYS_HASH, currently BLAKE2b(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"
}

Protocol Versioning

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 the payload before 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.

Authentication

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 /keys
  • GET /keys/properties
  • GET /keys/properties/{kid}
  • POST /keys/reload
  • POST /lifecycle/{kid}
  • POST /config/reload
  • GET /routes
  • GET /remote-routes
  • GET /permissions
  • GET /metrics
  • GET /self-test/init
  • GET /self-test/keys/{kid}
  • POST /sign/{kid}
  • POST /message/{sender_kid}
  • POST /message/decrypt
  • POST /message/internal/encrypt/{kid}
  • POST /message/internal/decrypt
  • POST /fpe/encrypt/{kid}
  • POST /fpe/encrypt/batch/{kid}
  • POST /fpe/decrypt
  • POST /fpe/decrypt/batch
  • POST /token/encode/{kid}
  • POST /token/encode/batch/{kid}
  • POST /token/decode
  • POST /token/decode/batch
  • POST /mac/{kid}
  • POST /mac/batch/{kid}
  • POST /mac/verify
  • POST /mac/verify/batch
  • POST /commit/{kid}
  • POST /commit/batch/{kid}
  • POST /commit/verify
  • POST /commit/verify/batch
  • POST /shares/split/{kid}
  • POST /shares/combine
  • POST /index/{kid}
  • POST /index/batch/{kid}
  • POST /index/verify
  • POST /index/verify/batch
  • POST /mask/{kid}
  • POST /mask/batch/{kid}
  • POST /time/attest

Endpoints without auth:

  • GET /healthz/startup
  • GET /healthz/live
  • GET /healthz/ready
  • GET /keys
  • GET /pub/{kid}
  • POST /sign/verification
  • POST /message

Supported Algorithms

Hash:

  • BLAKE2b(160), BLAKE2b(224), BLAKE2b(256), BLAKE2b(384), BLAKE2b(512)
  • SHA-224, SHA-256, SHA-384, SHA-512, SHA-512-256
  • SHA-3(224), SHA-3(256), SHA-3(384), SHA-3(512)
  • Whirlpool

Symmetric:

  • ChaCha20Poly1305
  • AES-128/GCM
  • AES-192/GCM
  • AES-256/GCM

EdDSA:

  • Ed25519
  • Ed448

XECDH:

  • X25519
  • X448

ML-DSA:

  • ML-DSA-44
  • ML-DSA-65
  • ML-DSA-87

ML-KEM:

  • ML-KEM-512
  • ML-KEM-768
  • ML-KEM-1024

Health and Validation

GET /healthz/startup

Startup probe. Reports when the HTTP service state was initialized.

Response:

{
  "status": "started",
  "timestamp": "1782058090"
}

GET /healthz/live

Liveness probe. Does not perform I/O.

Response:

{
  "status": "ok"
}

GET /healthz/ready

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
}

POST /time/attest

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.

GET /metrics

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} (allow or deny)
  • vectis_unsealed
  • vectis_keys_loaded
  • vectis_routes_loaded
  • vectis_remote_routes_loaded
  • vectis_permission_clients
  • vectis_fpe_profiles_loaded
  • vectis_tokenization_profiles_loaded
  • vectis_mac_profiles_loaded
  • vectis_masking_profiles_loaded
  • vectis_commitment_profiles_loaded
  • vectis_sharing_profiles_loaded
  • vectis_permission_total{result} (allow or deny)
  • vectis_config_reload_total{result} (success, stale, or failed)
  • vectis_config_last_reload_timestamp_seconds{result} (success, stale, or failed)
  • vectis_keys_reload_total{result} (success or failed)
  • vectis_message_total{operation,result} (send, receive, or decrypt; success, denied, or failed)
  • 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, or mask_batch; success or failed)

GET /self-test/init

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
  }
}

GET /self-test/keys/{kid}

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
  }
}

Keys

POST /keys

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-512
  • hybrid-standard-v1: SHA-3(256), AES-128/GCM, Ed25519, X25519, ML-DSA-44, ML-KEM-512
  • hybrid-high-assurance-v1: SHA-3(384), AES-192/GCM, Ed25519, X25519, ML-DSA-65, ML-KEM-768
  • hybrid-long-term-v1: SHA-3(512), AES-256/GCM, Ed448, X448, ML-DSA-87, ML-KEM-1024

Response:

{
  "kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed"
}

GET /keys

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"
    }
  ]
}

POST /keys/reload

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
      }
    }
  ]
}

GET /keys/properties

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
      }
    }
  ]
}

GET /keys/properties/{kid}

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
  }
}

POST /lifecycle/{kid}

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:

  • active
  • disabled
  • retired
  • compromised
  • destroyed

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, destroyed
  • disabled -> active
  • retired -> no transitions
  • compromised -> no transitions
  • destroyed -> no transitions

Transitions to the same status are rejected.

Response:

{
  "kid": "f55f086e75b58ac4dfaffd3e75c90d25719281df90e87880145fb9f2e32f2eed",
  "lifecycle": {
    "status": "disabled",
    "reason": "maintenance window",
    "changed_at": "1782059000"
  }
}

Config

POST /config/reload

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.

Routes

GET /routes

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"
    }
  ]
}

GET /remote-routes

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"
    }
  ]
}

API Key Permissions

Permissions are the permissions section of the unified signed config file:

VECTIS_CONFIG_PATH=config.json
VECTIS_CONFIG_SIGN_PATH=config_sign.json

vectis 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:

  • admin
  • keys
  • lifecycle
  • self-test
  • sign
  • message
  • fpe-encrypt
  • fpe-decrypt
  • token-encode
  • token-decode
  • mac-create
  • mac-verify
  • index-create
  • index-verify
  • mask
  • metrics
  • time-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"]
        }
      ]
    }
  ]
}

GET /permissions

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.

Public Keys

GET /pub/{kid}

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": "..."
    }
  }
}

Hybrid Timestamp Protocol

POST /sign/{kid}

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.

POST /sign/verification

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.

Protected Messages Between Instances

POST /message/{sender_kid}

Sends a protected message from this Vectis instance to another Vectis instance.

Requires auth.

Request:

{
  "recipient_kid": "b01cbe33187916f0f1367d07bc986d71bb0d91d7047ccd790c13fc9d85fe7259",
  "message": "hello vectis"
}

Flow:

  1. Validate sender_kid, recipient_kid, and message.
  2. Resolve recipient_kid through the signed config remote_routes section.
  3. Load the recipient's public keys from the public_keys of the resolved remote_routes entry. The signed config is the only source of peer public keys; Vectis never fetches /pub at runtime.
  4. Create a hybrid secret with XECDH + ML-KEM.
  5. Derive message_key with HKDF.
  6. Encrypt the message.
  7. Sign the payload with EdDSA and ML-DSA.
  8. Send the envelope to POST /message on 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"
}

POST /message

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.

POST /message/decrypt

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"
}

Internal Messages

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.

POST /message/internal/encrypt/{kid}

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"
  }
}

POST /message/internal/decrypt

Requires auth.

Request: the JSON returned by POST /message/internal/encrypt/{kid}.

Response:

{
  "plaintext": "hello vectis"
}

Format-Preserving Encryption

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
)

POST /fpe/encrypt/{kid}

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.

POST /fpe/encrypt/batch/{kid}

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" }
  ]
}

POST /fpe/decrypt

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"
}

POST /fpe/decrypt/batch

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

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).

POST /token/encode/{kid}

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"
}

POST /token/decode

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" }
}

POST /token/encode/batch/{kid}

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" }
  ]
}

POST /token/decode/batch

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

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.

POST /mac/{kid}

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..."
}

POST /mac/verify

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
}

POST /mac/batch/{kid}

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..." }
  ]
}

POST /mac/verify/batch

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 }
  ]
}

Cryptographic Commitments

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.

POST /commit/{kid}

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..."
}

POST /commit/verify

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
}

POST /commit/batch/{kid}

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..." }
  ]
}

POST /commit/verify/batch

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

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.

POST /mask/{kid}

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" }

POST /mask/batch/{kid}

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 Index

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.

POST /index/{kid}

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..."
}

POST /index/verify

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..."
}

POST /index/batch/{kid}

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..." }
  ]
}

POST /index/verify/batch

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..." }
  ]
}

Final App Delivery

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

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.

Configuration File (config.json)

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.json

Size 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. actionsadmin, 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:

  1. Resolve recipient_kid in the in-memory routes state.
  2. If a route exists, deliver to that route's final_app_addr and final_app_path.
  3. If no route exists for the kid, deliver to the default VECTIS_FINAL_APP_ADDR and VECTIS_FINAL_APP_PATH.
  4. A manual route is loaded only if its kid exists in the keys currently loaded in memory.
  5. During startup, a missing config starts with empty sections and uses the default final app fallback.
  6. During startup, an existing invalid config, bad signature, or section referencing an unloaded kid is fatal.
  7. During reload, a missing config reloads to empty sections; an invalid config, corrupt signature, or a section referencing an unloaded kid returns an error and keeps the previous in-memory config. If config.json changed but config_sign.json signs older content, reload returns status: "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 carry public_keys; a route without them is routing-only and sending returns 403.
  • POST /message requires the sender kid to match an active remote_routes entry with public_keys; messages from unregistered senders return 403.
  • POST /sign/verification can verify timestamp tokens whose signer kid is not local, resolving the signer's public keys from the matching active remote_routes entry.
  • 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.

Related Configuration

Main variables:

  • VECTIS_PUBLIC_ADDR: public address used as sender.host in protected messages.
  • VECTIS_MODE: central transport mode. dev uses HTTP everywhere; prod uses 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 when VECTIS_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 for VECTIS_CONFIG_PATH, created by vectis config sign.
  • VECTIS_APIKEY: client-side HTTP auth key sent as X-API-Key.
  • VECTIS_APIKEY_HASH: server-side HMAC value used to verify X-API-Key without storing the API key in plaintext.
  • VECTIS_STORAGE: storage backend, sqlite or postgres.
  • VECTIS_SQLITE_PATH: SQLite storage path.
  • VECTIS_POSTGRES_DSN: PostgreSQL connection string used when VECTIS_STORAGE=postgres.
  • VECTIS_DEFAULT_CRYPTO_PROFILE: default crypto profile for POST /keys.
  • VECTIS_CRYPTO_POLICY: controls whether POST /keys accepts 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 with vectis audit verify --file <path> using VECTIS_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: Ed25519
  • INTERNAL_KEYS_XECDH_ALGORITHM: X25519
  • INTERNAL_KEYS_ML_DSA_VARIANT: ML-DSA-44
  • INTERNAL_KEYS_ML_KEM_VARIANT: ML-KEM-512

CLI Mapping

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 encrypted VECTIS_INIT_KEYS_FILE, default init.json, prints VECTIS_UNSEAL_KEY, VECTIS_APIKEY, and VECTIS_APIKEY_HASH. It refuses to overwrite an existing init keys file; delete it manually before reinitializing.
  • vectis apikey create: decrypts VECTIS_INIT_KEYS_FILE, derives the internal API auth key, prints a new VECTIS_APIKEY and matching VECTIS_APIKEY_HASH, and does not write files.
  • vectis serve: validates VECTIS_INIT_KEYS_FILE, loads storage/config into memory, and starts the HTTP service. Unseal key resolution order is VECTIS_UNSEAL_KEY, VECTIS_UNSEAL_KEY_FILE with default .unseal_key, then hidden prompt.
  • vectis config sign: reads VECTIS_CONFIG_PATH, signs its canonical JSON with init EdDSA and init ML-DSA, and writes VECTIS_CONFIG_SIGN_PATH.
  • vectis config list: prints VECTIS_CONFIG_PATH locally.

Config Signature

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.json

Create or update the signature:

vectis config sign

config_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.type must be vectis-config.
  • payload.kid must be init-keys.
  • payload.info must be version=v1;type=vectis-config.
  • payload.message_hash must match canonical config.json using INTERNAL_KEYS_HASH.
  • ML-DSA and then EdDSA must verify over the exact encoded header.payload bytes 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.json and config_sign.json together is supported.
  • Older JSON timestamp-token envelopes are not accepted; run vectis config sign after upgrading.
  • Startup with missing config.json uses 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.