- 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.
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
- Auditability: Prove exactly how a verdict was reached
- Reproducibility: Replay the decision with identical results
- Compliance: Meet regulatory requirements for security decisions
- 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:
- Keys sorted alphabetically (ASCII order)
- No insignificant whitespace
- UTF-8 encoding without BOM
- Timestamps in ISO 8601 format with 'Z' suffix
- Arrays sorted by natural key (sourceId, then score)
- 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
- Retrieve stored manifest by ID
- Fetch pinned inputs (SBOM, VEX docs, feeds) by digest
- Re-execute trust lattice evaluation with identical inputs
- Compare result with stored verdict
- 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 }
}
}
}
}