Evidence Packet Schema

Overview

Evidence packets are cryptographically signed, immutable records of deployment decisions and outcomes. They provide audit-grade proof of who did what, when, and why.

The EvidencePacket model below is the design-target rich evidence record. The as-built runtime truth-store contract actually served by EvidenceEndpoints.cs over the release_orchestrator.release_evidence table is the flatter EvidencePacketDto documented in the next section — that is the authoritative shape the promote/deploy truth guards consult.

Canonical subject and evidence-spine contract (as-built, 2026-07-13)

This section is the cross-module identity and handoff contract for the release decision trace. A tenant is authorization context, not part of a subject and never a substitute for one of the identifiers below.

Subject identities

SubjectCanonical identityBinding and ownership
Artifact / imageLowercase OCI digest sha256:<64 lowercase hex>.Release Orchestrator validates and persists the digest. A mutable tag, semantic version, release id, evidence hash, policy-pack digest, or Merkle root is not an artifact identity.
ReleasereleaseId; version remains display/change context.Release Orchestrator owns the aggregate. Components add componentId plus their exact artifact digest.
ApprovalapprovalId.The approval is bound to releaseId, target environment, candidate digest, and component subjects. Quorum state alone is not a gate decision.
Gate decisiondecisionId.The durable decision is bound to approvalId, releaseId, target environment, decision-basis hash, envelope, key id, and verified policy-decision evidence.
DeploymentdeploymentId.The deployment projection repeats approvalId, releaseId, environmentId, candidate digest, and component subjects. A target id identifies only a deployment subtask.
Release evidence packetevidenceId; releaseId is its aggregate join.Release Orchestrator owns the canonical bytes, content hash, signature reference, and verification state. The content hash addresses the packet, not the deployed artifact.
Artifact Canonical RecordEvidenceLocker canonicalId.GET /api/v1/evidence/thread/{canonicalId} returns the record’s artifact_digest, PURL, and attestations. canonicalId locates the EvidenceLocker record; it does not replace the OCI digest.
Attestation receiptAttestor uuid; the in-toto subject is name plus its digest map.GET /api/v1/attestations?subject=... finds receipts for a digest/PURL subject. The receipt id identifies the logged record; verification must still prove subject binding and trust.
Observed estate rowenvironmentId + componentId, carrying digest, releaseId, and deploymentId.Release Orchestrator Estate owns current observed state. Release events, gate decisions, and audit remain the history authority.

Legacy release rows that cannot yield one exact artifact subject return legacy_release_digest_missing, legacy_release_digest_invalid, or release_digest_ambiguous. Registry/read-model currency failures keep separate reasons such as current_digest_missing or current_digest_invalid; consumers must not collapse an unavailable producer into a claim that evidence never existed.

Web route and query handoff

New links use one subject passport:

/evidence/custody?subject=<URL-encoded canonical subject>

Prefer the exact OCI digest. A PURL is valid when the package is the intended subject. A release name is a compatibility fallback for a legacy/no-digest row and may require the operator to choose one component. Chain of Custody still accepts the typed aliases digest, purl, and release for old links, but new producers must write subject.

The Web resolver preserves continuity across the three operator entry forms: an exact release name is resolved by the server-side release search, a canonical releaseId is read directly, and an OCI digest is joined through matching deployment/approval truth before the same release component list is loaded. The selected component digest remains the artifact subjectId; releaseId remains the aggregate identity. Component order is deterministic, and a digest match selects that exact component rather than silently falling back to the first component.

Owner records use path identifiers rather than overloading subject:

Owner surfaceParameter contract
Release detail/releases/detail/:releaseId
Approval decision cockpit/releases/approvals/:approvalId
Deployment monitor/releases/deployments/:deploymentId
Release evidence API/api/v1/release-orchestrator/evidence/:evidenceId; list join is ?releaseId=:releaseId
EvidenceLocker thread/api/v1/evidence/thread/:canonicalId; PURL lookup is ?purl=...
Attestor receipt list/api/v1/attestations?subject=<digest-or-purl>

Verify handoffs use /evidence/verify?packId=<id>&version=<integer>&subject=<value>; digest is a compatibility alias only. Release/promotion launchers pass releaseId, optional environmentId, and a validated local returnTo. Shared scope/view keys follow the UI context-route contract. Do not pass a package-ref id, version, release name, evidence hash, pack digest, or Merkle root as if it were an OCI digest.

Source to Watch evidence spine

The requested Source → Build → Scan → Decision → Deploy → Watch lifecycle has an explicit Verdict phase between Scan and Decision in the implemented Chain of Custody view:

PhaseProducer and durable subjectConsumer / continuity rule
SourceSCM/build integration identifies the component and source provenance. Web reads the selected component’s sourceRepoUrl, sourceCommitSha, pipelineRef, and provenanceAttestationRef from Release Orchestrator truth.Build carries the same component identity forward; source labels never replace the image digest. Missing lineage directs recovery to the SCM/build integration or provenance re-ingest; ambiguous lineage remains unavailable until component-version truth is reconciled.
BuildBuild/SBOM attestation names the artifact and exact digest.Scan queries and attestations use that digest.
ScanScanner emits scan/SBOM evidence for the digest.Verdict evaluation consumes the same digest and records its inputs.
VerdictPolicy/verdict attestation binds outcome, policy basis, and subject digest.Release gate decision records the verdict reference and basis; a receipt without verified binding is not proof.
DecisionRelease Orchestrator persists approval, gate decision, basis, component subjects, and verified decision evidence.Deploy re-runs the truth guard against the same release, approval, target, digest, basis, and evidence. WAITING, empty, advisory, or skipped gate truth cannot become an approval.
DeployDeployment projection and events repeat release, approval, environment, candidate digest, and component subjects.Target execution pulls by digest; completion alone does not prove landed state.
WatchEstate records observed component digest by environment and deployment.Drift/landing comparison closes the trace; unobserved remains explicit rather than being treated as success.

State namespaces and recovery

These namespaces are intentionally different. UI copy must not translate one namespace into a stronger claim from another.

NamespaceValuesMeaning
Global Signing posturechecking, signed, unsigned, unknownTenant signing-key readiness from active key inventory. signed means a signing capability is available; it says nothing about any evidence record.
Web release aggregate posturemissing, partial, verified, verification_failed, replay_mismatch, unknownRoll-up across the release’s evidence/verification results. partial is an aggregate mixture, not a packet status.
Release evidence packetrecorded, verified, verification_failedRuntime release_evidence truth defined below. Only the verification service writes verified or verification_failed.
Record-level UI badgemissing, unknown, recorded, signed, proven, tamperedsigned means signature material exists but is not proven; proven requires successful verification; tampered requires a failed verification result.
Operational availabilityunavailable, unknown, unobserved, or a typed owner errorThe owner could not be read or no observation exists. This is not evidence absence and must not be promoted to success.

Current Web compatibility debt: getEvidencePostureLabel renders the aggregate verified value as Recorded. That label understates the aggregate and is not a semantic conversion: it must not be used to turn a packet’s recorded state into verified, or to construct a record-level Proven badge without a real verification result.

Observed stateOwner / recovery action
missingAsk the phase producer to create or re-ingest the absent record; for legacy releases inspect the typed digest reason. Never synthesize an id or digest.
unavailable / unknownCheck owner health, authorization, and currency, then retry that read. Preserve successful sibling results.
unsigned / recorded / signedConfigure approved key/trust material or obtain the required signature, then run verification. Protected paths remain blocked where verified evidence is required.
verified / provenPreserve the subject, signer/key, basis, and verification checks; no recovery is implied.
verification_failed / tampered / replay_mismatchQuarantine the claim, compare stored bytes and subject/basis binding, regenerate only from the owning producer, then re-verify. Do not blindly retry approval/deploy.
partialInspect each child record and recover only missing, unavailable, or failed children; do not discard already verified evidence.
unobservedRestore the Watch signal or agent observation and compare the landed digest before declaring deployment success.

Runtime release-evidence status model (as-built, Sprint 20260704_002)

The runtime evidence packet (EvidenceEndpoints.EvidencePacketDto, table release_orchestrator.release_evidence) carries a statusthat is now honest about what has actually been proven (EVH-1/EVH-2). Before this sprint every packet was written verified at POST time from a content self-hash only — a self-asserted claim, never a signature verification. The vocabulary (EvidenceStatuses):

statusMeaningSet by
recordedStored and its canonical bytes re-hash to the recorded content hash (integrity precondition). Not authenticated. This is the landing state for every POST /evidence.POST ingest (EVH-1)
verifiedA real verification step validated a DSSE signature over the packet against a configured trust anchor and bound the signed payload to the stored content.EvidenceVerificationService only (EVH-2)
verification_failedA verification step ran against present signature material and did not pass (bad signature, broken payload binding, or tampered content).EvidenceVerificationService only (EVH-2)

Verification (EVH-2)IEvidenceVerificationService, backed by the Attestor DSSE verification engine (StellaOps.Attestation.IDsseVerifier). Ordered checks, each persisted on the packet (verificationChecks[], verificationReason, verifiedKeyId):

  1. content_integrity — stored bytes re-hash to content_hash (else verification_failed).
  2. signature_present — a DSSE envelope is found inline in signatureRef (raw or base64 JSON) or the canonical content is itself a DSSE envelope. If absent, the packet stays recorded (no_signature_material).
  3. trust_anchor_configured — at least one PEM public key is configured under ReleaseOrchestrator:EvidenceVerification:TrustedPublicKeys. If none, the packet stays recorded (no_trust_anchor) — honest “unable to verify”, never a false verified.
  4. signature_valid — at least one signature validates under a trusted key.
  5. payload_binding — the signed payload binds to this content: either sha256(payload) == content_hash (the envelope signs the content directly) or the payload is an in-toto Statement whose subject[].digest.sha256 equals the content hash’s hex.

All pass ⇒ verified; any of 2–5 fail while an envelope is present ⇒ verification_failed; missing envelope / trust anchor ⇒ recorded.

Promotion policy-decision packets follow the same rule. Their canonical content is the exact gate-decision+basis payload carried by the DSSE envelope, and the envelope is stored in signatureRef. A missing basis, unsigned decision, absent trust anchor, or failed verification therefore remains recorded/verification_failed; PromotionRequestProcessor never promotes a self-hash to verified.

Truth-guard enforcement (EVH-3)ReleaseTruthGuard’s ≥1-verified requirement is governed by ReleaseOrchestrator:EvidenceVerification and mirrors the reachability-gate rollout:

The console evidence chip (DTC-4 read-model) derives posture from the joined evidenceCount / verifiedEvidenceCount / failedEvidenceCount: 0 → missing, any failed → verification_failed, all verified → verified, otherwise partial (recorded but not verified).

Evidence Packet Types

TypeDescriptionGenerated When
release_decisionPromotion decision evidencePromotion approved/rejected
deploymentDeployment execution evidenceDeployment completes
rollbackRollback evidenceRollback completes
ab_promotionA/B release promotion evidenceA/B promotion completes

Schema Definition

Evidence Packet Structure

interface EvidencePacket {
  // Identification
  id: UUID;
  version: "1.0";
  type: EvidencePacketType;

  // Metadata
  generatedAt: DateTime;
  generatorVersion: string;
  tenantId: UUID;

  // Content
  content: EvidenceContent;

  // Integrity
  contentHash: string;           // SHA-256 of canonical JSON content
  signature: string;             // Base64-encoded signature
  signatureAlgorithm: string;    // "RS256", "ES256"
  signerKeyRef: string;          // Reference to signing key
}

type EvidencePacketType =
  | "release_decision"
  | "deployment"
  | "rollback"
  | "ab_promotion";

Evidence Content

interface EvidenceContent {
  // What was released
  release: ReleaseEvidence;

  // Where it was released
  environment: EnvironmentEvidence;

  // Who requested and approved
  actors: ActorEvidence;

  // Why it was allowed
  decision: DecisionEvidence;

  // How it was executed (deployment only)
  execution?: ExecutionEvidence;

  // Previous state (for rollback)
  previous?: PreviousStateEvidence;
}

Release Evidence

interface ReleaseEvidence {
  id: UUID;
  name: string;
  displayName: string;
  createdAt: DateTime;
  createdBy: ActorRef;

  components: Array<{
    id: UUID;
    name: string;
    digest: string;
    semver: string;
    tag: string;
    role: "primary" | "sidecar" | "init" | "migration";
  }>;

  sourceRef?: {
    scmIntegrationId?: UUID;
    repository?: string;
    commitSha?: string;
    branch?: string;
    ciIntegrationId?: UUID;
    buildId?: string;
    pipelineUrl?: string;
  };
}

Environment Evidence

interface EnvironmentEvidence {
  id: UUID;
  name: string;
  displayName: string;
  orderIndex: number;

  targets: Array<{
    id: UUID;
    name: string;
    type: string;
    healthStatus: string;
  }>;

  configuration: {
    requiredApprovals: number;
    requireSeparationOfDuties: boolean;
    promotionPolicy?: string;
    deploymentTimeout: number;
  };
}

Actor Evidence

interface ActorEvidence {
  requester: ActorRef;
  requestReason: string;
  requestedAt: DateTime;

  approvers: Array<{
    actor: ActorRef;
    action: "approved" | "rejected";
    comment?: string;
    timestamp: DateTime;
    roles: string[];
  }>;

  deployer?: {
    agent: AgentRef;
    triggeredBy: ActorRef;
    startedAt: DateTime;
  };
}

interface ActorRef {
  id: UUID;
  type: "user" | "system" | "agent";
  name: string;
  email?: string;
}

interface AgentRef {
  id: UUID;
  name: string;
  version: string;
}

Decision Evidence

interface DecisionEvidence {
  promotionId: UUID;
  decision: "allow" | "block";
  decidedAt: DateTime;

  gateResults: Array<{
    gateName: string;
    gateType: string;
    passed: boolean;
    blocking: boolean;
    message: string;
    evaluatedAt: DateTime;
    details: object;
  }>;

  freezeWindowCheck: {
    checked: boolean;
    windowActive: boolean;
    windowId?: UUID;
    exemption?: {
      grantedBy: UUID;
      reason: string;
    };
  };

  separationOfDuties: {
    required: boolean;
    satisfied: boolean;
    requesterIds: UUID[];
    approverIds: UUID[];
  };
}

Execution Evidence

interface ExecutionEvidence {
  deploymentJobId: UUID;
  strategy: string;
  startedAt: DateTime;
  completedAt: DateTime;
  status: "succeeded" | "failed" | "rolled_back";

  tasks: Array<{
    targetId: UUID;
    targetName: string;
    agentId: UUID;
    status: string;
    startedAt: DateTime;
    completedAt: DateTime;
    digest: string;
    stickerWritten: boolean;
    error?: string;
  }>;

  artifacts: Array<{
    name: string;
    type: string;
    sha256: string;
    storageRef: string;
  }>;

  metrics: {
    totalTasks: number;
    succeededTasks: number;
    failedTasks: number;
    totalDurationSeconds: number;
  };
}

Previous State Evidence

interface PreviousStateEvidence {
  releaseId: UUID;
  releaseName: string;
  deployedAt: DateTime;
  deployedBy: ActorRef;
  components: Array<{
    name: string;
    digest: string;
  }>;
}

Example Evidence Packet

{
  "id": "evid-12345-uuid",
  "version": "1.0",
  "type": "deployment",
  "generatedAt": "2026-01-10T14:35:00Z",
  "generatorVersion": "stella-evidence-generator@1.5.0",
  "tenantId": "tenant-uuid",

  "content": {
    "release": {
      "id": "rel-uuid",
      "name": "myapp-v2.3.1",
      "displayName": "MyApp v2.3.1",
      "createdAt": "2026-01-10T10:00:00Z",
      "createdBy": {
        "id": "user-uuid",
        "type": "user",
        "name": "John Doe",
        "email": "john@example.com"
      },
      "components": [
        {
          "id": "comp-api-uuid",
          "name": "api",
          "digest": "sha256:abc123def456...",
          "semver": "2.3.1",
          "tag": "v2.3.1",
          "role": "primary"
        },
        {
          "id": "comp-worker-uuid",
          "name": "worker",
          "digest": "sha256:789xyz...",
          "semver": "2.3.1",
          "tag": "v2.3.1",
          "role": "primary"
        }
      ],
      "sourceRef": {
        "repository": "github.com/myorg/myapp",
        "commitSha": "abc123",
        "branch": "main",
        "buildId": "build-456"
      }
    },

    "environment": {
      "id": "env-prod-uuid",
      "name": "production",
      "displayName": "Production",
      "orderIndex": 2,
      "targets": [
        {
          "id": "target-1-uuid",
          "name": "prod-web-01",
          "type": "compose_host",
          "healthStatus": "healthy"
        },
        {
          "id": "target-2-uuid",
          "name": "prod-web-02",
          "type": "compose_host",
          "healthStatus": "healthy"
        }
      ],
      "configuration": {
        "requiredApprovals": 2,
        "requireSeparationOfDuties": true,
        "deploymentTimeout": 600
      }
    },

    "actors": {
      "requester": {
        "id": "user-john-uuid",
        "type": "user",
        "name": "John Doe",
        "email": "john@example.com"
      },
      "requestReason": "Release v2.3.1 with performance improvements",
      "requestedAt": "2026-01-10T12:00:00Z",
      "approvers": [
        {
          "actor": {
            "id": "user-jane-uuid",
            "type": "user",
            "name": "Jane Smith",
            "email": "jane@example.com"
          },
          "action": "approved",
          "comment": "LGTM, tests passed",
          "timestamp": "2026-01-10T13:00:00Z",
          "roles": ["release_manager"]
        },
        {
          "actor": {
            "id": "user-bob-uuid",
            "type": "user",
            "name": "Bob Johnson",
            "email": "bob@example.com"
          },
          "action": "approved",
          "comment": "Approved for production",
          "timestamp": "2026-01-10T13:30:00Z",
          "roles": ["approver"]
        }
      ],
      "deployer": {
        "agent": {
          "id": "agent-prod-uuid",
          "name": "prod-agent-01",
          "version": "1.5.0"
        },
        "triggeredBy": {
          "id": "system",
          "type": "system",
          "name": "Stella Orchestrator"
        },
        "startedAt": "2026-01-10T14:00:00Z"
      }
    },

    "decision": {
      "promotionId": "promo-uuid",
      "decision": "allow",
      "decidedAt": "2026-01-10T13:55:00Z",
      "gateResults": [
        {
          "gateName": "security-gate",
          "gateType": "security",
          "passed": true,
          "blocking": true,
          "message": "No critical or high vulnerabilities",
          "evaluatedAt": "2026-01-10T13:50:00Z",
          "details": {
            "critical": 0,
            "high": 0,
            "medium": 5,
            "low": 12
          }
        },
        {
          "gateName": "approval-gate",
          "gateType": "approval",
          "passed": true,
          "blocking": true,
          "message": "2/2 required approvals received",
          "evaluatedAt": "2026-01-10T13:55:00Z",
          "details": {
            "required": 2,
            "received": 2
          }
        }
      ],
      "freezeWindowCheck": {
        "checked": true,
        "windowActive": false
      },
      "separationOfDuties": {
        "required": true,
        "satisfied": true,
        "requesterIds": ["user-john-uuid"],
        "approverIds": ["user-jane-uuid", "user-bob-uuid"]
      }
    },

    "execution": {
      "deploymentJobId": "job-uuid",
      "strategy": "rolling",
      "startedAt": "2026-01-10T14:00:00Z",
      "completedAt": "2026-01-10T14:35:00Z",
      "status": "succeeded",
      "tasks": [
        {
          "targetId": "target-1-uuid",
          "targetName": "prod-web-01",
          "agentId": "agent-prod-uuid",
          "status": "succeeded",
          "startedAt": "2026-01-10T14:00:00Z",
          "completedAt": "2026-01-10T14:15:00Z",
          "digest": "sha256:abc123def456...",
          "stickerWritten": true
        },
        {
          "targetId": "target-2-uuid",
          "targetName": "prod-web-02",
          "agentId": "agent-prod-uuid",
          "status": "succeeded",
          "startedAt": "2026-01-10T14:20:00Z",
          "completedAt": "2026-01-10T14:35:00Z",
          "digest": "sha256:abc123def456...",
          "stickerWritten": true
        }
      ],
      "artifacts": [
        {
          "name": "compose.stella.lock.yml",
          "type": "compose-lock",
          "sha256": "checksum...",
          "storageRef": "s3://artifacts/job-uuid/compose.stella.lock.yml"
        }
      ],
      "metrics": {
        "totalTasks": 2,
        "succeededTasks": 2,
        "failedTasks": 0,
        "totalDurationSeconds": 2100
      }
    }
  },

  "contentHash": "sha256:content-hash...",
  "signature": "base64-signature...",
  "signatureAlgorithm": "RS256",
  "signerKeyRef": "stella/signing/prod-key-2026"
}

Signature Verification

async function verifyEvidencePacket(packet: EvidencePacket): Promise<VerificationResult> {
  // 1. Verify content hash
  const canonicalContent = canonicalize(packet.content);
  const computedHash = sha256(canonicalContent);

  if (computedHash !== packet.contentHash) {
    return { valid: false, error: "Content hash mismatch" };
  }

  // 2. Get signing key
  const publicKey = await getPublicKey(packet.signerKeyRef);

  // 3. Verify signature
  const signatureValid = await verify(
    packet.signature,
    packet.contentHash,
    publicKey,
    packet.signatureAlgorithm
  );

  if (!signatureValid) {
    return { valid: false, error: "Invalid signature" };
  }

  return { valid: true };
}

Storage

Evidence packets are stored in an append-only table:

CREATE TABLE release.evidence_packets (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    promotion_id UUID NOT NULL REFERENCES release.promotions(id),
    type TEXT NOT NULL,
    version TEXT NOT NULL DEFAULT '1.0',
    content JSONB NOT NULL,
    content_hash TEXT NOT NULL,
    signature TEXT NOT NULL,
    signature_algorithm TEXT NOT NULL,
    signer_key_ref TEXT NOT NULL,
    generated_at TIMESTAMPTZ NOT NULL,
    generator_version TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
    -- Note: No updated_at - packets are immutable
);

-- Prevent modifications
REVOKE UPDATE, DELETE ON release.evidence_packets FROM app_role;

Export Formats

Evidence packets can be exported in multiple formats:

FormatUse Case
JSONAPI consumption, archival
SignedJSONDSSE-signed JSON for verification workflows
MarkdownHuman-readable documentation
HTMLStyled web reports
PDFHuman-readable compliance reports
CSVSpreadsheet analysis
SLSASLSA provenance format
EvidenceCardSingle-file evidence card with SBOM excerpt, DSSE envelope, and Rekor receipt (v1.1)
EvidenceCardCompactCompact evidence card without full SBOM (v1.1)

Evidence Card Format (v1.1)

The evidence-card format packages related artifacts into a single JSON file for offline verification:

Content type: application/vnd.stellaops.evidence-card+json

See Evidence Decision API for schema details.

References