378 lines
11 KiB
YAML
378 lines
11 KiB
YAML
|
|
openapi: 3.1.0
|
||
|
|
info:
|
||
|
|
title: Shade Prekey Server
|
||
|
|
description: |
|
||
|
|
Signal Protocol prekey distribution server. Stores identity public keys
|
||
|
|
and prekey bundles. Any language can implement a client — this spec
|
||
|
|
documents the wire contract.
|
||
|
|
|
||
|
|
**Security model:** Write operations (register, replenish, delete) are
|
||
|
|
authenticated by Ed25519 signatures over the request body. Bundle fetches
|
||
|
|
are anonymous. See the `SignedPayload` schema for the signing format.
|
||
|
|
version: "1.0.0"
|
||
|
|
license:
|
||
|
|
name: MIT
|
||
|
|
url: https://gt.zyon.no/Stian/Shade/raw/branch/main/LICENSE
|
||
|
|
|
||
|
|
servers:
|
||
|
|
- url: https://shade.example.com
|
||
|
|
description: Replace with your project's prekey server URL
|
||
|
|
|
||
|
|
paths:
|
||
|
|
/health:
|
||
|
|
get:
|
||
|
|
summary: Health check
|
||
|
|
description: Returns 200 if the server and its storage backend are reachable.
|
||
|
|
tags: [Infrastructure]
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Healthy
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/HealthResponse'
|
||
|
|
'503':
|
||
|
|
description: Unhealthy
|
||
|
|
|
||
|
|
/metrics:
|
||
|
|
get:
|
||
|
|
summary: Prometheus metrics
|
||
|
|
description: Counter, histogram, and gauge metrics in Prometheus text format.
|
||
|
|
tags: [Infrastructure]
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Metrics
|
||
|
|
content:
|
||
|
|
text/plain:
|
||
|
|
schema:
|
||
|
|
type: string
|
||
|
|
example: |
|
||
|
|
# HELP shade_requests_total Total HTTP requests
|
||
|
|
# TYPE shade_requests_total counter
|
||
|
|
shade_requests_total{route="/v1/keys/register",status="200"} 42
|
||
|
|
|
||
|
|
/openapi.yaml:
|
||
|
|
get:
|
||
|
|
summary: This OpenAPI spec
|
||
|
|
description: Serves the YAML spec itself, so clients can auto-download it.
|
||
|
|
tags: [Infrastructure]
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: OpenAPI spec
|
||
|
|
content:
|
||
|
|
application/yaml:
|
||
|
|
schema:
|
||
|
|
type: string
|
||
|
|
|
||
|
|
/v1/keys/register:
|
||
|
|
post:
|
||
|
|
summary: Register identity and upload prekey bundle
|
||
|
|
description: |
|
||
|
|
Register a new identity with the prekey server, or update an existing
|
||
|
|
one. The request body must be signed with the identity's own Ed25519
|
||
|
|
signing key (TOFU — first signature establishes the identity).
|
||
|
|
tags: [Keys]
|
||
|
|
security:
|
||
|
|
- Ed25519Signature: []
|
||
|
|
requestBody:
|
||
|
|
required: true
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
allOf:
|
||
|
|
- $ref: '#/components/schemas/SignedPayload'
|
||
|
|
- type: object
|
||
|
|
required: [address, identitySigningKey, identityDHKey, signedPreKey]
|
||
|
|
properties:
|
||
|
|
address:
|
||
|
|
$ref: '#/components/schemas/Address'
|
||
|
|
identitySigningKey:
|
||
|
|
type: string
|
||
|
|
description: Ed25519 public key, base64
|
||
|
|
identityDHKey:
|
||
|
|
type: string
|
||
|
|
description: X25519 public key, base64
|
||
|
|
signedPreKey:
|
||
|
|
$ref: '#/components/schemas/SignedPreKeyEntry'
|
||
|
|
oneTimePreKeys:
|
||
|
|
type: array
|
||
|
|
items:
|
||
|
|
$ref: '#/components/schemas/OneTimePreKeyEntry'
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Registered
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
type: object
|
||
|
|
properties:
|
||
|
|
ok: { type: boolean }
|
||
|
|
'400':
|
||
|
|
$ref: '#/components/responses/ValidationError'
|
||
|
|
'401':
|
||
|
|
$ref: '#/components/responses/Unauthorized'
|
||
|
|
'429':
|
||
|
|
$ref: '#/components/responses/RateLimited'
|
||
|
|
|
||
|
|
/v1/keys/bundle/{address}:
|
||
|
|
get:
|
||
|
|
summary: Fetch a prekey bundle (anonymous)
|
||
|
|
description: |
|
||
|
|
Fetch an identity's prekey bundle so Alice can start an X3DH session
|
||
|
|
with Bob. Each call consumes one one-time prekey (FIFO) if any are
|
||
|
|
available — if not, returns a bundle without the one-time prekey.
|
||
|
|
tags: [Keys]
|
||
|
|
parameters:
|
||
|
|
- name: address
|
||
|
|
in: path
|
||
|
|
required: true
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/Address'
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Prekey bundle
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/PreKeyBundle'
|
||
|
|
'404':
|
||
|
|
$ref: '#/components/responses/NotFound'
|
||
|
|
'429':
|
||
|
|
$ref: '#/components/responses/RateLimited'
|
||
|
|
|
||
|
|
/v1/keys/count/{address}:
|
||
|
|
get:
|
||
|
|
summary: Get remaining one-time prekey count (anonymous)
|
||
|
|
tags: [Keys]
|
||
|
|
parameters:
|
||
|
|
- name: address
|
||
|
|
in: path
|
||
|
|
required: true
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/Address'
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Count
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
type: object
|
||
|
|
properties:
|
||
|
|
count: { type: integer, minimum: 0 }
|
||
|
|
|
||
|
|
/v1/keys/replenish:
|
||
|
|
post:
|
||
|
|
summary: Upload more one-time prekeys (signed)
|
||
|
|
tags: [Keys]
|
||
|
|
security:
|
||
|
|
- Ed25519Signature: []
|
||
|
|
requestBody:
|
||
|
|
required: true
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
allOf:
|
||
|
|
- $ref: '#/components/schemas/SignedPayload'
|
||
|
|
- type: object
|
||
|
|
required: [address, oneTimePreKeys]
|
||
|
|
properties:
|
||
|
|
address:
|
||
|
|
$ref: '#/components/schemas/Address'
|
||
|
|
oneTimePreKeys:
|
||
|
|
type: array
|
||
|
|
items:
|
||
|
|
$ref: '#/components/schemas/OneTimePreKeyEntry'
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Replenished
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
type: object
|
||
|
|
properties:
|
||
|
|
ok: { type: boolean }
|
||
|
|
remaining: { type: integer, minimum: 0 }
|
||
|
|
'401':
|
||
|
|
$ref: '#/components/responses/Unauthorized'
|
||
|
|
'404':
|
||
|
|
$ref: '#/components/responses/NotFound'
|
||
|
|
'429':
|
||
|
|
$ref: '#/components/responses/RateLimited'
|
||
|
|
|
||
|
|
/v1/keys/{address}:
|
||
|
|
delete:
|
||
|
|
summary: Unregister an identity (signed)
|
||
|
|
tags: [Keys]
|
||
|
|
security:
|
||
|
|
- Ed25519Signature: []
|
||
|
|
parameters:
|
||
|
|
- name: address
|
||
|
|
in: path
|
||
|
|
required: true
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/Address'
|
||
|
|
requestBody:
|
||
|
|
required: true
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/SignedPayload'
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Deleted
|
||
|
|
'401':
|
||
|
|
$ref: '#/components/responses/Unauthorized'
|
||
|
|
|
||
|
|
/shade-observer/api/state:
|
||
|
|
get:
|
||
|
|
summary: Current observer snapshot (optional)
|
||
|
|
description: |
|
||
|
|
Returns the aggregated state visible to the dashboard. Only available
|
||
|
|
if the server was started with `SHADE_OBSERVER_TOKEN` set.
|
||
|
|
tags: [Observer]
|
||
|
|
security:
|
||
|
|
- ObserverBearerToken: []
|
||
|
|
responses:
|
||
|
|
'200':
|
||
|
|
description: Observer state snapshot
|
||
|
|
'401':
|
||
|
|
description: Invalid or missing token
|
||
|
|
|
||
|
|
components:
|
||
|
|
schemas:
|
||
|
|
Address:
|
||
|
|
type: string
|
||
|
|
description: |
|
||
|
|
An address on the prekey server. Alphanumeric plus `:_-.` characters,
|
||
|
|
max 256 chars. NFKC-normalized. Format is typically
|
||
|
|
`user@domain:deviceId` or `device:uuid`.
|
||
|
|
pattern: '^[a-zA-Z0-9][a-zA-Z0-9:_\-.]{0,255}$'
|
||
|
|
example: "alice@example.com:phone"
|
||
|
|
|
||
|
|
SignedPayload:
|
||
|
|
type: object
|
||
|
|
description: |
|
||
|
|
Base type for all signed request bodies. The `signature` field is
|
||
|
|
an Ed25519 signature over the canonical JSON of the payload
|
||
|
|
(all fields sorted, `signature` omitted) using the identity's
|
||
|
|
signing private key. `signedAt` must be within ±5 minutes of
|
||
|
|
server time.
|
||
|
|
required: [signedAt, signature]
|
||
|
|
properties:
|
||
|
|
signedAt:
|
||
|
|
type: integer
|
||
|
|
format: int64
|
||
|
|
description: Unix epoch milliseconds
|
||
|
|
signature:
|
||
|
|
type: string
|
||
|
|
description: Ed25519 signature, base64
|
||
|
|
|
||
|
|
SignedPreKeyEntry:
|
||
|
|
type: object
|
||
|
|
required: [keyId, publicKey, signature]
|
||
|
|
properties:
|
||
|
|
keyId:
|
||
|
|
type: integer
|
||
|
|
minimum: 0
|
||
|
|
publicKey:
|
||
|
|
type: string
|
||
|
|
description: X25519 public key, base64
|
||
|
|
signature:
|
||
|
|
type: string
|
||
|
|
description: Ed25519 signature over `publicKey`, base64
|
||
|
|
|
||
|
|
OneTimePreKeyEntry:
|
||
|
|
type: object
|
||
|
|
required: [keyId, publicKey]
|
||
|
|
properties:
|
||
|
|
keyId:
|
||
|
|
type: integer
|
||
|
|
minimum: 0
|
||
|
|
publicKey:
|
||
|
|
type: string
|
||
|
|
description: X25519 public key, base64
|
||
|
|
|
||
|
|
PreKeyBundle:
|
||
|
|
type: object
|
||
|
|
required: [identitySigningKey, identityDHKey, signedPreKey]
|
||
|
|
properties:
|
||
|
|
identitySigningKey:
|
||
|
|
type: string
|
||
|
|
description: Ed25519 public key, base64
|
||
|
|
identityDHKey:
|
||
|
|
type: string
|
||
|
|
description: X25519 public key, base64
|
||
|
|
signedPreKey:
|
||
|
|
$ref: '#/components/schemas/SignedPreKeyEntry'
|
||
|
|
oneTimePreKey:
|
||
|
|
$ref: '#/components/schemas/OneTimePreKeyEntry'
|
||
|
|
|
||
|
|
HealthResponse:
|
||
|
|
type: object
|
||
|
|
properties:
|
||
|
|
status:
|
||
|
|
type: string
|
||
|
|
enum: [ok, error]
|
||
|
|
service:
|
||
|
|
type: string
|
||
|
|
|
||
|
|
ErrorResponse:
|
||
|
|
type: object
|
||
|
|
properties:
|
||
|
|
name:
|
||
|
|
type: string
|
||
|
|
code:
|
||
|
|
type: string
|
||
|
|
pattern: '^SHADE_'
|
||
|
|
message:
|
||
|
|
type: string
|
||
|
|
|
||
|
|
responses:
|
||
|
|
ValidationError:
|
||
|
|
description: Invalid request body
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/ErrorResponse'
|
||
|
|
Unauthorized:
|
||
|
|
description: Signature verification failed or replay window exceeded
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/ErrorResponse'
|
||
|
|
NotFound:
|
||
|
|
description: Address not registered
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/ErrorResponse'
|
||
|
|
RateLimited:
|
||
|
|
description: Rate limit exceeded
|
||
|
|
headers:
|
||
|
|
Retry-After:
|
||
|
|
schema:
|
||
|
|
type: integer
|
||
|
|
description: Seconds until the client can retry
|
||
|
|
content:
|
||
|
|
application/json:
|
||
|
|
schema:
|
||
|
|
$ref: '#/components/schemas/ErrorResponse'
|
||
|
|
|
||
|
|
securitySchemes:
|
||
|
|
Ed25519Signature:
|
||
|
|
type: apiKey
|
||
|
|
in: header
|
||
|
|
name: X-Shade-Signature-Info
|
||
|
|
description: |
|
||
|
|
NOT an actual header. Signature is carried inside the JSON request
|
||
|
|
body as the `signature` and `signedAt` fields (see SignedPayload).
|
||
|
|
The canonical form signed is the request body with all keys sorted
|
||
|
|
and the `signature` field omitted. Use your Ed25519 signing key.
|
||
|
|
ObserverBearerToken:
|
||
|
|
type: http
|
||
|
|
scheme: bearer
|
||
|
|
description: |
|
||
|
|
`Authorization: Bearer <SHADE_OBSERVER_TOKEN>`. The observer also
|
||
|
|
accepts the token via `?token=...` query string for SSE endpoints
|
||
|
|
that can't set headers.
|