# RELEVI: Release Evidence **Purpose**: Cryptographically sealed evidence packets for audit-grade release governance. ## Modules ### Module: `evidence-collector` | Aspect | Specification | |--------|---------------| | **Responsibility** | Evidence aggregation; packet composition | | **Dependencies** | `promotion-manager`, `deploy-orchestrator`, `decision-engine` | | **Data Entities** | `EvidencePacket`, `EvidenceContent` | | **Events Produced** | `evidence.collected`, `evidence.packet_created` | **Evidence Packet Structure**: ```typescript interface EvidencePacket { id: UUID; tenantId: UUID; promotionId: UUID; packetType: EvidencePacketType; content: EvidenceContent; contentHash: string; // SHA-256 of content signature: string; // Cryptographic signature signerKeyRef: string; // Reference to signing key createdAt: DateTime; // Note: No updatedAt - packets are immutable } type EvidencePacketType = | "release_decision" // Promotion decision evidence | "deployment" // Deployment execution evidence | "rollback" // Rollback evidence | "ab_promotion"; // A/B promotion evidence interface EvidenceContent { // Metadata version: "1.0"; generatedAt: DateTime; generatorVersion: string; // What release: { id: UUID; name: string; components: Array<{ name: string; digest: string; semver: string; imageRepository: string; }>; sourceRef: SourceReference | null; }; // Where environment: { id: UUID; name: string; targets: Array<{ id: UUID; name: string; type: string; }>; }; // Who actors: { requester: { id: UUID; name: string; email: string; }; approvers: Array<{ id: UUID; name: string; action: string; at: DateTime; comment: string | null; }>; }; // Why decision: { result: "allow" | "deny"; gates: Array<{ type: string; name: string; status: string; message: string; details: Record; }>; reasons: string[]; }; // How execution: { workflowRunId: UUID | null; deploymentJobId: UUID | null; artifacts: Array<{ type: string; name: string; contentHash: string; }>; logs: string | null; // Compressed/truncated }; // When timeline: { requestedAt: DateTime; decidedAt: DateTime | null; startedAt: DateTime | null; completedAt: DateTime | null; }; // Integrity inputsHash: string; // Hash of all inputs for replay previousEvidenceId: UUID | null; // Chain to previous evidence } ``` --- ### Module: `evidence-signer` | Aspect | Specification | |--------|---------------| | **Responsibility** | Cryptographic signing of evidence packets | | **Dependencies** | `authority`, `vault` (for key storage) | | **Algorithms** | RS256, ES256, Ed25519 | **Signing Process**: ```typescript class EvidenceSigner { async sign(content: EvidenceContent): Promise { // 1. Canonicalize content (RFC 8785) const canonicalJson = canonicalize(content); // 2. Compute content hash const contentHash = crypto .createHash("sha256") .update(canonicalJson) .digest("hex"); // 3. Get signing key from vault const keyRef = await this.getActiveSigningKey(); const privateKey = await this.vault.getPrivateKey(keyRef); // 4. Sign the content hash const signature = await this.signWithKey(contentHash, privateKey); return { content, contentHash: `sha256:${contentHash}`, signature: base64Encode(signature), signerKeyRef: keyRef, algorithm: this.config.signatureAlgorithm, }; } async verify(packet: EvidencePacket): Promise { // 1. Canonicalize stored content const canonicalJson = canonicalize(packet.content); // 2. Verify content hash const computedHash = crypto .createHash("sha256") .update(canonicalJson) .digest("hex"); if (`sha256:${computedHash}` !== packet.contentHash) { return { valid: false, error: "Content hash mismatch" }; } // 3. Get public key const publicKey = await this.vault.getPublicKey(packet.signerKeyRef); // 4. Verify signature const signatureValid = await this.verifySignature( computedHash, base64Decode(packet.signature), publicKey ); return { valid: signatureValid, signerKeyRef: packet.signerKeyRef, signedAt: packet.createdAt, }; } } ``` --- ### Module: `sticker-writer` | Aspect | Specification | |--------|---------------| | **Responsibility** | Version sticker generation and placement | | **Dependencies** | `deploy-orchestrator`, `agent-manager` | | **Data Entities** | `VersionSticker` | **Version Sticker Schema**: ```typescript interface VersionSticker { stella_version: "1.0"; // Release identity release_id: UUID; release_name: string; // Component details components: Array<{ name: string; digest: string; semver: string; tag: string; image_repository: string; }>; // Deployment context environment: string; environment_id: UUID; deployed_at: string; // ISO 8601 deployed_by: UUID; // Traceability promotion_id: UUID; workflow_run_id: UUID; // Evidence chain evidence_packet_id: UUID; evidence_packet_hash: string; policy_decision_hash: string; // Orchestrator info orchestrator_version: string; // Source reference source_ref?: { commit_sha: string; branch: string; repository: string; }; } ``` **Sticker Placement**: - Written to `/var/stella/version.json` on each target - Atomic write (write to temp, rename) - Read during drift detection - Verified against expected state --- ### Module: `audit-exporter` | Aspect | Specification | |--------|---------------| | **Responsibility** | Compliance report generation; evidence export | | **Dependencies** | `evidence-collector` | | **Export Formats** | JSON, PDF, CSV | **Audit Report Types**: | Report Type | Description | |-------------|-------------| | `release_audit` | Full audit trail for a release | | `environment_audit` | All deployments to an environment | | `compliance_summary` | Summary for compliance review | | `change_log` | Chronological change log | **Report Generation**: ```typescript interface AuditReportRequest { type: AuditReportType; scope: { releaseId?: UUID; environmentId?: UUID; from?: DateTime; to?: DateTime; }; format: "json" | "pdf" | "csv"; options?: { includeDecisionDetails: boolean; includeApproverDetails: boolean; includeLogs: boolean; includeArtifacts: boolean; }; } interface AuditReport { id: UUID; type: AuditReportType; scope: ReportScope; generatedAt: DateTime; generatedBy: UUID; summary: { totalPromotions: number; successfulDeployments: number; failedDeployments: number; rollbacks: number; averageDeploymentTime: number; }; entries: AuditEntry[]; // For compliance signatureChain: { valid: boolean; verifiedPackets: number; invalidPackets: number; }; } ``` --- ## Immutability Enforcement Evidence packets are append-only. This is enforced at multiple levels: ### Database Level ```sql -- Evidence packets table with no UPDATE/DELETE CREATE TABLE release.evidence_packets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, promotion_id UUID NOT NULL REFERENCES release.promotions(id), packet_type VARCHAR(50) NOT NULL CHECK (packet_type IN ( 'release_decision', 'deployment', 'rollback', 'ab_promotion' )), content JSONB NOT NULL, content_hash VARCHAR(100) NOT NULL, signature TEXT, signer_key_ref VARCHAR(255), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- Note: No updated_at column; immutable by design ); -- Append-only enforcement via trigger CREATE OR REPLACE FUNCTION prevent_evidence_modification() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'Evidence packets are immutable and cannot be modified or deleted'; END; $$ LANGUAGE plpgsql; CREATE TRIGGER evidence_packets_immutable BEFORE UPDATE OR DELETE ON evidence_packets FOR EACH ROW EXECUTE FUNCTION prevent_evidence_modification(); -- Revoke UPDATE/DELETE from application role REVOKE UPDATE, DELETE ON release.evidence_packets FROM app_role; -- Version stickers table CREATE TABLE release.version_stickers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, target_id UUID NOT NULL REFERENCES release.targets(id), release_id UUID NOT NULL REFERENCES release.releases(id), promotion_id UUID NOT NULL REFERENCES release.promotions(id), sticker_content JSONB NOT NULL, content_hash VARCHAR(100) NOT NULL, written_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), verified_at TIMESTAMPTZ, drift_detected BOOLEAN NOT NULL DEFAULT FALSE ); CREATE INDEX idx_version_stickers_target ON release.version_stickers(target_id); CREATE INDEX idx_version_stickers_release ON release.version_stickers(release_id); CREATE INDEX idx_evidence_packets_promotion ON release.evidence_packets(promotion_id); CREATE INDEX idx_evidence_packets_created ON release.evidence_packets(created_at DESC); ``` ### Application Level ```csharp // Evidence service enforces immutability public sealed class EvidenceService { // Only Create method - no Update or Delete public async Task CreateAsync( EvidenceContent content, CancellationToken ct) { // Sign content var signed = await _signer.SignAsync(content, ct); // Store (append-only) var packet = new EvidencePacket { Id = Guid.NewGuid(), TenantId = content.TenantId, PromotionId = content.PromotionId, PacketType = content.PacketType, Content = content, ContentHash = signed.ContentHash, Signature = signed.Signature, SignerKeyRef = signed.SignerKeyRef, CreatedAt = DateTime.UtcNow, }; await _repository.InsertAsync(packet, ct); return packet; } // Read methods only public async Task GetAsync(Guid id, CancellationToken ct); public async Task> ListAsync( EvidenceFilter filter, CancellationToken ct); public async Task VerifyAsync( Guid id, CancellationToken ct); // No Update or Delete methods exist } ``` --- ## Evidence Chain Evidence packets form a verifiable chain: ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Evidence #1 │ │ Evidence #2 │ │ Evidence #3 │ │ (Dev Deploy) │────►│ (Stage Deploy) │────►│ (Prod Deploy) │ │ │ │ │ │ │ │ prevEvidenceId: │ │ prevEvidenceId: │ │ prevEvidenceId: │ │ null │ │ #1 │ │ #2 │ │ │ │ │ │ │ │ contentHash: │ │ contentHash: │ │ contentHash: │ │ sha256:abc... │ │ sha256:def... │ │ sha256:ghi... │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` **Chain Verification**: ```typescript async function verifyEvidenceChain(releaseId: UUID): Promise { const packets = await getPacketsForRelease(releaseId); const results: PacketVerificationResult[] = []; let previousHash: string | null = null; for (const packet of packets) { // 1. Verify packet signature const signatureValid = await verifySignature(packet); // 2. Verify content hash const contentValid = await verifyContentHash(packet); // 3. Verify chain link const chainValid = packet.content.previousEvidenceId === null ? previousHash === null : await verifyPreviousLink(packet, previousHash); results.push({ packetId: packet.id, signatureValid, contentValid, chainValid, valid: signatureValid && contentValid && chainValid, }); previousHash = packet.contentHash; } return { valid: results.every(r => r.valid), packets: results, }; } ``` --- ## API Endpoints ```yaml # Evidence Packets GET /api/v1/evidence-packets Query: ?promotionId={uuid}&type={type}&from={date}&to={date} Response: EvidencePacket[] GET /api/v1/evidence-packets/{id} Response: EvidencePacket (full content) GET /api/v1/evidence-packets/{id}/verify Response: VerificationResult GET /api/v1/evidence-packets/{id}/download Query: ?format={json|pdf} Response: binary # Evidence Chain GET /api/v1/releases/{id}/evidence-chain Response: EvidenceChain GET /api/v1/releases/{id}/evidence-chain/verify Response: ChainVerificationResult # Audit Reports POST /api/v1/audit-reports Body: { type: "release" | "environment" | "compliance", scope: { releaseId?, environmentId?, from?, to? }, format: "json" | "pdf" | "csv" } Response: { reportId: UUID, status: "generating" } GET /api/v1/audit-reports/{id} Response: { status, downloadUrl? } GET /api/v1/audit-reports/{id}/download Response: binary # Version Stickers GET /api/v1/version-stickers Query: ?targetId={uuid}&releaseId={uuid} Response: VersionSticker[] GET /api/v1/version-stickers/{id} Response: VersionSticker ``` --- ## Deterministic Replay Evidence packets enable deterministic replay - given the same inputs and policy version, the same decision is produced: ```typescript async function replayDecision(evidencePacket: EvidencePacket): Promise { const content = evidencePacket.content; // 1. Verify inputs hash const currentInputsHash = computeInputsHash( content.release, content.environment, content.decision.gates ); if (currentInputsHash !== content.inputsHash) { return { valid: false, error: "Inputs have changed since original decision" }; } // 2. Re-evaluate decision with same inputs const replayedDecision = await evaluateDecision( content.release, content.environment, { asOf: content.timeline.decidedAt } // Use policy version from that time ); // 3. Compare decisions const decisionsMatch = replayedDecision.result === content.decision.result; return { valid: decisionsMatch, originalDecision: content.decision.result, replayedDecision: replayedDecision.result, differences: decisionsMatch ? [] : computeDifferences(content.decision, replayedDecision), }; } ``` --- ## References - [Module Overview](overview.md) - [Design Principles](../design/principles.md) - [Security Architecture](../security/overview.md) - [Evidence Schema](../appendices/evidence-schema.md)