Witness Schema v1 Contract
Version:
stellaops.witness.v1
Status: Active (implemented)
Sprint:SPRINT_3700_0001_0001_witness_foundation
Overview
Audience: engineers producing, consuming, or verifying Stella Ops reachability witnesses (Scanner, Attestor, CLI, and Evidence Locker integrators).
A witness is a cryptographically-signed proof of a reachability path from an entrypoint to a vulnerable sink. Witnesses provide:
- Auditability - Proof that a path was found at scan time
- Offline verification - Verify claims without re-running analysis
- Provenance - Links to the source graph and analysis context
- Transparency - Can be published to transparency logs
Two representations
This contract describes two related-but-distinct on-disk shapes. Do not confuse them:
- Wire / DSSE payload schema (
stellaops.witness.v1) — the canonical signed payload produced and consumed by the Scanner. This is the authoritative runtime shape. Modelled byStellaOps.Scanner.Reachability.Witnesses.PathWitness(snake_case JSON). The DSSE-signed payload, the CLIwitness show/list/verify/exportcommands, and theGET /api/v1/witnessesAPI all return this shape. - In-toto predicate JSON Schema (
stellaops-path-witness.v1.schema.json) — a separate draft JSON Schema used by the Attestor for predicate validation. It uses a UUIDwitness_id, a separatewitness_hash, aprovenanceblock, a nestedpath.{entrypoint,sink,steps,hop_count}structure, andcas://-styleevidence_uris. These fields are NOT present in the wire schema.
The same field-shape divergence is reflected in storage: the scanner.witnesses table (see Storage) persists predicate-style columns (witness_hash, witness_type, graph_hash, scan_id, run_id, entrypoint_fqn, sink_cve, created_at) in addition to the full wire payload in payload_json.
Sections below are labelled with which representation they describe.
Schema Definition — wire / DSSE payload (stellaops.witness.v1)
Source of truth:
src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Witnesses/PathWitness.csandWitnessSchema.cs. This is the shape that is DSSE-signed, stored inscanner.witnesses.payload_json, and returned by the CLI and API. JSON is serialized snake_case.
{
"witness_schema": "stellaops.witness.v1",
"witness_id": "wit:sha256:abcd1234...",
"artifact": {
"sbom_digest": "sha256:...",
"component_purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
},
"vuln": {
"id": "CVE-2021-44228",
"source": "NVD",
"affected_range": "<2.15.0"
},
"entrypoint": {
"kind": "http",
"name": "GET /api/users/{id}",
"symbol_id": "com.example.MyController.handleRequest"
},
"path": [
{
"symbol": "MyController.handleRequest",
"symbol_id": "com.example.MyController.handleRequest",
"file": "src/main/java/com/example/MyController.java",
"line": 45,
"column": null
},
{
"symbol": "LoggingService.logMessage",
"symbol_id": "com.example.LoggingService.logMessage",
"file": "src/main/java/com/example/LoggingService.java",
"line": 23
},
{
"symbol": "Logger.log",
"symbol_id": "org.apache.log4j.Logger.log",
"file": null,
"line": null
}
],
"sink": {
"symbol": "Logger.log",
"symbol_id": "org.apache.log4j.Logger.log",
"sink_type": "deserialization"
},
"gates": [
{
"type": "AuthRequired",
"guard_symbol": "com.example.MyController.requireAuth",
"confidence": 0.92,
"detail": "Requires authenticated user"
}
],
"evidence": {
"callgraph_digest": "blake3:...",
"surface_digest": "sha256:...",
"analysis_config_digest": "sha256:...",
"build_id": "gnu-build-id:..."
},
"observed_at": "2025-12-18T12:00:00Z",
"path_hash": "path:sha256:...",
"node_hashes": ["sha256:...", "sha256:..."],
"evidence_uris": ["evidence:callgraph:blake3:...", "evidence:sbom:sha256:..."],
"predicate_type": "https://stella.ops/predicates/path-witness/v1",
"observation_type": 0,
"claim_id": null,
"observations": null,
"symbolization": null
}
Field Definitions — wire schema
Root Fields
| Field | Type | Required | Description |
|---|---|---|---|
witness_schema | string | Yes | Must be stellaops.witness.v1 |
witness_id | string | Yes | Content-addressed ID: wit:sha256:<hex> (see Witness ID) |
artifact | object | Yes | SBOM/component context |
vuln | object | Yes | Vulnerability the witness concerns |
entrypoint | object | Yes | Entry point that starts the path |
path | array | Yes | Flat ordered list of PathStep (caller -> callee) |
sink | object | Yes | Vulnerable sink at end of path |
gates | array | No | Detected guards/controls along the path |
evidence | object | Yes | Evidence digests for reproducibility |
observed_at | ISO8601 | Yes | When the witness was generated (UTC) |
path_hash | string | No | Path fingerprint (path:sha256:<hex>; see Path Hash Recipe) |
node_hashes | array | No | Top-K node hashes (sha256:<hex>), deterministically ordered |
evidence_uris | array | No | evidence:<kind>:<digest> traceability URIs |
predicate_type | string | No | Default https://stella.ops/predicates/path-witness/v1 |
observation_type | enum (int) | No | 0=Static (default), 1=Runtime, 2=Confirmed (serialized as integer) |
claim_id | string | No | Links runtime witnesses to a static claim: claim:<artifact-digest>:<path-hash> |
observations | array | No | Runtime observations (empty for purely static witnesses) |
symbolization | object | No | Deterministic symbolization tuple; required for runtime/confirmed witnesses (see Runtime Symbolization Tuple) |
There is no
witness_hash,witness_type,provenance,created_at, orhop_countfield in the wire schema. Those fields belong to the in-toto predicate / storage row only (see below).
artifact
| Field | Type | Required | Description |
|---|---|---|---|
sbom_digest | string | Yes | SHA-256 digest of the SBOM |
component_purl | string | Yes | PURL of the vulnerable component |
vuln
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Vulnerability identifier (e.g., CVE-2024-12345) |
source | string | Yes | Source (e.g., NVD, OSV, GHSA) |
affected_range | string | Yes | Affected version range expression |
entrypoint
| Field | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | Entrypoint kind (e.g., http, grpc, cli, job, event) |
name | string | Yes | Human-readable name (e.g., GET /api/users/{id}) |
symbol_id | string | Yes | Canonical symbol ID for the entrypoint |
Path Step (path[])
| Field | Type | Required | Description |
|---|---|---|---|
symbol | string | Yes | Human-readable symbol name |
symbol_id | string | Yes | Canonical symbol ID |
file | string | No | Source file path (null for external/binary symbols) |
line | integer | No | Line number (1-based) |
column | integer | No | Column number (1-based) |
The wire
PathStephas noindex,call_site, oredge_typefield — those exist only in the in-toto predicate’spathStepdefinition.
sink
| Field | Type | Required | Description |
|---|---|---|---|
symbol | string | Yes | Human-readable symbol name |
symbol_id | string | Yes | Canonical symbol ID |
sink_type | string | Yes | Sink taxonomy (e.g., deserialization, sql_injection, path_traversal) |
Gates (gates[])
Optional array of detected guards/mitigating controls along the path.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Gate type. Emitted as the GateType enum’s .ToString() value (e.g., AuthRequired, InputValidation, RateLimited), not a snake_case enum |
guard_symbol | string | Yes | Symbol that implements the gate |
confidence | number | Yes | Confidence level (0.0 - 1.0) |
detail | string | No | Human-readable detail about the gate |
evidence
| Field | Type | Required | Description |
|---|---|---|---|
callgraph_digest | string | Yes | BLAKE3 digest of the call graph used |
surface_digest | string | No | SHA-256 digest of the attack surface manifest |
analysis_config_digest | string | No | SHA-256 digest of the analysis configuration |
build_id | string | No | Build identifier for the analyzed artifact |
In-toto predicate JSON Schema (separate artifact)
Source of truth:
src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-path-witness.v1.schema.json($idhttps://stella.ops/schemas/predicates/path-witness/v1).
This draft-2020-12 JSON Schema is used by the Attestor for predicate validation and has a different shape than the wire payload. It is the origin of the historical example previously shown in this document. Its required fields are witness_id, witness_hash, provenance, path.
{
"witness_id": "uuid",
"witness_hash": "sha256:abcd1234... (pattern ^(blake3|sha256):[a-f0-9]{64}$)",
"witness_type": "reachability_path (enum: reachability_path | gate_proof; default reachability_path)",
"provenance": {
"graph_hash": "sha256:efgh5678... (required)",
"scan_id": "uuid",
"run_id": "uuid",
"analyzer_version": "1.0.0 (required)",
"analysis_timestamp": "2025-12-18T11:59:00Z (required)"
},
"path": {
"entrypoint": { "fqn": "...", "kind": "http_handler", "location": {"file": "...", "line": 42}, "node_hash": "sha256:..." },
"sink": { "fqn": "...", "cve": "CVE-2021-44228", "package": "pkg:maven/...", "node_hash": "sha256:..." },
"steps": [
{ "index": 0, "fqn": "...", "call_site": "...:45", "edge_type": "call", "node_hash": "sha256:..." }
],
"hop_count": 3,
"path_hash": "sha256:...",
"node_hashes": ["sha256:..."]
},
"gates": [
{ "type": "auth_required", "location": "MyController.java:40", "description": "Requires authenticated user" }
],
"evidence": {
"graph_fragment_hash": "sha256:ijkl9012...",
"path_hash": "sha256:mnop3456..."
},
"evidence_uris": {
"graph": "cas://sha256:...",
"sbom": "cas://sha256:...",
"attestation": "cas://sha256:...",
"rekor": "https://rekor.sigstore.dev/..."
}
}
Predicate enum domains (from the JSON Schema):
| Field | Allowed values |
|---|---|
witness_type | reachability_path, gate_proof |
path.entrypoint.kind | http_handler, grpc_handler, cli_main, scheduler, message_handler, other |
path.steps[].edge_type | call, virtual, static, sink, interface, delegate |
gates[].type | auth_required, feature_flag, admin_only, non_default_config, rate_limited, other |
Hash patterns in the predicate schema accept either
blake3:orsha256:prefixes (^(blake3\|sha256):[a-f0-9]{64}$), exceptpath.path_hash,node_hashes, and*.node_hashwhich are^sha256:[a-f0-9]{64}$only.
Witness ID (content addressing)
The wire witness_id is a content address, not a UUID. It is computed by PathWitnessBuilder.ComputeWitnessId over a fixed subset of fields:
- Build a canonical projection containing exactly:
witness_schema,artifact,vuln,entrypoint,path,sink,evidence(snake_case, no indentation, Web defaults). Note this projection excludesgates,observed_at,path_hash,node_hashes,evidence_uris,predicate_type, and the runtime fields. - Hash the UTF-8 bytes via
ICryptoHash.ComputePrefixedHashForPurpose(json, HashPurpose.Content). The default compliance profile mapsHashPurpose.Contentto SHA-256 (prefixsha256:); the FIPS profile is also SHA-256. - Prefix the result with
wit:(WitnessSchema.WitnessIdPrefix).
Result format: wit:sha256:<64-hex>.
var canonical = new {
witness.WitnessSchema, witness.Artifact, witness.Vuln,
witness.Entrypoint, witness.Path, witness.Sink, witness.Evidence
};
var json = JsonSerializer.SerializeToUtf8Bytes(canonical, jsonOptions);
var hash = _cryptoHash.ComputePrefixedHashForPurpose(json, HashPurpose.Content); // "sha256:<hex>"
var witnessId = $"{WitnessSchema.WitnessIdPrefix}{hash}"; // "wit:sha256:<hex>"
The separate
witness_hashvalue (predicate / storage column) follows the predicate schema pattern^(blake3|sha256):[a-f0-9]{64}$and is a hash of the canonical witness payload. It is distinct fromwitness_id. There is no BLAKE3-over-the-whole-witness-excluding-created_at recipe in the implementation.
DSSE Constants
Sprint: SPRINT_3700_0001_0001 (WIT-007C)
The following constants are used for DSSE envelope creation and verification:
| Constant | Value | Location |
|---|---|---|
| Payload Type | application/vnd.stellaops.witness.v1+json | WitnessSchema.DssePayloadType |
| Schema Version | stellaops.witness.v1 | WitnessSchema.Version |
| Witness ID prefix | wit: | WitnessSchema.WitnessIdPrefix |
| JSON Schema URI | https://stellaops.org/schemas/witness-v1.json | WitnessSchema.JsonSchemaUri |
| Legacy predicate constant | stella.ops/pathWitness@v1 | WitnessSchema.PredicateType, PredicateTypes.StellaOpsPathWitness |
| Canonical predicate (default) | https://stella.ops/predicates/path-witness/v1 | WitnessPredicateTypes.PathWitnessCanonical |
Note:
WitnessSchema.PredicateTypestill equals the legacy short formstella.ops/pathWitness@v1, butPathWitness.predicate_typedefaults to the canonicalhttps://stella.ops/predicates/path-witness/v1(see Canonical Predicate Type and Aliases). The DSSEpayloadTypewritten byWitnessDsseSignerisWitnessSchema.DssePayloadType(application/vnd.stellaops.witness.v1+json), not an in-toto predicate type.WitnessSchema.JsonSchemaUri(https://stellaops.org/schemas/witness-v1.json) is the wire-schema URI and is distinct from the in-toto predicate schema$idhttps://stella.ops/schemas/predicates/path-witness/v1.
Witness Types
Defined as WitnessSchema.WitnessTypeReachabilityPath / WitnessTypeGateProof. These values populate the storage witness_type column and the in-toto predicate witness_type enum. The wire PathWitness record itself has no witness_type field — it always represents a reachability path.
| Value | Description |
|---|---|
reachability_path | Path witness from entrypoint to vulnerable sink |
gate_proof | Evidence of mitigating control (gate) along path |
Canonical Predicate Type and Aliases
Sprint: SPRINT_20260112_004_SCANNER_path_witness_nodehash
Sprint: SPRINT_20260112_008_DOCS_path_witness_contracts (PW-DOC-001)
The canonical predicate type for path witnesses is:
https://stella.ops/predicates/path-witness/v1
The following aliases are recognized for backward compatibility:
| Alias | Status |
|---|---|
stella.ops/pathWitness@v1 | Active (legacy short form) |
https://stella.ops/pathWitness/v1 | Active (URL variant) |
Consumers must accept all aliases when verifying; producers should emit the canonical form.
Node Hash Recipe
Canonical node hash recipe for deterministic static/runtime evidence joining.
Recipe
NodeHash = SHA256(normalize(PURL) + ":" + normalize(SYMBOL_FQN))
Output format: sha256:<64-hex-chars>
PURL Normalization Rules
- Lowercase scheme (
pkg:) - Lowercase type (e.g.,
NPM->npm) - Preserve namespace/name case (some ecosystems are case-sensitive)
- Sort qualifiers alphabetically by key
- Remove trailing slashes
- Normalize empty version to
unversioned
Symbol FQN Normalization Rules
- Trim whitespace
- Normalize multiple dots (
..) to single dot - Normalize signature whitespace:
(type,type)->(type, type) - Empty signatures become
() - Collapse the module-level placeholder
._.to.
Caveats (matching the current
NodeHashRecipecode, not just its XML comments): rules 5/6 above are partial.NormalizePurllowercases the scheme and type and sorts qualifiers, but does not substitute an empty version withunversioned(despite the doc-comment).NormalizeSymbolFqncollapses the literal._.placeholder rather than stripping bare_type placeholders.
Example
Input:
PURL: pkg:npm/lodash@4.17.21
Symbol: lodash.merge(object, object)
Normalized Input:
"pkg:npm/lodash@4.17.21:lodash.merge(object, object)"
Output:
sha256:a1b2c3d4e5f6... (64 hex chars)
Implementation
See src/__Libraries/StellaOps.Reachability.Core/NodeHashRecipe.cs (NodeHashRecipe.ComputeHash / NormalizePurl / NormalizeSymbolFqn). The normalization rules above describe this canonical recipe.
Divergence warning:
PathWitnessBuilderdoes not callNodeHashRecipe. Its privateComputeNodeHashuses a simplified normalization — it lowercases the entire PURL (purl.Trim().ToLowerInvariant()) and does not sort qualifiers or expand empty versions — then hashesSHA256(normalizedPurl + ":" + symbolFqn.Trim()). The two recipes can therefore produce different node hashes for the same input. The canonicalNodeHashRecipeis the intended long-term recipe; the builder’s inline variant is the value actually emitted inPathWitness.node_hashestoday.
Path Hash Recipe
Canonical path hash recipe for deterministic path fingerprinting.
Recipe
PathHash = SHA256(nodeHash1 + ">" + nodeHash2 + ">" + ... + nodeHashN)
The > separator represents directed edges in the path.
Top-K Selection
For efficiency, witnesses include a top-K subset of node hashes:
- Take first K/2 nodes (entry points)
- Take last K/2 nodes (exit/vulnerable points)
- Deduplicate while preserving order
- Default K = 10
PathFingerprint Fields
The PathFingerprint record (PathHashRecipe.CreateFingerprint) carries:
| Field (C# property) | Type | Description |
|---|---|---|
PathHash | string | sha256:<hex> of full path |
NodeCount | integer | Total nodes in path |
TopKNodeHashes | array | Top-K node hashes for lookup |
SourceNodeHash | string | Hash of entry (first) node |
SinkNodeHash | string | Hash of sink (last) node |
PathFingerprintis an in-memory record only; it is not embedded as apath_fingerprintobject in the witness JSON. The witness instead carries the flatpath_hashandnode_hashesfields.
Implementation
See src/__Libraries/StellaOps.Reachability.Core/PathHashRecipe.cs (PathHashRecipe.ComputeHash / ComputeWithTopK / CreateFingerprint).
Divergence warning: the
>-separated recipe above is the canonicalPathHashRecipe, but thepath_hashactually emitted on aPathWitnessis produced byPathWitnessBuilder’s privateComputeCombinedPathHash, which uses a different recipe:"path:sha256:" + SHA256(hexPart1 + ":" + hexPart2 + ... )— i.e. apath:sha256:prefix and a:separator over the hex parts of each node hash (not the>directed-edge separator, and not the baresha256:prefix). The builder also computes node hashes over all path steps for the path hash, while exposing only the deduplicated top-K (sorted, K=10) innode_hashes.
Evidence URI Fields
There are two distinct evidence-URI conventions; do not mix them.
Wire schema — evidence_uris (flat string array)
In the wire payload, evidence_uris is an array of strings built by PathWitnessBuilder.BuildEvidenceUris. Each entry uses the evidence:<kind>:<value> scheme and is emitted only when the corresponding request field is present:
| Entry | Emitted when | Example |
|---|---|---|
evidence:callgraph:<callgraphDigest> | CallgraphDigest set | evidence:callgraph:blake3:... |
evidence:sbom:<sbomDigest> | SbomDigest set | evidence:sbom:sha256:... |
evidence:surface:<surfaceDigest> | SurfaceDigest set | evidence:surface:sha256:... |
evidence:build:<buildId> | BuildId set | evidence:build:gnu-build-id:... |
{
"evidence_uris": [
"evidence:callgraph:blake3:abc123...",
"evidence:sbom:sha256:def456..."
]
}
In-toto predicate schema — evidence_uris (object with cas://)
The predicate JSON Schema (stellaops-path-witness.v1.schema.json) instead defines evidence_uris as an object whose graph/sbom/attestation values match ^cas://sha256:[a-f0-9]{64}$ and whose rekor value is a free-form URI:
{
"evidence_uris": {
"graph": "cas://sha256:abc123...",
"sbom": "cas://sha256:def456...",
"attestation": "cas://sha256:ghi789...",
"rekor": "https://rekor.sigstore.dev/api/v1/log/entries/abc123def456"
}
}
There are no
graph_uri/sbom_uri/attestation_uri/rekor_uriscalar fields anywhere in the implementation.
stella:// evidence URIs (separate builder)
StellaOps.Reachability.Core.EvidenceUriBuilder produces a third, unrelated URI scheme used for ReachGraph/runtime-fact references — stella://reachgraph/<digest>, stella://signals/runtime/<tenant>/<digest>, stella://cvemap/<cve>, stella://attestation/<digest>, stella://hybrid/<digest> — not cas:// and not the witness evidence: scheme.
Runtime Symbolization Tuple
Runtime witnesses — those whose observation_type is Runtime (1) or Confirmed (2), i.e. anything other than Static (0), or that carry a non-empty observations array — must include a deterministic symbolization tuple. This is enforced by PathWitness.ValidateDeterministicSymbolization, which WitnessDsseSigner calls before signing and during verification (a runtime witness with a missing or invalid symbolization tuple fails both signing and verification):
{
"symbolization": {
"build_id": "gnu-build-id:...",
"debug_artifact_uri": "cas://symbols/by-build-id/.../artifact.debug",
"symbol_table_uri": "cas://symbols/by-build-id/.../symtab.json",
"symbolizer": {
"name": "llvm-symbolizer",
"version": "18.1.7",
"digest": "sha256:..."
},
"libc_variant": "glibc",
"sysroot_digest": "sha256:..."
}
}
Validation rules:
build_id,symbolizer.name,symbolizer.version,symbolizer.digest,libc_variant, andsysroot_digestare required.- At least one of
debug_artifact_uriorsymbol_table_urimust be present. - Missing runtime symbolization inputs must fail witness signing/verification validation.
- Runtime observation arrays must be canonicalized before witness hashing/signing (stable sort by timestamp and deterministic tiebreakers) so equivalent inputs produce byte-identical DSSE payloads.
Runtime Witness Artifact Triplet (MWD-004)
Source of truth:
src/EvidenceLocker/__Libraries/StellaOps.EvidenceLocker.Export/Models/BundleManifest.cs(ArtifactEntry,RuntimeWitnessIndexKey,RuntimeWitnessArtifactRoles,BundlePaths,BundleMediaTypes).
Runtime witnesses exported through Evidence Locker use a deterministic three-file profile, stored under the bundle’s runtime-witnesses/ directory (BundlePaths.RuntimeWitnessesDirectory) and indexed via the manifest’s runtimeWitnesses array:
trace.json- canonical witness payload (media typeapplication/vnd.stellaops.witness.v1+json)trace.dsse.json- DSSE envelope overtrace.json(media typeapplication/vnd.dsse.envelope+json)trace.sigstore.json- Sigstore bundle for offline replay (media typeapplication/vnd.dev.sigstore.bundle.v0.3+json)
Each manifest ArtifactEntry links to:
witnessIdwitnessRole— one oftrace,dsse,sigstore_bundle(RuntimeWitnessArtifactRoles)witnessIndex— the deterministic replay lookup keys (RuntimeWitnessIndexKey):buildIdkernelReleaseprobeIdpolicyRunId
linkedArtifacts— paths of the sibling triplet files
(Field names above are the JSON property names; C# uses PascalCase, e.g. WitnessRole.)
Offline verification must use only bundle-contained artifacts; no network lookups are required for triplet integrity checks.
Cross-Distro Replay Matrix Verification (MWD-005)
Deterministic replay verification must include a minimum matrix of:
- Three kernel releases.
- Both
glibcandmusllibc variants. - Fixed witness artifacts replayed across matrix rows with byte-identical replay-frame output.
QA evidence for MWD-005 was intended to be captured at:
docs/qa/feature-checks/runs/signals/ebpf-micro-witness-determinism/run-001/tier2-replay-matrix-tests.logdocs/qa/feature-checks/runs/signals/ebpf-micro-witness-determinism/run-001/tier2-replay-matrix-summary.json
Status — evidence pending. Neither file exists in the repository yet, and the
ebpf-micro-witness-determinism/run-001/directory is absent. The feature is documented atdocs/modules/signals/contracts/ebpf-micro-witness-determinism-profile.mdanddocs/product/ebpf-micro-witness-determinism.md, but the cited replay-matrix run artifacts have not yet been produced. Treat the matrix requirement as a roadmap target until the evidence lands.
DSSE Signing
Witnesses are signed by WitnessDsseSigner.SignWitness using DSSE (Dead Simple Signing Envelope). The payload is the witness serialized via CanonJson.Canonicalize with snake_case naming, no indentation, and null-omission (DefaultIgnoreCondition.WhenWritingNull). The signature is produced over the DSSE PAE by the shared EnvelopeSignatureService and stored base64 (standard, via Convert.ToBase64String):
{
"payloadType": "application/vnd.stellaops.witness.v1+json",
"payload": "<base64-encoded canonical witness JSON>",
"signatures": [
{
"keyid": "<signing key id>",
"sig": "<base64-encoded signature>"
}
]
}
Verification
WitnessDsseSigner.VerifyWitness performs, in order:
- Check
payloadTypeequalsapplication/vnd.stellaops.witness.v1+json(else reject). - Deserialize the payload into
PathWitness. - Check
witness_schemaequalsstellaops.witness.v1(else reject as unsupported schema). - Run
ValidateDeterministicSymbolization()— runtime/confirmed witnesses must carry a validsymbolizationtuple. - Locate the signature whose
keyidmatches the supplied public key, then verify it viaEnvelopeSignatureService.VerifyDsse.
Verification does not recompute
witness_hashorwitness_id; it validates payload type, schema version, symbolization, and the signature. Transparency-log inclusion is out of scope of the signer/verifier and is not performed here.
Storage
Source of truth:
src/Scanner/__Libraries/StellaOps.Scanner.Storage/Postgres/Migrations/013_witness_storage.sql. The migration is idempotent (CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS).
Witnesses are stored in the scanner.witnesses table. Note this row uses the predicate-style fields (witness_hash, witness_type, graph_hash, created_at) alongside the full wire payload:
| Column | Type | Description |
|---|---|---|
witness_id | UUID | Primary key (gen_random_uuid() default). This is the DB row ID — not the wire wit:sha256: content address |
witness_hash | TEXT | Witness payload hash; UNIQUE (uk_witness_hash) |
schema_version | TEXT | Default stellaops.witness.v1 |
witness_type | TEXT | reachability_path, gate_proof, etc. |
graph_hash | TEXT | Source rich-graph reference (NOT NULL) |
scan_id | UUID | Nullable |
run_id | UUID | Nullable |
payload_json | JSONB | Full PathWitness wire JSON |
dsse_envelope | JSONB | Signed envelope (nullable until signed) |
created_at | TIMESTAMPTZ | Default now() |
signed_at | TIMESTAMPTZ | Nullable |
signer_key_id | TEXT | Nullable |
entrypoint_fqn | TEXT | Lookup-by-entrypoint (nullable) |
sink_cve | TEXT | Lookup-by-CVE (nullable) |
Indexes: ix_witnesses_graph_hash, ix_witnesses_scan_id (partial), ix_witnesses_sink_cve (partial), ix_witnesses_entrypoint (partial), ix_witnesses_created_at (DESC), and a GIN index ix_witnesses_payload_gin on payload_json.
A companion audit table scanner.witness_verifications records each verification attempt (verification_id, witness_id FK, verified_at, verified_by, verification_status, verification_error, verifier_key_id).
API Endpoints
Source of truth:
src/Scanner/StellaOps.Scanner.WebService/Endpoints/WitnessEndpoints.cs, mounted under the API base path/api/v1(ScannerWebServiceOptions.Api.BasePath). All endpoints require thescanner.scans.readauthorization policy (ScannerPolicies.ScansRead).
| Method | Path | Description |
|---|---|---|
GET | /api/v1/witnesses/{witnessId:guid} | Get witness by row ID (UUID) |
GET | /api/v1/witnesses/by-hash/{witnessHash} | Get witness by witness_hash |
GET | /api/v1/witnesses?scanId={scanId} | List witnesses for a scan |
GET | /api/v1/witnesses?cve={cve} | List witnesses for a CVE |
GET | /api/v1/witnesses?graphHash={graphHash} | List witnesses for a graph hash |
POST | /api/v1/witnesses/{witnessId:guid}/verify | Verify witness signature (audited) |
The list endpoint returns an empty list when none of
scanId/cve/graphHashis supplied (it deliberately avoids a full-table scan). The path parameter is the DB-row UUIDwitnessId, and the list query parameter isscanId(notscan). There is noPOST /api/v1/witnessescreate endpoint — witnesses are produced by the Scanner pipeline, not created over the API.
Authorization
The witness endpoints reuse the Scanner read policy scanner.scans.read. There are no witness-specific scopes in the canonical scope catalog (src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs), which defines Scanner scopes such as scanner:read, scanner:scan, scanner:export, and scanner:write. The scanner.scans.read policy/scope name is a Scanner-local constant (ScannerPolicies.ScansRead / ScannerAuthorityScopes.ScansRead) and is not present in the canonical StellaOpsScopes catalog.
CLI
stella witness <show|list|verify|export> (src/Cli/StellaOps.Cli/Commands/CommandHandlers.Witness.cs) wrap the API. witness list accepts --scan, --vuln, --tier, --reachable-only, --probe-type, --limit; witness export supports json, dsse, and sarif formats.
