Files
git.stella-ops.org/docs/modules/authority/verdict-manifest.md
StellaOps Bot 634233dfed feat: Implement distro-native version comparison for RPM, Debian, and Alpine packages
- Add RpmVersionComparer for RPM version comparison with epoch, version, and release handling.
- Introduce DebianVersion for parsing Debian EVR (Epoch:Version-Release) strings.
- Create ApkVersion for parsing Alpine APK version strings with suffix support.
- Define IVersionComparator interface for version comparison with proof-line generation.
- Implement VersionComparisonResult struct to encapsulate comparison results and proof lines.
- Add tests for Debian and RPM version comparers to ensure correct functionality and edge case handling.
- Create project files for the version comparison library and its tests.
2025-12-22 09:50:12 +02:00

11 KiB

Verdict Manifest Specification

Status: Draft (Sprint 7100) Last Updated: 2025-12-22 Source Advisory: docs/product-advisories/archived/22-Dec-2026 - Building a Trust Lattice for VEX Sources.md

1. Overview

A Verdict Manifest is a signed, immutable record of a VEX decisioning outcome. It captures all inputs used to produce a verdict, enabling deterministic replay and audit compliance.

Purpose

  1. Auditability: Prove exactly how a verdict was reached
  2. Reproducibility: Replay the decision with identical results
  3. Compliance: Meet regulatory requirements for security decisions
  4. Debugging: Investigate unexpected verdict changes

2. Manifest Schema

2.1 Complete Schema

interface VerdictManifest {
  // Identity
  manifestId: string;      // Unique identifier
  tenant: string;          // Tenant scope

  // Scope
  assetDigest: string;     // SHA256 of asset/SBOM
  vulnerabilityId: string; // CVE/GHSA/vendor ID

  // Inputs (pinned for replay)
  inputs: VerdictInputs;

  // Result
  result: VerdictResult;

  // Policy context
  policyHash: string;      // SHA256 of policy file
  latticeVersion: string;  // Trust lattice version

  // Metadata
  evaluatedAt: string;     // ISO 8601 UTC timestamp
  manifestDigest: string;  // SHA256 of canonical manifest
}

interface VerdictInputs {
  sbomDigests: string[];           // SBOM document digests
  vulnFeedSnapshotIds: string[];   // Feed snapshot identifiers
  vexDocumentDigests: string[];    // VEX document digests
  reachabilityGraphIds: string[];  // Call graph identifiers
  clockCutoff: string;             // Evaluation timestamp
}

interface VerdictResult {
  status: "affected" | "not_affected" | "fixed" | "under_investigation";
  confidence: number;              // 0.0 to 1.0
  explanations: VerdictExplanation[];
  evidenceRefs: string[];          // Attestation/proof references
}

interface VerdictExplanation {
  sourceId: string;
  reason: string;
  provenanceScore: number;
  coverageScore: number;
  replayabilityScore: number;
  strengthMultiplier: number;
  freshnessMultiplier: number;
  claimScore: number;
}

2.2 Identity Fields

Field Type Description
manifestId string Format: verd:{tenant}:{asset_short}:{vuln_id}:{timestamp}
tenant string Tenant identifier for multi-tenancy

2.3 Input Pinning

All inputs that affect the verdict must be pinned for deterministic replay:

Field Description
sbomDigests SHA256 digests of SBOM documents used
vulnFeedSnapshotIds Identifiers for vulnerability feed snapshots
vexDocumentDigests SHA256 digests of VEX documents considered
reachabilityGraphIds Identifiers for call graph snapshots
clockCutoff Timestamp used for freshness calculations

2.4 Verdict Result

The result section contains the actual verdict and full explanation:

Field Description
status Final verdict: affected, not_affected, fixed, under_investigation
confidence Numeric confidence score (0.0 to 1.0)
explanations Per-source breakdown of scoring
evidenceRefs Links to attestations and proof bundles

3. Deterministic Serialization

3.1 Canonical JSON

Manifests are serialized using canonical JSON rules:

  1. Keys sorted alphabetically (ASCII order)
  2. No insignificant whitespace
  3. UTF-8 encoding without BOM
  4. Timestamps in ISO 8601 format with 'Z' suffix
  5. Arrays sorted by natural key (sourceId, then score)
  6. Numbers without trailing zeros

3.2 Digest Computation

The manifest digest is computed over the canonical JSON:

public static string ComputeDigest(VerdictManifest manifest)
{
    var json = CanonicalJsonSerializer.Serialize(manifest with { ManifestDigest = "" });
    var bytes = Encoding.UTF8.GetBytes(json);
    var hash = SHA256.HashData(bytes);
    return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}

4. Signing

4.1 DSSE Envelope

Verdict manifests are signed using DSSE (Dead Simple Signing Envelope):

{
  "payloadType": "application/vnd.stellaops.verdict+json",
  "payload": "<base64-encoded-manifest>",
  "signatures": [
    {
      "keyid": "projects/stellaops/keys/verdict-signer-2025",
      "sig": "<base64-encoded-signature>"
    }
  ]
}

4.2 Predicate Type

For in-toto attestations:

https://stella-ops.org/attestations/vex-verdict/1

4.3 Rekor Integration

Optionally, verdicts can be logged to Sigstore Rekor for transparency:

{
  "rekorLogId": "rekor.sigstore.dev",
  "rekorLogIndex": 12345678,
  "rekorEntryUrl": "https://rekor.sigstore.dev/api/v1/log/entries/..."
}

5. Storage

5.1 PostgreSQL Schema

CREATE TABLE authority.verdict_manifests (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    manifest_id TEXT NOT NULL UNIQUE,
    tenant TEXT NOT NULL,

    -- Scope
    asset_digest TEXT NOT NULL,
    vulnerability_id TEXT NOT NULL,

    -- Inputs (JSONB)
    inputs_json JSONB NOT NULL,

    -- Result
    status TEXT NOT NULL,
    confidence DOUBLE PRECISION NOT NULL,
    result_json JSONB NOT NULL,

    -- Policy context
    policy_hash TEXT NOT NULL,
    lattice_version TEXT NOT NULL,

    -- Metadata
    evaluated_at TIMESTAMPTZ NOT NULL,
    manifest_digest TEXT NOT NULL,

    -- Signature
    signature_json JSONB,
    rekor_log_id TEXT,

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

5.2 Indexing Strategy

Index Purpose
(tenant, asset_digest, vulnerability_id) Primary lookup
(tenant, policy_hash, lattice_version) Policy version queries
(evaluated_at) BRIN Time-based queries
Unique: (tenant, asset_digest, vulnerability_id, policy_hash, lattice_version) Ensure one verdict per configuration

6. Replay Verification

6.1 Verification Protocol

  1. Retrieve stored manifest by ID
  2. Fetch pinned inputs (SBOM, VEX docs, feeds) by digest
  3. Re-execute trust lattice evaluation with identical inputs
  4. Compare result with stored verdict
  5. Verify signature if present

6.2 Verification Request

POST /api/v1/authority/verdicts/{manifestId}/replay
Authorization: Bearer <token>

6.3 Verification Response

{
  "success": true,
  "originalManifest": { ... },
  "replayedManifest": { ... },
  "differences": [],
  "signatureValid": true,
  "verifiedAt": "2025-12-22T15:30:00Z"
}

6.4 Failure Handling

When replay produces different results:

{
  "success": false,
  "originalManifest": { ... },
  "replayedManifest": { ... },
  "differences": [
    {
      "field": "result.confidence",
      "original": 0.82,
      "replayed": 0.79,
      "reason": "VEX document digest mismatch"
    }
  ],
  "signatureValid": true,
  "error": "Verdict replay produced different confidence score"
}

7. API Reference

Get Verdict Manifest

GET /api/v1/authority/verdicts/{manifestId}
Authorization: Bearer <token>

Response: 200 OK
{
  "manifest": { ... },
  "signature": { ... },
  "rekorEntry": { ... }
}

List Verdicts by Scope

GET /api/v1/authority/verdicts?assetDigest={digest}&vulnerabilityId={cve}
Authorization: Bearer <token>

Response: 200 OK
{
  "verdicts": [ ... ],
  "pageToken": "..."
}

Replay Verdict

POST /api/v1/authority/verdicts/{manifestId}/replay
Authorization: Bearer <token>

Response: 200 OK
{
  "success": true,
  ...
}

Download Signed Manifest

GET /api/v1/authority/verdicts/{manifestId}/download
Authorization: Bearer <token>

Response: 200 OK
Content-Type: application/vnd.stellaops.verdict+json
Content-Disposition: attachment; filename="verdict-{manifestId}.json"

8. JSON Schema

verdict-manifest.schema.json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://stella-ops.org/schemas/verdict-manifest/1.0.0",
  "type": "object",
  "required": [
    "manifestId",
    "tenant",
    "assetDigest",
    "vulnerabilityId",
    "inputs",
    "result",
    "policyHash",
    "latticeVersion",
    "evaluatedAt",
    "manifestDigest"
  ],
  "properties": {
    "manifestId": {
      "type": "string",
      "pattern": "^verd:[a-z0-9-]+:[a-f0-9]+:[A-Z0-9-]+:[0-9]+$"
    },
    "tenant": { "type": "string", "minLength": 1 },
    "assetDigest": {
      "type": "string",
      "pattern": "^sha256:[a-f0-9]{64}$"
    },
    "vulnerabilityId": { "type": "string", "minLength": 1 },
    "inputs": { "$ref": "#/$defs/VerdictInputs" },
    "result": { "$ref": "#/$defs/VerdictResult" },
    "policyHash": {
      "type": "string",
      "pattern": "^sha256:[a-f0-9]{64}$"
    },
    "latticeVersion": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
    },
    "evaluatedAt": {
      "type": "string",
      "format": "date-time"
    },
    "manifestDigest": {
      "type": "string",
      "pattern": "^sha256:[a-f0-9]{64}$"
    }
  },
  "$defs": {
    "VerdictInputs": {
      "type": "object",
      "required": ["sbomDigests", "vulnFeedSnapshotIds", "vexDocumentDigests", "clockCutoff"],
      "properties": {
        "sbomDigests": {
          "type": "array",
          "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
        },
        "vulnFeedSnapshotIds": {
          "type": "array",
          "items": { "type": "string" }
        },
        "vexDocumentDigests": {
          "type": "array",
          "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
        },
        "reachabilityGraphIds": {
          "type": "array",
          "items": { "type": "string" }
        },
        "clockCutoff": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "VerdictResult": {
      "type": "object",
      "required": ["status", "confidence", "explanations"],
      "properties": {
        "status": {
          "type": "string",
          "enum": ["affected", "not_affected", "fixed", "under_investigation"]
        },
        "confidence": {
          "type": "number",
          "minimum": 0,
          "maximum": 1
        },
        "explanations": {
          "type": "array",
          "items": { "$ref": "#/$defs/VerdictExplanation" }
        },
        "evidenceRefs": {
          "type": "array",
          "items": { "type": "string" }
        }
      }
    },
    "VerdictExplanation": {
      "type": "object",
      "required": ["sourceId", "reason", "claimScore"],
      "properties": {
        "sourceId": { "type": "string" },
        "reason": { "type": "string" },
        "provenanceScore": { "type": "number", "minimum": 0, "maximum": 1 },
        "coverageScore": { "type": "number", "minimum": 0, "maximum": 1 },
        "replayabilityScore": { "type": "number", "minimum": 0, "maximum": 1 },
        "strengthMultiplier": { "type": "number", "minimum": 0, "maximum": 1 },
        "freshnessMultiplier": { "type": "number", "minimum": 0, "maximum": 1 },
        "claimScore": { "type": "number", "minimum": 0, "maximum": 1 }
      }
    }
  }
}