Skip to content

Latest commit

 

History

History
773 lines (561 loc) · 22.1 KB

File metadata and controls

773 lines (561 loc) · 22.1 KB

API Reference — PARAMANT v0.9.0-beta

Base URLs

Sector URL Compliance
General https://relay.paramant.app
Healthcare https://health.paramant.app NEN 7510, DICOM
Legal https://legal.paramant.app eIDAS, KNB
Finance https://finance.paramant.app NIS2, DORA
IoT https://iot.paramant.app IEC 62443

Authentication

Three credential types are in use across different API surfaces:

Credential Header / Mechanism Used for
API key (pgp_ prefix) X-Api-Key: pgp_your_key Data plane — uploads, downloads, CT log (developer clients)
Operator key (plk_ prefix) X-Api-Key: plk_your_key Data plane — unlimited throughput (operator license)
Session cookie Cookie: paramant_user_session=<token> /api/user/* endpoints — set automatically after TOTP login
Admin token X-Admin-Token: <token> /admin/api/admin/* endpoints — admin panel only

CT log and STH endpoints are public — no credential required.

The /v2/auth/capabilities endpoint is public and returns which authentication modes are enabled on this relay instance.


Data plane

POST /v2/inbound — Upload an encrypted blob

curl -X POST https://relay.paramant.app/v2/inbound \
  -H "X-Api-Key: pgp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"hash":"sha256hex","payload":"base64_5mb_blob","ttl_ms":3600000}'

Response:

{
  "ok": true,
  "hash": "a3f2…",
  "ttl_ms": 3600000,
  "size": 5242880,
  "sig_verified": true,
  "download_token": "6b3c…",
  "merkle_proof": {
    "leaf_hash":  "d4e1…",
    "leaf_index": 42,
    "tree_size":  43,
    "audit_path": [
      {"hash": "8a0b…", "position": "left"},
      {"hash": "f391…", "position": "right"}
    ],
    "root":          "c7a9…",
    "sth":           { "relay_id": "relay.paramant.app", "sha3_root": "c7a9…", "tree_size": 43, "timestamp": 1744123456789, "signature": "" },
    "sth_signature": "ML-DSA-65 base64…"
  }
}

merkle_proof proves the blob was appended to the CT log. Re-walk audit_path from leaf_hash to reproduce root, then verify sth.signature with /v2/pubkey.


GET /v2/outbound/:hash — Download (burn-on-read)

curl https://relay.paramant.app/v2/outbound/a3f2… \
  -H "X-Api-Key: pgp_your_key" \
  --output received.bin

Response headers:

Header Value
X-Paramant-Burned true if blob was destroyed
X-Paramant-Hash SHA-256 hex of the blob
X-Paramant-Receipt Base64url-encoded signed delivery receipt

The X-Paramant-Receipt value is a base64url-encoded JSON object:

{
  "blob_hash":               "a3f2…",
  "sector":                  "health",
  "retrieved_at":            "2026-04-15T09:00:00.000Z",
  "relay_id":                "health.paramant.app",
  "tree_size_at_retrieval":  43,
  "inclusion_proof":         { "leaf_hash": "d4e1…", "audit_path": [], "root": "c7a9…" },
  "burn_confirmed":          true,
  "signature":               "ML-DSA-65 base64…"
}

Pass this to POST /v2/verify-receipt to cryptographically confirm delivery.


POST /v2/verify-receipt — Verify a delivery receipt

Public. No API key required.

curl -X POST https://relay.paramant.app/v2/verify-receipt \
  -H "Content-Type: application/json" \
  -d '{"receipt":"<base64url from X-Paramant-Receipt>"}'

Success:

{
  "valid": true,
  "blob_hash": "a3f2…",
  "burn_confirmed": true,
  "tree_size_at_retrieval": 43,
  "retrieved_at": "2026-04-15T09:00:00.000Z"
}

Failure (signature invalid, proof mismatch, missing fields):

{ "valid": false, "reason": "signature_invalid" }
{ "valid": false, "reason": "inclusion_proof_invalid", "detail": "recomputed root 8a0b… ≠ claimed root c7a9…" }

Verification performs two independent checks: ML-DSA-65 signature over the canonical receipt JSON, then re-walks the Merkle audit path to recompute the root.


GET /v2/stream-next — Poll for next pending blob

curl https://relay.paramant.app/v2/stream-next \
  -H "X-Api-Key: pgp_your_key" \
  -H "X-Device-Id: receiver-001"
# 200: {"blob_hash":"a3f2…","queued_at":"2026-04-15T…"}
# 204: no pending blobs

GET /v2/status/:hash — Check blob availability

curl https://relay.paramant.app/v2/status/a3f2… \
  -H "X-Api-Key: pgp_your_key"
# {"available":true,"bytes":5242880,"ttl_remaining_ms":3598012,"sig_valid":true}

Certificate Transparency log

All CT endpoints are public — no API key required.

GET /v2/sth — Latest Signed Tree Head

curl https://relay.paramant.app/v2/sth
{
  "ok": true,
  "sth": {
    "relay_id":   "relay.paramant.app",
    "sha3_root":  "c7a9ef34…",
    "tree_size":  43,
    "timestamp":  1744123456789,
    "version":    1,
    "signature":  "ML-DSA-65 base64…",
    "pk_hash":    "sha3-256 of relay public key"
  }
}

The relay signs {relay_id, sha3_root, timestamp, tree_size, version} (keys sorted, JSON-serialised) using ML-DSA-65. Verify the signature against the key returned by GET /v2/pubkey.


GET /v2/sth/history — STH history

curl "https://relay.paramant.app/v2/sth/history?limit=10"
# {"ok":true,"count":10,"total":48,"sths":[…]}

limit max 100.


GET /v2/sth/:unixms — STH at or after a timestamp

curl https://relay.paramant.app/v2/sth/1744100000000
# {"ok":true,"sth":{…}}   — first STH at or after that Unix millisecond timestamp
# 404 if none exists

GET /v2/pubkey — Relay identity public key

curl https://relay.paramant.app/v2/pubkey
{
  "ok": true,
  "alg": "ML-DSA-65",
  "public_key": "base64…",
  "pk_hash": "sha3-256 hex of the key"
}

Use this key to independently verify any STH signature or delivery receipt signature. The key is generated once at first boot and persisted; pk_hash is its SHA3-256 fingerprint.


GET /v2/ct/log — CT log entries

curl "https://relay.paramant.app/v2/ct/log?limit=20"
# {"ok":true,"entries":[{…}],"tree_size":43,"root":"c7a9…"}

GET /v2/ct/proof — Inclusion proof for a specific index

curl "https://relay.paramant.app/v2/ct/proof?index=7"
# {"ok":true,"leaf_hash":"d4e1…","audit_path":[…],"root":"c7a9…","tree_size":43}

GET /v2/sth/consistency — RFC 6962 consistency proof

Prove that tree at size from is a prefix of tree at size to:

curl "https://relay.paramant.app/v2/sth/consistency?from=20&to=43"
# {"ok":true,"from":20,"to":43,"proof":["hash1","hash2",…]}

to defaults to current tree size if omitted.


Cross-relay gossip

These endpoints power the peer-to-peer STH exchange. They allow any relay (or auditor) to independently archive and verify each other's tree heads.

POST /v2/sth/ingest — Submit a peer STH

curl -X POST https://relay.paramant.app/v2/sth/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "relay_id":  "health.paramant.app",
    "sha3_root": "c7a9…",
    "timestamp": 1744123456789,
    "tree_size": 43,
    "version":   1,
    "signature": "base64…",
    "public_key":"base64…"
  }'
# {"ok":true,"relay_pk_hash":"sha3-256 hex"}

The relay verifies the ML-DSA-65 signature before storing. Replay attacks are blocked by a 5-minute timestamp window.


GET /v2/sth/peers — List mirrored peer relays

curl https://relay.paramant.app/v2/sth/peers
{
  "ok": true,
  "count": 3,
  "peers": [
    {
      "relay_pk_hash":     "a1b2…",
      "relay_id":          "health.paramant.app",
      "sth_count":         12,
      "latest_root":       "c7a9…",
      "latest_tree_size":  43,
      "latest_ts":         "2026-04-15T09:00:00.000Z"
    }
  ]
}

GET /v2/sth/peers/:pk_hash — Full STH history for a specific peer

curl "https://relay.paramant.app/v2/sth/peers/a1b2…?limit=50&offset=0"
# {"ok":true,"relay_pk_hash":"a1b2…","sths":[…],"total":12,"limit":50,"offset":0}

CT log web UI and feeds

Path Description
GET /ct/ Public web UI — live tree view, verify button, no auth
GET /ct/feed JSON feed for the UI (auto-refresh every 10s)
GET /ct/feed.xml RSS feed — last 20 STHs. Subscribe to independently archive roots.

The RSS feed is designed for external archiving: any subscriber retains an independent copy of each signed tree head, making log tampering detectable even if the relay is compromised later.


Other endpoints

GET /health — Relay status (public)

curl https://relay.paramant.app/health
# {"ok":true,"version":"2.4.5","sector":"relay","edition":"community"}

GET /v2/relays — Relay registry (public)

curl https://relay.paramant.app/v2/relays
# {"total":5,"relays":[{"url":"…","version":"2.4.5","sector":"relay",…}]}

POST /v2/request-trial — Request a free trial API key

curl -X POST https://relay.paramant.app/v2/request-trial \
  -H "Content-Type: application/json" \
  -d '{"name":"Jane Smith","email":"jane@example.com","use_case":"DICOM transfer for radiology dept"}'
# {"ok":true,"message":"Trial key sent to jane@example.com"}

Rate limits: 3 requests per IP per 24 hours, 1 request per email address per 7 days. Key is delivered via Resend. Also available via the web form at https://paramant.app/request-key.

POST /v2/pubkey — Register device public keys

curl -X POST https://relay.paramant.app/v2/pubkey \
  -H "X-Api-Key: pgp_your_key" \
  -H "Content-Type: application/json" \
  -d '{"device_id":"phone-001","ecdh_pub":"base64…","kyber_pub":"base64…"}'
# {"ok":true}

GET /v2/pubkey/:device — Fetch a device's public keys

curl https://relay.paramant.app/v2/pubkey/phone-001 \
  -H "X-Api-Key: pgp_your_key"
# {"device_id":"phone-001","ecdh_pub":"…","kyber_pub":"…","registered_at":"…"}

Device Identity

Ghost Pipe supports W3C-compatible decentralised identifiers (did:paramant:) for field devices. Registering a device identity enrolls it in the CT log and allows transfers to be attributed to a specific device without exposing the API key.

POST /v2/did/register — Enroll a device

curl -X POST https://iot.paramant.app/v2/did/register \
  -H "X-Api-Key: plk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id": "plc-factory-01",
    "ecdh_pub":  "<base64 ECDH P-256 uncompressed public key, 65 bytes>",
    "dsa_pub":   "<base64 ML-DSA-65 public key — optional>"
  }'

Response:

{
  "ok": true,
  "did": "did:paramant:a3f2b7c1…",
  "document": {
    "id": "did:paramant:a3f2b7c1…",
    "verificationMethod": [
      {
        "id": "did:paramant:a3f2b7c1…#keys-1",
        "type": "JsonWebKey2020",
        "controller": "did:paramant:a3f2b7c1…",
        "publicKeyHex": "<ecdh_pub>"
      }
    ]
  },
  "ct_index": 42
}

ct_index is the CT log position of this registration — auditors can verify the enrollment timestamp via /v2/ct/proof?index=42.

Limits: max 500 DIDs per API key. Receiver sessions (device_id starting with inv_) do not require an API key.


GET /v2/did/:did — Resolve a DID document

Public endpoint — no API key required.

curl https://iot.paramant.app/v2/did/did:paramant:a3f2b7c1…

Returns the W3C DID document including the device's public key and CT registration index.


GET /v2/did — List enrolled devices

curl https://iot.paramant.app/v2/did \
  -H "X-Api-Key: plk_your_key"
{
  "ok": true,
  "count": 3,
  "dids": [
    { "did": "did:paramant:a3f2…", "device": "plc-factory-01", "ts": "2026-04-01T…" },
    { "did": "did:paramant:b8e1…", "device": "plc-factory-02", "ts": "2026-04-01T…" }
  ]
}

POST /v2/attest — Attest a device

Verify that a device holds the private key corresponding to its registered public key:

curl -X POST https://iot.paramant.app/v2/attest \
  -H "X-Api-Key: plk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "device_id":   "plc-factory-01",
    "attestation": {
      "method":    "ecdh-challenge",
      "challenge": "<base64 challenge bytes>",
      "response":  "<base64 signed response>"
    }
  }'
{ "ok": true, "valid": true, "device": "plc-factory-01" }

Rate limits

Tier Uploads/day Retention
Free (pgp_) 10 1 hour
Community (plk_) unlimited 1 hour
Professional unlimited 24 hours
Enterprise unlimited configurable

Error codes

Code Meaning
400 Bad request — missing or invalid fields
401 Invalid API key or signature
403 Forbidden — wrong API key for this blob
404 No blob / no STH at that timestamp
429 Rate limit exceeded
503 ML-DSA-65 not available on this relay
500 Relay error

Python SDK

pip install paramant-sdk
from paramant_sdk import GhostPipe

gp = GhostPipe(api_key="pgp_xxx", device="device-001", sector="health")

# Send — returns (hash, inclusion_proof)
hash_, proof = gp.send(open("scan.dcm", "rb").read(), ttl=3600)
print(proof["root"])          # Merkle root at time of upload
print(proof["leaf_index"])    # Position in the tree

# Receive — returns (data, receipt)
data, receipt = gp.receive(hash_)
print(receipt["burn_confirmed"])   # True if blob was destroyed
print(receipt["tree_size_at_retrieval"])

# Verify receipt (calls POST /v2/verify-receipt)
result = gp.verify_receipt(receipt)
print(result["valid"])        # True if ML-DSA-65 sig + Merkle proof both check out

# Anonymous drop (BIP39 mnemonic)
mnemonic = gp.drop(b"sensitive data", ttl=3600)
data, _   = gp.receive(mnemonic)  # pickup by mnemonic

CLI tools

Install via:

curl -fsSL https://paramant.app/install-client.sh | bash

Or included in paramantOS. Full list and source: scripts/.

CT log verification

# Fetch the latest STH and verify the ML-DSA-65 signature
paramant-verify-sth --relay https://relay.paramant.app

# Verify against a specific relay and print the tree state
paramant-verify-sth --relay https://health.paramant.app --verbose

# Cross-check STH consistency across all peer relays
paramant-verify-peers
paramant-verify-peers --relay https://relay.paramant.app

paramant-verify-sth fetches /v2/sth and /v2/pubkey, verifies the ML-DSA-65 signature, and exits non-zero if invalid.

paramant-verify-peers fetches /v2/sth/peers and verifies that each mirrored STH is internally consistent and that tree sizes only grow.

Delivery receipts

# View the receipt returned after a receive operation
paramant-receipt --hash a3f2…

# Save receipt to file
paramant-receipt --hash a3f2… --save receipt.json

# Verify a saved receipt
paramant-receipt --verify receipt.json
paramant-receipt --verify <base64url>

paramant-receipt --verify calls POST /v2/verify-receipt and prints the result. Exit code 0 = valid, 1 = invalid.


Trust model

The CT log follows the same trust model as Certificate Transparency (RFC 6962): you need at least one honest participant in the ecosystem to detect misbehaviour.

  • Monitors call GET /v2/sth on a schedule and archive each root. A root that changes without a corresponding tree extension is a fork — proof of log manipulation.
  • Auditors call GET /v2/ct/proof?index=N to check inclusion of any known blob hash.
  • Gossip (/v2/sth/ingest, /v2/sth/peers) lets relays cross-check each other's trees. A relay cannot silently show different trees to different parties if peers are exchanging STHs.
  • RSS archiving (/ct/feed.xml) lets anyone subscribe to the STH feed. Once published, a root cannot be un-published without leaving evidence.

You do not need to trust the relay operator to detect log tampering — you only need to trust that at least one monitor, auditor, or RSS subscriber is honest and retains their copy.


User account API

All /api/user/ endpoints are served by the admin panel (https://paramant.app), not the sector relays. They require either an active session cookie or are part of the unauthenticated signup and login flows.

POST /api/user/signup

curl -X POST https://paramant.app/api/user/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com"}'
# {"ok":true}

Sends a TOTP setup link to the given email address. Rate-limited per IP and per email.

POST /api/user/auth/setup/:token

curl -X POST https://paramant.app/api/user/auth/setup/abc123... \
  -H "Content-Type: application/json"
# {"secret":"BASE32SECRET","backup_codes":["code1","code2",…]}

Returns the TOTP secret (as a Base32 string) and one-time backup codes. Idempotent: if the enrollment is provisional (QR scanned but not yet confirmed), the same secret is returned on repeat calls. Returns 409 only if TOTP is already fully activated.

POST /api/user/auth/setup/:token/confirm

curl -X POST https://paramant.app/api/user/auth/setup/abc123.../confirm \
  -H "Content-Type: application/json" \
  -d '{"totp_code":"123456"}'
# {"ok":true}

Verifies the first TOTP code and activates the account. After this call the setup token is consumed and cannot be reused.

POST /api/user/auth/login

curl -X POST https://paramant.app/api/user/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com","totp_code":"123456"}'
# Sets: Set-Cookie: paramant_user_session=<token>; HttpOnly; Secure; SameSite=Strict
# {"ok":true}

POST /api/user/auth/login-with-backup

curl -X POST https://paramant.app/api/user/auth/login-with-backup \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com","backup_code":"abc-def-ghi"}'
# {"ok":true}

Backup codes are single-use. The account is re-locked after use; a new TOTP enrollment is required.

POST /api/user/auth/logout

curl -X POST https://paramant.app/api/user/auth/logout \
  -H "Cookie: paramant_user_session=<token>"
# {"ok":true}

GET /api/user/session/verify

curl https://paramant.app/api/user/session/verify \
  -H "Cookie: paramant_user_session=<token>"
# {"ok":true,"user_id":"…","email":"jane@example.com"}

GET /api/user/account

Returns the account profile, active sessions, and billing status.

curl https://paramant.app/api/user/account \
  -H "Cookie: paramant_user_session=<token>"
# {"ok":true,"email":"jane@example.com","plan":"free","sessions":[…]}

DELETE /api/user/account

Permanently deletes the account and all associated Redis state.

curl -X DELETE https://paramant.app/api/user/account \
  -H "Cookie: paramant_user_session=<token>"
# {"ok":true}

POST /api/user/account/totp/reset

Sends a new TOTP setup link, invalidating the current TOTP secret. Requires an active session.

POST /api/user/account/sessions/revoke-others

Revokes all sessions except the current one.

POST /api/user/account/backup-codes/regenerate

Generates a new set of backup codes and invalidates all previous ones.

POST /api/user/billing/checkout

Initiates a Stripe checkout session (stub — Stripe integration pending).

POST /api/user/billing/cancel

Cancels the active subscription.

GET /api/user/billing/status

Returns current plan and subscription state.

GET /api/user/billing/history

Returns billing history.


Relay internal endpoints (operators only)

These endpoints are served by the sector relays and are intended for internal calls from the admin panel. They require the X-Internal-Auth header set to the value of INTERNAL_AUTH_TOKEN in the relay environment.

They are not accessible from the public internet.

Method Path Description
GET /v2/auth/capabilities Public — returns {user_totp_available: bool}
POST /v2/user/setup-totp Provision a TOTP secret; idempotent for provisional state
POST /v2/user/verify-totp Verify a TOTP code against the stored secret
POST /v2/user/activate-totp Mark TOTP as fully activated
POST /v2/user/consume-backup Consume and invalidate a single backup code
POST /v2/user/regenerate-backup Generate a new backup code set
POST /v2/user/delete-totp Remove all TOTP state for a user
POST /v2/user/get-totp-provisional Return the existing secret if enrollment is provisional
POST /v2/user/get-backup-codes-plaintext Return plaintext backup codes (used during setup display)

Idempotency note for /v2/user/setup-totp: If a TOTP secret already exists but totp_active is false (provisional), the endpoint returns the existing secret and backup codes rather than 409. A 409 is returned only when totp_active is true.


Error codes

HTTP Code Meaning
400 bad_request Malformed request body or missing required field
401 unauthorized Missing or invalid API key, session cookie, or admin token
401 invalid_or_expired_token Setup token not found or past TTL
401 invalid_totp_code TOTP code incorrect or outside allowed window
401 invalid_backup_code Backup code not found or already consumed
403 totp_not_activated Account exists but TOTP setup was not completed
404 not_found Resource does not exist
409 totp_already_configured TOTP is fully activated; cannot re-enroll without a reset
409 email_already_registered Signup attempted for an email that already has an account
429 rate_limited Too many requests from this IP or email address
500 internal Unexpected server error; check relay logs

POST /admin/api/admin/force-totp

Require or remove TOTP for a specific user.

Body:

{ "key": "pgp_...", "required": true, "reason": "compliance policy" }

Effect when required: true:

  • paramant:user:totp_required:{key} set in Redis
  • All active user sessions revoked
  • Setup email sent automatically
  • Login blocked until TOTP active

Rate limit: 20/admin/24h Audit event: admin_totp_required_toggled