Files
git.stella-ops.org/docs/contracts/artifact-canonical-record-v1.md
2026-02-19 22:07:11 +02:00

7.6 KiB

Contract: Artifact Canonical Record (v1)

Status

  • Status: DRAFT (2026-02-19)
  • Owners: Attestor Guild, EvidenceLocker Guild
  • Consumers: Evidence Thread API, Audit Pack Export, Console UI
  • Sprint: SPRINT_20260219_009 (CID-03)

Purpose

Define a unified document that aggregates all evidence references for a single artifact identified by canonical_id. Today this information is distributed across Attestor (dsse_envelopes), Scanner (SBOM refs), VexLens (consensus records), and OCI registries (referrers). The Artifact Canonical Record provides a single query target for:

  1. The Evidence Thread API (GET /api/v1/evidence/thread/{id})
  2. Audit pack generation (export all evidence for an artifact)
  3. Console UI proof chain visualization

Record Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Artifact Canonical Record v1",
  "type": "object",
  "required": ["canonical_id", "format", "sbom_ref", "created_at"],
  "properties": {
    "canonical_id": {
      "type": "string",
      "description": "sha256:<hex> computed per canonical-sbom-id-v1.md",
      "pattern": "^sha256:[a-f0-9]{64}$"
    },
    "format": {
      "type": "string",
      "description": "Canonicalization format identifier",
      "const": "cyclonedx-jcs:1"
    },
    "sbom_ref": {
      "type": "string",
      "description": "Content-addressable reference to the SBOM (CAS URI or OCI ref)",
      "examples": [
        "cas://sbom/inventory/abc123.json",
        "oci://registry/repo@sha256:abc123"
      ]
    },
    "attestations": {
      "type": "array",
      "description": "All DSSE attestations referencing this artifact",
      "items": {
        "type": "object",
        "required": ["predicate_type", "dsse_digest", "signed_at"],
        "properties": {
          "predicate_type": {
            "type": "string",
            "description": "Predicate type URI from the predicate registry"
          },
          "dsse_digest": {
            "type": "string",
            "description": "SHA-256 of the DSSE envelope body",
            "pattern": "^sha256:[a-f0-9]{64}$"
          },
          "signer_keyid": {
            "type": "string",
            "description": "Key ID of the signer"
          },
          "rekor_entry_id": {
            "type": "string",
            "description": "Rekor transparency log entry UUID (null if offline)"
          },
          "rekor_tile": {
            "type": "string",
            "description": "Rekor tile URL for inclusion proof verification"
          },
          "signed_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "referrers": {
      "type": "array",
      "description": "OCI referrers (symbol bundles, attestation manifests)",
      "items": {
        "type": "object",
        "required": ["media_type", "descriptor_digest"],
        "properties": {
          "media_type": {
            "type": "string",
            "description": "OCI media type",
            "examples": [
              "application/vnd.stella.symbols+tar",
              "application/vnd.in-toto+json"
            ]
          },
          "descriptor_digest": {
            "type": "string",
            "pattern": "^sha256:[a-f0-9]{64}$"
          },
          "registry": {
            "type": "string",
            "description": "Registry hostname"
          }
        }
      }
    },
    "vex_refs": {
      "type": "array",
      "description": "VEX consensus records targeting this artifact",
      "items": {
        "type": "object",
        "required": ["vulnerability_id", "consensus_status"],
        "properties": {
          "vulnerability_id": {
            "type": "string",
            "description": "CVE or advisory ID"
          },
          "consensus_status": {
            "type": "string",
            "enum": ["affected", "not_affected", "under_investigation", "fixed"]
          },
          "confidence_score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "consensus_digest": {
            "type": "string",
            "description": "SHA-256 of the VexLens consensus record"
          },
          "dsse_digest": {
            "type": "string",
            "description": "SHA-256 of the VEX attestation DSSE (if signed)"
          },
          "rekor_tile": {
            "type": "string",
            "description": "Rekor tile URL (if anchored)"
          }
        }
      }
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time"
    }
  }
}

Content Addressing

The record itself is content-addressed:

record_id := sha256(JCS(record_json))

This enables deduplication and integrity verification of the record.

Storage Design

Create a PostgreSQL materialized view joining existing tables:

CREATE MATERIALIZED VIEW proofchain.artifact_canonical_records AS
SELECT
    se.bom_digest AS canonical_id,
    'cyclonedx-jcs:1' AS format,
    se.artifact_digest,
    se.purl,
    se.created_at,
    -- Attestations aggregated as JSONB array
    COALESCE(
        jsonb_agg(DISTINCT jsonb_build_object(
            'predicate_type', de.predicate_type,
            'dsse_digest', de.body_hash,
            'signer_keyid', de.signer_keyid,
            'rekor_entry_id', re.uuid,
            'rekor_tile', re.log_id,
            'signed_at', de.signed_at
        )) FILTER (WHERE de.env_id IS NOT NULL),
        '[]'::jsonb
    ) AS attestations
FROM proofchain.sbom_entries se
LEFT JOIN proofchain.dsse_envelopes de ON de.entry_id = se.entry_id
LEFT JOIN proofchain.rekor_entries re ON re.env_id = de.env_id
GROUP BY se.entry_id, se.bom_digest, se.artifact_digest, se.purl, se.created_at;

CREATE UNIQUE INDEX idx_acr_canonical_id ON proofchain.artifact_canonical_records(canonical_id);

Refresh strategy: REFRESH MATERIALIZED VIEW CONCURRENTLY triggered by:

  • New DSSE envelope submission
  • New Rekor entry confirmation
  • Periodic schedule (every 5 minutes)

Option B: Dedicated Table (for v2 if write patterns emerge)

If the record needs to be written to directly (e.g., for referrers and VEX refs that come from different services), migrate to a dedicated table with trigger-based population from upstream sources.

API Integration

Evidence Thread Endpoint

GET /api/v1/evidence/thread/{canonical_id}

Returns the Artifact Canonical Record for the given canonical_id. The response includes all attestations, referrers, and VEX refs.

Query parameters:

  • include_attestations (boolean, default true)
  • include_vex (boolean, default true)
  • include_referrers (boolean, default true)

Evidence Thread by PURL

GET /api/v1/evidence/thread?purl={purl}

Resolves PURL to canonical_id via sbom_entries table, then returns the record.

Offline Mode

When Rekor is unavailable (air-gapped deployment):

  • rekor_entry_id and rekor_tile fields are null
  • A transparency_status field indicates "offline" with a reason sub-field
  • Verification uses bundled checkpoints instead of live Rekor queries

Determinism

The record's content is deterministic given frozen inputs:

  • Attestation arrays sorted by signed_at ascending, then predicate_type ascending
  • VEX refs sorted by vulnerability_id ascending
  • Referrers sorted by media_type ascending, then descriptor_digest ascending

References

  • docs/contracts/canonical-sbom-id-v1.md - Canonical ID computation
  • docs/modules/attestor/architecture.md - DSSE envelope storage
  • docs/modules/evidence-locker/attestation-contract.md - Evidence Bundle v1
  • Endpoint contract: S00-T05-EVID-01