Portal API reference
The management portal's API is an open specification, so that the offline portal and the
TestifySec platform can serve the same contract. Paths are under /api/v1. The tenant comes from
the deployment or the credential, never from the path. JSON request bodies refuse unknown fields.
Errors are RFC 9457 application/problem+json documents whose error member holds a stable code.
Every route except discovery and the device routes requires a client certificate that the
portal verifies against the enclave CA. The certificate subject identifies the user, and the role
(admin, approver or auditor) comes from the portal's user directory.
The portal is on main. Operations marked "specified, not built" are part of the contract and
have no implementation yet.
Generated from testifysec/portal/api/openapi.yaml: Badger management portal API, version 0.6.0, OpenAPI 3.1.0.
| Method | Path | Status | Summary |
|---|---|---|---|
| GET | /api/v1/profiles | implemented | Latest signed version of every profile (admin, approver, auditor) |
| GET | /api/v1/profiles/{profileId}/versions | implemented | Every version of a profile, newest first, without envelopes (admin, approver, auditor) |
| POST | /api/v1/profiles/{profileId}/versions | implemented | Sign the next version (admin). The server assigns tenant and sequence. |
| GET | /api/v1/profiles/{profileId}/trust | implemented | Build-provisioned admission trust input for this profile (admin, auditor) |
| GET | /api/v1/profiles/{profileId}/versions/{sequence} | implemented | One version, without its envelope (admin, approver, auditor) |
| GET | /api/v1/profiles/{profileId}/versions/{sequence}/envelope | implemented | The DSSE envelope exactly as signed (admin, approver, auditor) |
| GET | /api/v1/deployments/{deploymentId}/assignment | implemented | The version a deployment (device group) runs (admin, approver, auditor) |
| PUT | /api/v1/deployments/{deploymentId}/assignment | implemented | Point a deployment at a signed version of its profile (admin) |
| GET | /.well-known/badger-portal | implemented | Discovery; public, names no tenant, deployment or key |
| GET | /api/v1/users | implemented | The user directory (admin, approver, auditor) |
| POST | /api/v1/users | implemented | Provision an identity the enclave PKI or IdP issued (admin). Pending, no role, until an approver activates it. Inviting a pending user with no open activation request issues one; otherwise 409. |
| POST | /api/v1/users/{userId}/drop-role | implemented | Remove an active user's role at once (admin); body {} |
| PUT | /api/v1/users/{userId}/deployments/{deploymentId} | implemented | Add an active user to a deployment (admin); body {} |
| DELETE | /api/v1/users/{userId}/deployments/{deploymentId} | implemented | Remove a membership (admin); body {} |
| POST | /api/v1/users/{userId}/offboard | implemented | Offboard (admin, not self); final. Clears role and memberships and drops pending requests by or about the user; body {} |
| GET | /api/v1/role-requests | implemented | The approver queue, pending requests only (admin, approver, auditor) |
| POST | /api/v1/users/{userId}/role-requests | implemented | Request a role for an active user (admin); nothing changes until an approver decides |
| POST | /api/v1/role-requests/{requestId}/approve | implemented | Approve (approver who is neither the requester nor the user) |
| POST | /api/v1/role-requests/{requestId}/reject | implemented | Reject (same rule as approve) |
| GET | /api/v1/device/nonce | specified, not built | One-use nonce for a device-key proof |
| POST | /api/v1/device/profile | specified, not built | A workstation pulls its signed profile, its deployment's activation record and any pending reset |
GET /api/v1/profiles
Latest signed version of every profile (admin, approver, auditor)
Status: implemented. Authentication: client certificate (mutual TLS).
Response 200: Versions, without envelopes
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
items | array of Version | yes |
Response default: Error
application/problem+json: Problem
GET /api/v1/profiles/{profileId}/versions
Every version of a profile, newest first, without envelopes (admin, approver, auditor)
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
profileId | path | Identifier | yes |
Response 200: Versions
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
items | array of Version | yes |
Response default: Error
application/problem+json: Problem
POST /api/v1/profiles/{profileId}/versions
Sign the next version (admin). The server assigns tenant and sequence.
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
profileId | path | Identifier | yes |
Request body:
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
deploymentId | Identifier | yes | |
notBefore | Instant | yes | |
expiresAt | Instant | yes |
Response 201: Signed and stored
application/json: Version
Response default: Error
application/problem+json: Problem
GET /api/v1/profiles/{profileId}/trust
Build-provisioned admission trust input for this profile (admin, auditor)
Exactly the schema testifysec/managed/README.md accepts; minimumSequence is the latest sequence.
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
profileId | path | Identifier | yes |
Response 200: Trust input
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
schemaVersion | 1 | yes | |
publicKeys | array of string (at least 1, at most 16) | yes | |
threshold | integer (minimum 1) | yes | |
tenantId | Identifier | yes | |
deploymentId | Identifier | yes | |
profileId | Identifier | yes | |
minimumSequence | Sequence | yes |
Response default: Error
application/problem+json: Problem
GET /api/v1/profiles/{profileId}/versions/{sequence}
One version, without its envelope (admin, approver, auditor)
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
profileId | path | Identifier | yes |
sequence | path | string (pattern ^[1-9][0-9]{0,15}$) | yes |
Response 200: Version
application/json: Version
Response default: Error
application/problem+json: Problem
GET /api/v1/profiles/{profileId}/versions/{sequence}/envelope
The DSSE envelope exactly as signed (admin, approver, auditor)
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
profileId | path | Identifier | yes |
sequence | path | string (pattern ^[1-9][0-9]{0,15}$) | yes |
Response 200: Envelope bytes; sha256 equals the version's envelopeSha256
application/vnd.dsse.envelope.v1+json:
| Field | Type | Required | Description |
|---|
Response default: Error
application/problem+json: Problem
GET /api/v1/deployments/{deploymentId}/assignment
The version a deployment (device group) runs (admin, approver, auditor)
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
deploymentId | path | Identifier | yes |
Response 200: Assignment
application/json: Assignment
Response default: Error
application/problem+json: Problem
PUT /api/v1/deployments/{deploymentId}/assignment
Point a deployment at a signed version of its profile (admin)
Forward only. An older sequence, or a different profile once one is assigned, is 409 rollback; roll back by signing a new version.
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
deploymentId | path | Identifier | yes |
Request body:
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
profileId | Identifier | yes | |
sequence | Sequence | yes |
Response 200: Assignment
application/json: Assignment
Response default: Error
application/problem+json: Problem
GET /.well-known/badger-portal
Discovery; public, names no tenant, deployment or key
Status: implemented. Authentication: none.
Response 200: Discovery document
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
specVersion | string | yes | |
apiBase | string | yes | |
profilePayloadType | string | yes | |
authentication | array of mutualTLS | yes |
GET /api/v1/users
The user directory (admin, approver, auditor)
Status: implemented. Authentication: client certificate (mutual TLS).
Response 200: Users
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
items | array of User | yes |
Response default: Error
application/problem+json: Problem
POST /api/v1/users
Provision an identity the enclave PKI or IdP issued (admin). Pending, no role, until an approver activates it. Inviting a pending user with no open activation request issues one; otherwise 409.
Status: implemented. Authentication: client certificate (mutual TLS).
Request body:
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
kind | x509 | yes | |
subject | string | yes | Certificate subject DN |
Response 201: The pending user and its activation request
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
user | User | yes | |
activation | RoleRequest | yes |
Response default: Error
application/problem+json: Problem
POST /api/v1/users/{userId}/drop-role
Remove an active user's role at once (admin); body {}
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
userId | path | Digest | yes |
Response 200: User
application/json: User
Response default: Error
application/problem+json: Problem
PUT /api/v1/users/{userId}/deployments/{deploymentId}
Add an active user to a deployment (admin); body {}
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
userId | path | Digest | yes |
deploymentId | path | Identifier | yes |
Response 200: User
application/json: User
Response default: Error
application/problem+json: Problem
DELETE /api/v1/users/{userId}/deployments/{deploymentId}
Remove a membership (admin); body {}
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
userId | path | Digest | yes |
deploymentId | path | Identifier | yes |
Response 200: User
application/json: User
Response default: Error
application/problem+json: Problem
POST /api/v1/users/{userId}/offboard
Offboard (admin, not self); final. Clears role and memberships and drops pending requests by or about the user; body {}
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
userId | path | Digest | yes |
Response 200: User
application/json: User
Response default: Error
application/problem+json: Problem
GET /api/v1/role-requests
The approver queue, pending requests only (admin, approver, auditor)
Status: implemented. Authentication: client certificate (mutual TLS).
Response 200: Pending requests
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
items | array of RoleRequest | yes |
Response default: Error
application/problem+json: Problem
POST /api/v1/users/{userId}/role-requests
Request a role for an active user (admin); nothing changes until an approver decides
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
userId | path | Digest | yes |
Request body:
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
role | admin, approver, auditor | yes |
Response 201: Request
application/json: RoleRequest
Response default: Error
application/problem+json: Problem
POST /api/v1/role-requests/{requestId}/approve
Approve (approver who is neither the requester nor the user)
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
requestId | path | string (pattern ^[0-9a-f]{32}$) | yes |
Response 200: Request
application/json: RoleRequest
Response default: Error
application/problem+json: Problem
POST /api/v1/role-requests/{requestId}/reject
Reject (same rule as approve)
Status: implemented. Authentication: client certificate (mutual TLS).
| Parameter | In | Type | Required |
|---|---|---|---|
requestId | path | string (pattern ^[0-9a-f]{32}$) | yes |
Response 200: Request
application/json: RoleRequest
Response default: Error
application/problem+json: Problem
GET /api/v1/device/nonce
One-use nonce for a device-key proof
Status: specified, not built. Authentication: none.
Response 200: Nonce, valid for 60 seconds
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
nonce | string | yes |
POST /api/v1/device/profile
A workstation pulls its signed profile, its deployment's activation record and any pending reset
The device proves possession of its cilock-enrolled device key (TPM or Secure Enclave custody): proof is a compact ES256 JWS by that key over the nonce, the key thumbprint and this portal's audience. Returns both documents exactly as signed for the device's deployment. 403 when unknown or revoked. The portal never pushes.
Status: specified, not built. Authentication: none.
Request body:
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
nonce | string | yes | |
proof | string | yes |
Response 200: The current pair
application/json:
| Field | Type | Required | Description |
|---|---|---|---|
envelope | object | yes | Profile DSSE envelope |
activation | object | yes | Activation record DSSE envelope |
reset | object | no | Pending device-bound high-water reset record (DSSE), present only when one awaits this device |
Response default: Error
application/problem+json: Problem
Schemas
User
| Field | Type | Required | Description |
|---|---|---|---|
id | Digest | yes | |
kind | x509 | yes | |
subject | string | yes | |
role | ``, admin, approver, auditor | yes | |
status | pending, active, offboarded | yes | |
createdAt | string (date-time) | yes | |
createdBy | string | yes | |
deployments | array of Identifier | no |
RoleRequest
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
kind | activate, grant | yes | |
userId | Digest | yes | |
role | ``, admin, approver, auditor | yes | |
requestedBy | string | yes | |
requesterId | Digest | yes | |
requestedAt | string (date-time) | yes | |
status | pending, approved, rejected, dropped | yes | |
decidedBy | string | no | |
decidedAt | string (date-time) | no |
Assignment
| Field | Type | Required | Description |
|---|---|---|---|
deploymentId | Identifier | yes | |
profileId | Identifier | yes | |
sequence | Sequence | yes | |
payloadSha256 | Digest | yes | |
assignedAt | string (date-time) | yes | |
assignedBy | string | yes |
Identifier
string (pattern ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$)
Instant
string (pattern ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$)
Sequence
integer (minimum 1, maximum 9007199254740991)
Digest
string (pattern ^[0-9a-f]{64}$)
Version
| Field | Type | Required | Description |
|---|---|---|---|
profileId | Identifier | yes | |
tenantId | Identifier | yes | |
deploymentId | Identifier | yes | |
sequence | Sequence | yes | |
notBefore | Instant | yes | |
expiresAt | Instant | yes | |
payloadSha256 | Digest | yes | |
envelopeSha256 | Digest | yes | |
signerKeyIds | array of Digest | yes | |
createdAt | string (date-time) | yes | |
createdBy | string | yes | Authenticated principal (certificate subject DN or OIDC subject) |
Problem
RFC 9457 problem document; "error" repeats the stable code.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | |
title | string | yes | |
status | integer | yes | |
detail | string | no | |
error | unauthenticated, forbidden, cross-origin, invalid-request, invalid-profile, sequence-conflict, rollback, not-found, store, issue-failed, assign-failed, conflict, invalid-state, users-failed | yes |
Reference generated from the product documentation. Match commands and support details to your installed release.