Proof Chain API Reference

Version: 1.0.0
Status: Forward-looking spec — the spine/anchor/verify surface below is not yet implemented (501 stubs). See the banner below.
OpenAPI Spec: proofs-openapi.yaml (present; describes the forward-looking contract, which is currently 501-stubbed in the service).

Reconciliation banner (2026-05-29). Verified against src/Attestor/StellaOps.Attestor/StellaOps.Attestor.WebService/:

  • The endpoints in this document (/proofs/{entry}/spine, /proofs/{entry}/receipt, /proofs/{entry}/vex, /anchors*, /verify) are backed by ProofsController, AnchorsController, and VerifyController. Every handler returns 501 Not Implemented, gated behind feature flags (ProofsEnabled / AnchorsEnabled / VerifyEnabled, all default false); even when a flag is enabled the body still returns 501. Treat this whole document as a forward-looking contract, not live API.
  • The controllers are mounted root-relative ([Route("proofs")], [Route("anchors")], [Route("verify")]) — there is no /api/v1 prefix on these routes.
  • Authorization uses policies attestor:read / attestor:write / attestor:verify, which require the JWT scopes attestor.read / attestor.write / attestor.verify (NOT proofs.read / proofs.write / proofs.verify / anchors.manage, which do not exist).
  • A separate, implemented proof-chain query controller (ProofChainController) exists at /api/v1/proofs/{subjectDigest} keyed by artifact subject digest — see Implemented proof-chain queries.

The Proof Chain API creates and verifies cryptographic proof bundles that link SBOM entries to vulnerability assessments through attestable DSSE envelopes. This document describes the forward-looking spine/anchor/verify contract; for the proof-chain queries you can call today, see Implemented proof-chain queries.


Overview

The proof chain creates an auditable, cryptographically-verifiable trail from vulnerability evidence through policy reasoning to VEX verdicts. Each component is signed with DSSE envelopes and aggregated into a merkle-rooted proof spine.

Proof Chain Components

ComponentPredicate TypePurpose
Evidenceevidence.stella/v1Raw findings from scanners/feeds
Reasoningreasoning.stella/v1Policy evaluation trace
VEX Verdictcdx-vex.stella/v1Final VEX status determination
Proof Spineproofspine.stella/v1Merkle aggregation of all components
Verdict Receiptverdict.stella/v1Human-readable verification receipt

Content-Addressed IDs

All proof chain components use content-addressed identifiers:

Format: sha256:<64-hex-chars>
Example: sha256:e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6...

IDs are computed by:

  1. Canonicalizing the JSON payload (RFC 8785/JCS)
  2. Computing SHA-256 hash
  3. Prefixing with sha256:

Authentication

All endpoints require authentication via:

Required scopes (corrected 2026-05-29 — the previously listed proofs.read / proofs.write / proofs.verify / anchors.manage scopes do not exist):


Endpoints

All routes in this section are root-relative (no /api/v1 prefix) and currently return 501 Not Implemented (ProofsController / AnchorsController / VerifyController). The request/response shapes below are the intended contract.

Proofs

POST /proofs/{entry}/spine — 501 (not implemented)

ProofsController.CreateSpineAsync, scope attestor:write. Create a proof spine for an SBOM entry.

Parameters:

Request Body:

{
  "evidenceIds": ["sha256:e7f8a9b0..."],
  "reasoningId": "sha256:f0e1d2c3...",
  "vexVerdictId": "sha256:d4c5b6a7...",
  "policyVersion": "v1.2.3"
}

Response (201 Created):

{
  "proofBundleId": "sha256:1a2b3c4d...",
  "receiptUrl": "/proofs/sha256:abc:pkg:npm/lodash@4.17.21/receipt"
}

Errors:


GET /proofs/{entry}/spine — 501 (not implemented)

ProofsController.GetSpineAsync, scope attestor:read. Get the proof spine for an SBOM entry.

Parameters:

Response (200 OK):

{
  "sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
  "proofBundleId": "sha256:1a2b3c4d...",
  "evidenceIds": ["sha256:e7f8a9b0..."],
  "reasoningId": "sha256:f0e1d2c3...",
  "vexVerdictId": "sha256:d4c5b6a7...",
  "policyVersion": "v1.2.3",
  "createdAt": "2025-12-17T10:00:00Z"
}

GET /proofs/{entry}/receipt — 501 (not implemented)

ProofsController.GetReceiptAsync, scope attestor:read. Get the verification receipt for an SBOM entry’s proof spine.

Response (200 OK):

{
  "graphRevisionId": "grv_sha256:9f8e7d6c...",
  "findingKey": {
    "sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
    "vulnerabilityId": "CVE-2025-1234"
  },
  "rule": {
    "id": "critical-vuln-block",
    "version": "v1.0.0"
  },
  "decision": {
    "verdict": "pass",
    "severity": "none",
    "reasoning": "Not affected - vulnerable code not present"
  },
  "createdAt": "2025-12-17T10:00:00Z",
  "verified": true
}

GET /proofs/{entry}/vex — 501 (not implemented)

ProofsController.GetVexAsync, scope attestor:read. Get the VEX attestation for an SBOM entry.

Response (200 OK):

{
  "sbomEntryId": "sha256:abc123:pkg:npm/lodash@4.17.21",
  "vulnerabilityId": "CVE-2025-1234",
  "status": "not_affected",
  "justification": "vulnerable_code_not_present",
  "policyVersion": "v1.2.3",
  "reasoningId": "sha256:f0e1d2c3...",
  "vexVerdictId": "sha256:d4c5b6a7..."
}

Trust Anchors

All anchor routes are served by AnchorsController (root-relative /anchors) and return 501 Not Implemented. Full implemented route set: GET /anchors (attestor:read), GET /anchors/{anchorId} (attestor:read), POST /anchors (attestor:write), PATCH /anchors/{anchorId:guid} (attestor:write), POST /anchors/{anchorId:guid}/revoke-key (attestor:write), DELETE /anchors/{anchorId:guid} (attestor:write).

GET /anchors — 501 (not implemented)

List all configured trust anchors.

Response (200 OK):

{
  "anchors": [
    {
      "id": "anchor-001",
      "keyId": "sha256:abc123...",
      "algorithm": "ECDSA-P256",
      "status": "active",
      "createdAt": "2025-01-01T00:00:00Z"
    }
  ]
}

POST /anchors — 501 (not implemented)

Create a new trust anchor. (Implemented request DTO is CreateTrustAnchorRequest, which is pattern-based — purlPattern — rather than the keyId/algorithm/publicKey body shown below.)

Request Body:

{
  "keyId": "sha256:abc123...",
  "algorithm": "ECDSA-P256",
  "publicKey": "-----BEGIN PUBLIC KEY-----\n..."
}

Response (201 Created):

{
  "id": "anchor-002",
  "keyId": "sha256:abc123...",
  "algorithm": "ECDSA-P256",
  "status": "active",
  "createdAt": "2025-12-17T10:00:00Z"
}

DELETE /anchors/{anchorId} — 501 (not implemented)

Delete (deactivate) a trust anchor. Route correction: the parameter is constrained to a GUID (DELETE /anchors/{anchorId:guid}). To revoke a key within an anchor (not deactivate the whole anchor), the implemented route is POST /anchors/{anchorId:guid}/revoke-key with a RevokeKeyRequest body, not this DELETE.

Response (intended): 204 No Content


Verification

Served by VerifyController (root-relative /verify), all 501 Not Implemented. Implemented routes: POST /verify/{proofBundleId} (attestor:verify), GET /verify/envelope/{envelopeHash} (attestor:read), GET /verify/rekor/{envelopeHash} (attestor:read).

POST /verify/{proofBundleId} — 501 (not implemented)

Perform full verification of a proof bundle.

Route correction (2026-05-29): The bundle id is a path parameter (POST /verify/{proofBundleId}); there is no POST /verify collection route taking the id in the body. The proofBundleId field in the body example below is therefore redundant with the path. There is no POST /verify/batch endpoint (see below).

Request Body (VerifyProofRequest, optional):

{
  "proofBundleId": "sha256:1a2b3c4d...",
  "checkRekor": true,
  "anchorIds": ["anchor-001"]
}

Response (200 OK):

{
  "proofBundleId": "sha256:1a2b3c4d...",
  "verified": true,
  "checks": {
    "signatureValid": true,
    "idRecomputed": true,
    "merklePathValid": true,
    "rekorInclusionValid": true
  },
  "errors": [],
  "verifiedAt": "2025-12-17T10:00:00Z"
}

Verification Steps:

  1. Signature Verification: Verify DSSE envelope signatures against trust anchors
  2. ID Recomputation: Recompute content-addressed IDs and compare
  3. Merkle Path Verification: Verify proof bundle merkle tree construction
  4. Rekor Inclusion: Verify transparency log inclusion proof (if enabled)

POST /verify/batch — DOES NOT EXIST

Removed (2026-05-29): VerifyController exposes no batch route. The implemented sibling verify routes are GET /verify/envelope/{envelopeHash} and GET /verify/rekor/{envelopeHash} (both currently 501). If batch verification is required, file a sprint before re-documenting it. Note the Attestor does have a separate Rekor bulk-verify minimal endpoint at POST /api/v1/rekor/verify:bulk (different surface from this proof-chain spec).


Implemented proof-chain queries (LIVE)

While the spine/anchor/verify surface above is stubbed, ProofChainController (route /api/v1/proofs, scope attestor:read) is implemented and keyed by artifact subject digest:

MethodRouteScopeDescription
GET/api/v1/proofs/{subjectDigest}attestor:readList proofs for an artifact (ProofListResponse)
GET/api/v1/proofs/{subjectDigest}/chain?maxDepth=attestor:readEvidence-chain graph (ProofChainResponse; depth 1–10, default 5)
GET/api/v1/proofs/id/{proofId}attestor:readProof detail (ProofDetail)
GET/api/v1/proofs/id/{proofId}/verifyattestor:verifyVerify a proof (ProofVerificationResult)

This is the controller to use for “Show Me The Proof” workflows today; the {entry}/spine creation flow above remains future work.


Error Handling

All errors follow RFC 7807 Problem Details format:

{
  "title": "Validation Error",
  "detail": "Evidence ID sha256:abc... not found",
  "status": 422,
  "errors": {
    "evidenceIds[0]": ["Evidence not found"]
  }
}

Common Error Codes

StatusMeaning
400Invalid request format or parameters
401Authentication required
403Insufficient permissions
404Resource not found
409Conflict (e.g., anchor already exists)
422Validation error
500Internal server error

Offline Verification

For air-gapped environments, verification can be performed without Rekor:

{
  "proofBundleId": "sha256:1a2b3c4d...",
  "checkRekor": false
}

This skips Rekor inclusion proof verification but still performs: