From cef10a881823b2e6a94384c4dd13753e2d4c788d Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Wed, 18 Feb 2026 07:08:57 -0500 Subject: [PATCH 1/4] fix tests: resolve built cli path on windows --- internal/testutil/testutil.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index daec4f3..378fee9 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -27,7 +27,14 @@ func BuildBinary(t *testing.T, root string) string { if err != nil { t.Fatalf("build binary: %v: %s", err, string(out)) } - return bin + if _, err := os.Stat(bin); err == nil { + return bin + } + if _, err := os.Stat(bin + ".exe"); err == nil { + return bin + ".exe" + } + t.Fatalf("build binary output not found at %s (or %s.exe)", bin, bin) + return "" } func CommandExitCode(t *testing.T, err error) int { From 4892993ca7cd4f281c242214afa4d43b32938228 Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Wed, 18 Feb 2026 07:11:08 -0500 Subject: [PATCH 2/4] fix tests: emit .exe binary path on windows --- internal/testutil/testutil.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 378fee9..f95af69 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -20,6 +20,9 @@ func RepoRoot(t *testing.T) string { func BuildBinary(t *testing.T, root string) string { t.Helper() bin := filepath.Join(t.TempDir(), "proof") + if runtime.GOOS == "windows" { + bin += ".exe" + } // #nosec G204 -- test helper executes a fixed go build command. cmd := exec.Command("go", "build", "-o", bin, "./cmd/proof") cmd.Dir = root @@ -27,14 +30,10 @@ func BuildBinary(t *testing.T, root string) string { if err != nil { t.Fatalf("build binary: %v: %s", err, string(out)) } - if _, err := os.Stat(bin); err == nil { - return bin - } - if _, err := os.Stat(bin + ".exe"); err == nil { - return bin + ".exe" + if _, err := os.Stat(bin); err != nil { + t.Fatalf("build binary output not found at %s: %v", bin, err) } - t.Fatalf("build binary output not found at %s (or %s.exe)", bin, bin) - return "" + return bin } func CommandExitCode(t *testing.T, err error) int { From 21702c1dc93f06cfabb54b7e7025aa7f90924b38 Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Wed, 18 Feb 2026 07:24:15 -0500 Subject: [PATCH 3/4] Run Gait OSS audit --- README.md | 439 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 266 insertions(+), 173 deletions(-) diff --git a/README.md b/README.md index e003146..ecc623c 100644 --- a/README.md +++ b/README.md @@ -1,240 +1,333 @@ -# Proof — Tamper-Evident Governance Primitive for AI Systems +# Proof -## Overview +The universal record format for AI governance. -Use Proof when you need deterministic, offline-verifiable records for AI system actions. +[![Main](https://github.com/Clyra-AI/proof/actions/workflows/main.yml/badge.svg)](https://github.com/Clyra-AI/proof/actions/workflows/main.yml) +[![CodeQL](https://github.com/Clyra-AI/proof/actions/workflows/codeql.yml/badge.svg)](https://github.com/Clyra-AI/proof/actions/workflows/codeql.yml) +[![Determinism](https://github.com/Clyra-AI/proof/actions/workflows/determinism.yml/badge.svg)](https://github.com/Clyra-AI/proof/actions/workflows/determinism.yml) -Proof is not an agent runtime, not a policy engine, and not a dashboard. It is a Go library + CLI for canonicalization, signing, chain verification, schema validation, and artifact integrity checks. +--- -Core capabilities: +## Why Proof Exists -- Deterministic record creation and hashing -- Hash-chain append and integrity verification -- Ed25519 signing and verification -- Cosign/Sigstore-backed signature verification support -- Bundle manifest signing and verification (Ed25519 + cosign) -- Runtime custom record type registration (schema-per-type) -- Offline CLI verification and inspection -- Versioned schemas for proof record types -- Gait pack/runpack compatibility verification +AI agents are moving from pilot to production. Regulators are catching up — the EU AI Act, SOC 2 AI controls, state-level AI laws — and every one of them requires evidence: what did the agent do, what controls were in place, can you prove it? -## When To Use Proof +Today that evidence is scattered across CloudWatch JSON, LangSmith exports, CSV dumps, screenshots, and PDF summaries. Every tool invents its own format. Auditors get a pile of incompatible files. Verification is manual. Tamper evidence is nonexistent. -- You need portable proof artifacts that can be verified offline. -- You need stable exit-code behavior for CI gates. -- You want a shared primitive that multiple products (including Gait) can reuse. -- You need deterministic canonicalization and repeatable digests. +Proof solves this by defining one record format that any tool can produce and any auditor can verify. One binary, one schema, one chain. Offline, signed, deterministic. -## When Not To Use Proof +The primitive is separate from the products built on top of it. Discovery tools, compliance engines, and policy enforcers all import Proof as their shared record and signing layer. Third parties — agent frameworks, MCP servers, CI pipelines — can adopt the format independently by importing the Go module or implementing the JSON Schema spec. The format earns standard status through adoption, not announcement. -- You need policy authoring/enforcement logic (use your policy layer above Proof). -- You need orchestration of agent workflows or tools. -- You only need live telemetry/observability and no artifact verification contract. +## What Proof Is -## Try It (Offline, <60s) +An open-source Go module and verification CLI. Four operations: -```bash -# Install CLI -GO111MODULE=on go install github.com/Clyra-AI/proof/cmd/proof@latest +- **Create** — Structured, schema-validated proof records with deterministic hashing +- **Chain** — Append records to tamper-evident hash chains (same mechanism as certificate transparency logs) +- **Sign** — Ed25519 or cosign (Sigstore) signatures on records, chains, and bundles +- **Verify** — Offline verification of any proof artifact with a single binary -# Explore built-in surfaces -proof --version -proof types list -proof frameworks list -``` +## What Proof Is Not + +- Not a policy engine. Proof does not make allow/block decisions. +- Not a scanner or collector. Proof does not discover tools or capture agent activity. +- Not a compliance engine. Proof does not evaluate whether evidence satisfies controls — it defines what records look like and what controls require. +- Not AI-powered. Zero LLMs, zero ML, zero probabilistic components. Deterministic canonicalization, cryptographic hashing, schema validation. Same inputs, same outputs, always. -If you already have an artifact: +## Quick Start ```bash -proof verify ./artifact.json +go install github.com/Clyra-AI/proof/cmd/proof@latest + +proof types list # 15 built-in record types +proof frameworks list # 8 compliance framework definitions +proof verify ./artifact # Verify any proof artifact offline ``` -## Install +### Library Usage (4 lines) -Current stable install path: +```go +import "github.com/Clyra-AI/proof" -```bash -go install github.com/Clyra-AI/proof/cmd/proof@latest -``` +record, _ := proof.NewRecord(proof.RecordOpts{ + Source: "my-mcp-server", + SourceProduct: "my-product", + Type: "tool_invocation", + Event: map[string]any{"tool": "postgres_query", "action": "SELECT"}, +}) -Binary location: +chain := proof.NewChain("default") +_ = proof.AppendToChain(chain, record) -- `$(go env GOBIN)/proof` when `GOBIN` is set -- otherwise `$(go env GOPATH)/bin/proof` (typically `~/go/bin/proof`) +key, _ := proof.GenerateSigningKey() +_, _ = proof.Sign(&chain.Records[0], key) +``` -Release artifact path (enabled on `v*.*.*` tags via `.github/workflows/release.yml`): +Every tool invocation now produces a signed, chainable, verifiable proof record. No configuration files, no account, no network access. -```bash -# GitHub CLI -gh release download vX.Y.Z -R Clyra-AI/proof -D /tmp/proof-release +### Custom Record Types -# Direct download example -curl -fL -o /tmp/proof-release/proof.tar.gz \ - https://github.com/Clyra-AI/proof/releases/download/vX.Y.Z/proof_X.Y.Z__.tar.gz -curl -fL -o /tmp/proof-release/checksums.txt \ - https://github.com/Clyra-AI/proof/releases/download/vX.Y.Z/checksums.txt +```go +_ = proof.RegisterCustomTypeSchema("vendor.custom_event", "./custom.schema.json") ``` -Verify release artifacts: +Custom types validate against the base proof record schema plus your type-specific schema. They chain and sign identically to built-in types. -```bash -cd /tmp/proof-release -sha256sum -c checksums.txt - -# Optional (if release includes signature + cert and cosign is installed) -cosign verify-blob \ - --certificate checksums.txt.pem \ - --signature checksums.txt.sig \ - checksums.txt +### Bundle Signing and Verification + +```go +manifest, _ := proof.SignBundle("./bundle", key) + +result, _ := proof.VerifyBundle("./bundle", proof.BundleVerifyOpts{ + VerifySignatures: true, + PublicKey: proof.PublicKey{Public: key.Public}, +}) ``` -## What You Get - -- **Deterministic canonicalization**: JSON/SQL/URL/text domains with stable digests. -- **Digest metadata**: `algo_id` + optional `salt_id`, including HMAC-SHA-256 helpers. -- **Tamper evidence**: record hashes + chain head continuity checks. -- **Signature options**: Ed25519 and cosign verification support. -- **Schema contracts**: built-in proof record schemas plus runtime custom type validation. -- **Cross-product compatibility**: Gait pack/runpack verification and migration-friendly compatibility packages. - -## CLI Surface - -```text -proof verify Verify record, chain, bundle, gait pack/runpack/signed JSON -proof verify --signatures --public-key -proof verify --signatures --cosign-key -proof verify --custom-type-schema = -proof verify --revocation-list --revocation-key -proof chain verify [--from RFC3339] [--to RFC3339] -proof inspect [--record ] -proof types list -proof types validate -proof frameworks list -proof frameworks show -proof completion +## Record Format + +The atomic unit is the proof record — a structured, signed artifact that captures what happened, what controls were in place, and the cryptographic integrity needed to prove it wasn't tampered with. + +```yaml +record_id: "prf-2026-09-15T10:30:00Z-a7f3b2c1" +record_version: "1.0" +timestamp: "2026-09-15T10:30:00Z" +source: "my-mcp-server" +source_product: "my-product" +record_type: "tool_invocation" + +event: + tool: "postgres_query" + action: "SELECT" + parameters: + query_hash: "sha256:abc123..." # digest, not the query itself + target: "payments.transactions" + +controls: + permissions_enforced: true + approved_scope: "read-only on payments.*" + within_scope: true + +integrity: + record_hash: "sha256:def456..." + previous_record_hash: "sha256:ghi789..." # chain link + signing_key_id: "a1b2c3..." + signature: "base64:..." ``` -Global flags: +Records are immutable, deterministic, and JSON-native — readable by any language, any tool, any text editor. -- `--json` -- `--quiet` -- `--explain` (step diagnostics to stderr) +## Built-in Record Types -## Gait Compatibility +15 types covering the full AI governance surface, each with its own JSON Schema: -Proof can verify Gait artifacts directly: +| Type | Description | +|---|---| +| `tool_invocation` | An AI agent invoked a tool | +| `decision` | An AI agent made a decision | +| `guardrail_activation` | A guardrail triggered or passed | +| `permission_check` | A permission was enforced | +| `human_oversight` | A human reviewed or approved | +| `policy_enforcement` | A policy rule was evaluated | +| `scan_finding` | An AI tool or risk was discovered | +| `risk_assessment` | A risk was identified and scored | +| `deployment` | An AI system was deployed or changed | +| `model_change` | A model version or config changed | +| `test_result` | A test or evaluation was run | +| `incident` | An AI-related incident occurred | +| `data_pipeline_run` | A data pipeline executed | +| `replay_certification` | A replay was run and certified | +| `approval` | An approval or delegation was issued | + +Record types are extensible. Define a custom type by providing a JSON Schema that extends the base record schema. + +## Hash Chains + +Records are chained — each record's hash includes the previous record's hash, creating a tamper-evident sequence. Modify or delete a record and the chain breaks. `proof chain verify` identifies the exact break point. -```bash -proof verify ./gait-pack.zip -proof verify ./gait-runpack.zip -proof verify --signatures --public-key ./gait-pack.zip +``` +Record 1 → hash(record_1) →┐ +Record 2 → hash(record_2 + prev_hash) →┐ +Record 3 → hash(record_3 + prev_hash) →┐ +... +$ proof chain verify ./records/ +Chain intact. 1,427 records. No gaps. ``` -For Gait extraction/migration support, Proof exposes compatibility packages: +This is not blockchain. It is the same mechanism used in certificate transparency logs. Simple, proven, auditable. -- `github.com/Clyra-AI/proof/signing` -- `github.com/Clyra-AI/proof/canon` -- `github.com/Clyra-AI/proof/schema` -- `github.com/Clyra-AI/proof/exitcode` +## Signing -Compatibility fixtures used by tests: +Two backends, one verification surface: -- `testdata/gait_compat/trace_signed.json` -- `testdata/gait_compat/approval_token_signed.json` -- `testdata/gait_compat/delegation_token_signed.json` +- **Ed25519** (default) — Fast, offline, no external dependencies +- **Cosign / Sigstore** — Supply chain ecosystem alignment, OIDC keyless signing -## Library Usage +Both backends produce records that pass chain verification. The signing backend is transparent to the verifier. Key management supports customer-managed keys, ephemeral keys for development, and key revocation with signed revocation lists. -Primary API: +## Canonicalization -```go -import "github.com/Clyra-AI/proof" -``` +Deterministic serialization is essential for hash stability. Proof implements canonicalization across four domains: -Quick example: +| Domain | Method | +|---|---| +| JSON | RFC 8785 (JCS) — deterministic field ordering, number normalization | +| SQL | UTF-8, trim, collapse whitespace, lowercase keywords, strip trailing semicolons | +| URL | Lowercase scheme + host, normalize path, sort query params | +| Text/Prompt | UTF-8, trim, collapse whitespace, normalize line endings | -```go -record, _ := proof.NewRecord(proof.RecordOpts{ - Source: "example", - SourceProduct: "example", - Type: "tool_invocation", - Event: map[string]any{"tool": "postgres_query"}, -}) +All digests carry `algo_id` (sha256 or hmac-sha256) and optional `salt_id` metadata. -chain := proof.NewChain("default") -_ = proof.AppendToChain(chain, record) +## Compliance Framework Definitions -key, _ := proof.GenerateSigningKey() -_, _ = proof.Sign(&chain.Records[0], key) -_, _ = proof.VerifyChain(chain) +YAML files that declare what regulatory controls require — which record types, what fields, what frequency. Zero evaluation logic. Configuration data consumed by downstream compliance tools. + +8 frameworks ship with v1: + +| Framework | Scope | +|---|---| +| EU AI Act | Articles 9, 12, 13, 14, 15, 26 | +| SOC 2 | CC6, CC7, CC8 (AI-specific sub-controls) | +| SOX | Change management, SoD, access controls | +| PCI-DSS | Requirement 10 (logging and monitoring) | +| Texas TRAIGA | State AI regulation | +| Colorado AI Act | State AI regulation | +| ISO 42001 | AI Management System | +| NIST AI 600-1 | Agent security guidance | + +Adding a new framework is a YAML file and a PR — no code changes required. + +## CLI Reference + +``` +proof verify Verify record, chain, bundle, or Gait artifact + --chain Verify chain integrity + --signatures Verify signatures + --public-key Ed25519 public key + --cosign-key Cosign public key + --cosign-cert Cosign certificate + --cosign-cert-identity Expected certificate identity + --cosign-cert-issuer Expected OIDC issuer + --custom-type-schema = Custom type schema (repeatable) + --revocation-list Signed key revocation list + --revocation-key Revocation list signer public key + +proof inspect Human-readable artifact display + --record Inspect specific record by ID + +proof chain verify Verify chain integrity + --from Start of time range + --to End of time range + +proof types list List all registered record types +proof types validate Validate a custom record type schema + +proof frameworks list List available compliance frameworks +proof frameworks show Display framework controls + +proof completion Shell completion generation ``` -Custom type registration + bundle signing: +Global flags: `--json`, `--quiet`, `--explain` -```go -_ = proof.RegisterCustomTypeSchema("vendor.custom_event", "./custom.schema.json") +## Exit Codes -manifest, _ := proof.SignBundle("./bundle", key) -_, _ = proof.VerifyBundle("./bundle", proof.BundleVerifyOpts{ - VerifySignatures: true, - PublicKey: proof.PublicKey{Public: key.Public}, -}) -_ = manifest +Stable contract. CI pipelines can rely on consistent semantics across all tools that adopt this vocabulary. + +| Code | Meaning | +|---|---| +| 0 | Success | +| 1 | Internal / runtime failure | +| 2 | Verification failure | +| 3 | Policy / schema violation | +| 4 | Approval required | +| 5 | Regression drift detected | +| 6 | Invalid input | +| 7 | Dependency missing | +| 8 | Unsafe operation blocked | + +## Gait Compatibility + +Proof verifies Gait artifacts directly — packs, runpacks, and signed JSON — without any Gait dependency: + +```bash +proof verify ./gait-pack.zip +proof verify ./gait-runpack.zip +proof verify --signatures --public-key ./gait-pack.zip ``` -## Contract Commitments +The signing and canonicalization code in Proof was extracted from Gait's production codebase. `KeyID()`, `Signature{}`, and JCS digest functions produce byte-identical output, so artifacts signed by Gait before the extraction verify correctly with Proof. + +Compatibility packages for migration: + +- `github.com/Clyra-AI/proof/signing` — Ed25519 key management with dev/prod modes +- `github.com/Clyra-AI/proof/canon` — `CanonicalizeJSON()` and `DigestJCS()` aliases +- `github.com/Clyra-AI/proof/schema` — `ValidateJSON()` and `ValidateJSONL()` helpers +- `github.com/Clyra-AI/proof/exitcode` — Exit code constants with legacy aliases -- Deterministic canonicalization and hashing for supported domains -- Offline-first verification workflows -- Stable exit-code contract -- Versioned schema assets under `schemas/v1` +## Design Principles -Exit codes: +1. **The spec is the product.** JSON Schema files are the normative specification. The Go module is the reference implementation. If they disagree, the spec wins. +2. **Zero opinions.** Proof does not know what compliance means. It creates, chains, signs, and verifies. What records mean is someone else's problem. +3. **Boring cryptography.** Ed25519 (RFC 8032), SHA-256, RFC 8785. No novel constructions, no experimental algorithms. +4. **Library-first, CLI-second.** The Go module is the primary artifact. The CLI is a thin wrapper. Every command maps to a module function. +5. **Extracted, not greenfield.** Built by extracting production-proven code from Gait, not by writing from scratch. Same pattern as Sigstore (extracted from cosign), OCI (extracted from Docker), HCL (extracted from Terraform). +6. **Offline-first.** All core operations — create, chain, sign, verify — work without network access. Air-gapped environments are first-class. -- `0` success -- `1` internal/runtime failure -- `2` verification failure -- `3` policy/schema violation -- `4` approval required -- `5` regression drift detected -- `6` invalid input -- `7` dependency missing -- `8` unsafe operation blocked +## Repository Layout -## Developer Workflow +``` +cmd/proof/ CLI (Cobra-based) +core/ + record/ Record creation, validation, hashing + chain/ Hash chain append and verification + signing/ Ed25519 + cosign signing + canon/ Canonicalization (JSON, SQL, URL, text) + schema/ JSON Schema validation + type registry + framework/ YAML framework definition loading + gait/ Gait pack/runpack compatibility verification + exitcode/ Exit code constants +signing/ Compatibility package (Gait migration) +canon/ Compatibility package (Gait migration) +schema/ Compatibility package (Gait migration) +exitcode/ Compatibility package (Gait migration) +schemas/v1/ JSON Schema spec files (language-agnostic contract) + types/ 15 record type schemas +frameworks/ 8 compliance framework YAML definitions +testdata/ Golden vectors and test fixtures +scripts/ Test and validation scripts +perf/ Performance budgets +``` -![Main](https://github.com/Clyra-AI/proof/actions/workflows/main.yml/badge.svg) -![CodeQL](https://github.com/Clyra-AI/proof/actions/workflows/codeql.yml/badge.svg) -![Determinism](https://github.com/Clyra-AI/proof/actions/workflows/determinism.yml/badge.svg) +## Development ```bash -make fmt -make lint -make test -make prepush-full -make test-uat-local +make fmt # Format +make lint # Vet + golangci-lint +make test # Unit tests +make prepush-full # Full gate: lint + test + coverage + contract + integration + e2e + acceptance +make test-uat-local # Local UAT across install paths ``` -Key automation: +CI pipelines: main, PR, determinism (cross-platform), CodeQL, nightly (hardening + chaos + performance + soak), release (GoReleaser + checksums + SBOM + cosign + SLSA provenance). -- Main CI: `.github/workflows/main.yml` -- PR CI: `.github/workflows/pr.yml` -- Determinism CI: `.github/workflows/determinism.yml` -- Nightly hardening/perf: `.github/workflows/nightly.yml` -- Release: `.github/workflows/release.yml` +## Install -## Repository Map +```bash +# From source +go install github.com/Clyra-AI/proof/cmd/proof@latest -- CLI: `cmd/proof` -- Core packages: `core/*` -- Compatibility packages: `signing`, `canon`, `schema`, `exitcode` -- Schemas: `schemas/v1` -- Framework definitions: `frameworks/` -- Scripts: `scripts/` -- Performance budgets: `perf/` +# From release +gh release download vX.Y.Z -R Clyra-AI/proof -D /tmp/proof-release +cd /tmp/proof-release && sha256sum -c checksums.txt +``` + +Go module: + +```bash +go get github.com/Clyra-AI/proof +``` -## Notes +## License -- Current Go baseline: `1.25.7` -- Implementation checklist: `IMPLEMENTATION_CHECK.md` -- Product PRD/source of scope: `product/proof.md` +See [LICENSE](LICENSE). From 71ecf644c974529f7703ec60ffe96f89cb2d80c3 Mon Sep 17 00:00:00 2001 From: David Ahmann Date: Wed, 18 Feb 2026 07:37:12 -0500 Subject: [PATCH 4/4] close launch gaps: license, dependency exit codes, and install-path UAT --- LICENSE | 201 +++++++++++++++++++++++++++++ README.md | 4 +- cmd/proof/errors.go | 12 ++ cmd/proof/exitcode_test.go | 25 ++++ cmd/proof/verify.go | 12 +- core/signing/cosign.go | 11 +- core/signing/signing_test.go | 11 ++ proof.go | 4 + scripts/test_contract_exitcodes.sh | 54 ++++++++ scripts/test_uat_local.sh | 64 ++++++++- 10 files changed, 386 insertions(+), 12 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ebe261 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + including, without limitation, any warranties or conditions of + TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index ecc623c..3b0acdf 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ make fmt # Format make lint # Vet + golangci-lint make test # Unit tests make prepush-full # Full gate: lint + test + coverage + contract + integration + e2e + acceptance -make test-uat-local # Local UAT across install paths +make test-uat-local # UAT for source, go-install, and local release-archive install paths ``` CI pipelines: main, PR, determinism (cross-platform), CodeQL, nightly (hardening + chaos + performance + soak), release (GoReleaser + checksums + SBOM + cosign + SLSA provenance). @@ -317,7 +317,7 @@ CI pipelines: main, PR, determinism (cross-platform), CodeQL, nightly (hardening # From source go install github.com/Clyra-AI/proof/cmd/proof@latest -# From release +# From release (after a tagged release is published) gh release download vX.Y.Z -R Clyra-AI/proof -D /tmp/proof-release cd /tmp/proof-release && sha256sum -c checksums.txt ``` diff --git a/cmd/proof/errors.go b/cmd/proof/errors.go index 2365996..61ab91e 100644 --- a/cmd/proof/errors.go +++ b/cmd/proof/errors.go @@ -1,5 +1,10 @@ package main +import ( + "github.com/Clyra-AI/proof" + "github.com/Clyra-AI/proof/core/exitcode" +) + type cliError struct { code int msg string @@ -11,3 +16,10 @@ func (e cliError) ExitCode() int { return e.code } func newCLIError(code int, msg string) error { return cliError{code: code, msg: msg} } + +func verificationErrorCode(err error) int { + if proof.IsDependencyMissing(err) { + return exitcode.DependencyMiss + } + return exitcode.VerificationErr +} diff --git a/cmd/proof/exitcode_test.go b/cmd/proof/exitcode_test.go index 970650e..de6ee6d 100644 --- a/cmd/proof/exitcode_test.go +++ b/cmd/proof/exitcode_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "os" "os/exec" "path/filepath" "testing" @@ -78,3 +79,27 @@ func TestExitCodeRevokedKeyFailure(t *testing.T) { _, err = cmd.CombinedOutput() require.Equal(t, 2, testutil.CommandExitCode(t, err)) } + +func TestExitCodeDependencyMissing(t *testing.T) { + root := testutil.RepoRoot(t) + bin := testutil.BuildBinary(t, root) + + dir := t.TempDir() + r, err := proof.NewRecord(proof.RecordOpts{ + Timestamp: time.Date(2026, 2, 17, 14, 0, 0, 0, time.UTC), + Source: "axym", + SourceProduct: "axym", + Type: "decision", + Event: map[string]any{"action": "allow"}, + }) + require.NoError(t, err) + r.Integrity.Signature = "cosign:ZmFrZXNpZw==" + r.Integrity.SigningKeyID = "cosign:test-key" + recordPath := filepath.Join(dir, "record.json") + require.NoError(t, proof.WriteRecord(recordPath, r)) + + cmd := exec.Command(bin, "verify", "--signatures", "--cosign-key", filepath.Join(dir, "cosign.pub"), recordPath) + cmd.Env = append(os.Environ(), "PATH="+t.TempDir()) + _, err = cmd.CombinedOutput() + require.Equal(t, 7, testutil.CommandExitCode(t, err)) +} diff --git a/cmd/proof/verify.go b/cmd/proof/verify.go index 34320fb..884c132 100644 --- a/cmd/proof/verify.go +++ b/cmd/proof/verify.go @@ -90,7 +90,7 @@ func newVerifyCmd(opts *globalOpts) *cobra.Command { CertificateIssuer: cosignCertIssuer, } if err := proof.VerifyCosignWithOptions(r, opts); err != nil { - return newCLIError(exitcode.VerificationErr, err.Error()) + return newCLIError(verificationErrorCode(err), err.Error()) } } else { if publicKeyHex == "" { @@ -140,7 +140,7 @@ func newVerifyCmd(opts *globalOpts) *cobra.Command { CertificateIssuer: cosignCertIssuer, } if err := proof.VerifyCosignWithOptions(&c.Records[i], opts); err != nil { - return newCLIError(exitcode.VerificationErr, fmt.Sprintf("signature verification failed for record %s: %v", c.Records[i].RecordID, err)) + return newCLIError(verificationErrorCode(err), fmt.Sprintf("signature verification failed for record %s: %v", c.Records[i].RecordID, err)) } continue } @@ -170,7 +170,7 @@ func newVerifyCmd(opts *globalOpts) *cobra.Command { CertificateIdentity: cosignCertIdentity, CertificateIssuer: cosignCertIssuer, }); err != nil { - return newCLIError(exitcode.VerificationErr, err.Error()) + return newCLIError(verificationErrorCode(err), err.Error()) } } if verifyChain { @@ -194,7 +194,7 @@ func newVerifyCmd(opts *globalOpts) *cobra.Command { CertificateIssuer: cosignCertIssuer, }) if err != nil { - return newCLIError(exitcode.VerificationErr, err.Error()) + return newCLIError(verificationErrorCode(err), err.Error()) } printResult(opts, map[string]any{ "ok": true, @@ -215,7 +215,7 @@ func newVerifyCmd(opts *globalOpts) *cobra.Command { CertificateIssuer: cosignCertIssuer, }) if err != nil { - return newCLIError(exitcode.VerificationErr, err.Error()) + return newCLIError(verificationErrorCode(err), err.Error()) } printResult(opts, map[string]any{"ok": true, "kind": kind, "run_id": res.RunID, "manifest_digest": res.ManifestDigest, "files_verified": res.FilesVerified, "signatures_verified": res.SignaturesVerified}, fmt.Sprintf("Gait runpack verified. Files: %d.", res.FilesVerified)) return nil @@ -223,7 +223,7 @@ func newVerifyCmd(opts *globalOpts) *cobra.Command { explainf(opts, "gait signed JSON verify") if verifySignatures { if err := verifyGaitSignedJSON(path, publicKeyHex); err != nil { - return newCLIError(exitcode.VerificationErr, err.Error()) + return newCLIError(verificationErrorCode(err), err.Error()) } } printResult(opts, map[string]any{"ok": true, "kind": kind}, "Gait signed artifact verified.") diff --git a/core/signing/cosign.go b/core/signing/cosign.go index 0becf87..0f9ea4d 100644 --- a/core/signing/cosign.go +++ b/core/signing/cosign.go @@ -1,6 +1,7 @@ package signing import ( + "errors" "fmt" "os" "os/exec" @@ -17,6 +18,8 @@ type CosignVerifyOpts struct { CertificateIssuer string } +var ErrDependencyMissing = errors.New("dependency missing") + var cosignLookPath = exec.LookPath var cosignRun = func(args ...string) ([]byte, error) { // #nosec G204 -- executable is fixed to cosign; args are assembled from controlled flags/paths. @@ -24,6 +27,10 @@ var cosignRun = func(args ...string) ([]byte, error) { return cmd.CombinedOutput() } +func IsDependencyMissing(err error) bool { + return errors.Is(err, ErrDependencyMissing) +} + func SignRecordCosign(r *record.Record, keyPath string) (*record.Record, error) { if r == nil { return nil, fmt.Errorf("record is nil") @@ -52,7 +59,7 @@ func SignDigestCosign(digest string, keyPath string) (Signature, error) { return Signature{}, fmt.Errorf("cosign key path is required") } if _, err := cosignLookPath("cosign"); err != nil { - return Signature{}, fmt.Errorf("cosign binary not found: %w", err) + return Signature{}, fmt.Errorf("%w: cosign binary not found: %v", ErrDependencyMissing, err) } tmpDir, err := os.MkdirTemp("", "proof-cosign-") if err != nil { @@ -111,7 +118,7 @@ func VerifyDigestCosign(sig Signature, digest string, opts CosignVerifyOpts) err return fmt.Errorf("cosign verification requires --cosign-key or --cosign-cert") } if _, err := cosignLookPath("cosign"); err != nil { - return fmt.Errorf("cosign binary not found: %w", err) + return fmt.Errorf("%w: cosign binary not found: %v", ErrDependencyMissing, err) } if strings.TrimSpace(sig.SignedDigest) == "" { return fmt.Errorf("signed digest is required") diff --git a/core/signing/signing_test.go b/core/signing/signing_test.go index 3e902e6..85b31a0 100644 --- a/core/signing/signing_test.go +++ b/core/signing/signing_test.go @@ -506,6 +506,17 @@ func TestSignDigestCosignErrorBranches(t *testing.T) { require.ErrorContains(t, err, "cosign binary not found") } +func TestIsDependencyMissing(t *testing.T) { + origLookPath := cosignLookPath + t.Cleanup(func() { cosignLookPath = origLookPath }) + + cosignLookPath = func(file string) (string, error) { return "", errors.New("missing") } + _, err := SignDigestCosign("sha256:abcd", "/tmp/cosign.key") + require.Error(t, err) + require.True(t, IsDependencyMissing(err)) + require.False(t, IsDependencyMissing(errors.New("other error"))) +} + func TestVerifyRecordCosignNilAndPrefixBranches(t *testing.T) { err := VerifyRecordCosign(nil, CosignVerifyOpts{KeyPath: "/tmp/pub"}) require.ErrorContains(t, err, "record is nil") diff --git a/proof.go b/proof.go index 2bdac0f..b35bb63 100644 --- a/proof.go +++ b/proof.go @@ -236,6 +236,10 @@ func VerifyCosignWithOptions(r *Record, opts CosignVerifyOpts) error { return signing.VerifyRecordCosign(r, opts) } +func IsDependencyMissing(err error) bool { + return signing.IsDependencyMissing(err) +} + func VerifyBundle(path string, opts BundleVerifyOpts) (*BundleManifest, error) { manifestPath := filepath.Join(path, "manifest.json") // #nosec G304 -- caller provides explicit local artifact path. diff --git a/scripts/test_contract_exitcodes.sh b/scripts/test_contract_exitcodes.sh index ea2e151..4daf1f9 100755 --- a/scripts/test_contract_exitcodes.sh +++ b/scripts/test_contract_exitcodes.sh @@ -45,3 +45,57 @@ if [[ "${code}" -ne 2 ]]; then fi echo "verification failure contract check passed" + +cat > "${TMPDIR}/generate_cosign_record.go" <<'GO' +package main + +import ( + "encoding/json" + "os" + "time" + + "github.com/Clyra-AI/proof" +) + +func main() { + if len(os.Args) != 2 { + panic("usage: go run generate_cosign_record.go ") + } + + r, err := proof.NewRecord(proof.RecordOpts{ + Timestamp: time.Date(2026, 2, 17, 12, 30, 0, 0, time.UTC), + Source: "axym", + SourceProduct: "axym", + Type: "decision", + Event: map[string]any{"action": "allow"}, + }) + if err != nil { + panic(err) + } + r.Integrity.Signature = "cosign:ZmFrZXNpZw==" + r.Integrity.SigningKeyID = "cosign:test-key" + + raw, err := json.MarshalIndent(r, "", " ") + if err != nil { + panic(err) + } + if err := os.WriteFile(os.Args[1], raw, 0o600); err != nil { + panic(err) + } +} +GO + +go run "${TMPDIR}/generate_cosign_record.go" "${TMPDIR}/cosign_record.json" + +mkdir -p "${TMPDIR}/empty-path" +set +e +PATH="${TMPDIR}/empty-path" "${BIN}" verify --signatures --cosign-key "${TMPDIR}/cosign.pub" "${TMPDIR}/cosign_record.json" >/dev/null 2>&1 +code=$? +set -e + +if [[ "${code}" -ne 7 ]]; then + echo "expected exit code 7 for missing dependency, got ${code}" + exit 1 +fi + +echo "dependency missing contract check passed" diff --git a/scripts/test_uat_local.sh b/scripts/test_uat_local.sh index 3fd2362..11884bc 100755 --- a/scripts/test_uat_local.sh +++ b/scripts/test_uat_local.sh @@ -76,6 +76,19 @@ run_step() { fi } +resolve_binary_path() { + local candidate="$1" + if [[ -x "${candidate}" ]]; then + printf '%s' "${candidate}" + return 0 + fi + if [[ -x "${candidate}.exe" ]]; then + printf '%s' "${candidate}.exe" + return 0 + fi + return 1 +} + run_binary_contract_suite() { local label="$1" local bin_path="$2" @@ -197,6 +210,24 @@ GO rm -f "${generator}" } +create_local_release_archive() { + local source_bin="$1" + local release_dir="$2" + local stage_dir="${release_dir}/stage" + mkdir -p "${stage_dir}" + + local staged_name="proof" + if [[ "${source_bin}" == *.exe ]]; then + staged_name="proof.exe" + fi + + cp "${source_bin}" "${stage_dir}/${staged_name}" + + local archive="${release_dir}/proof-local.tar.gz" + tar -czf "${archive}" -C "${stage_dir}" "${staged_name}" + printf '%s' "${archive}" +} + extract_release_binary() { local release_dir="$1" local extraction_dir="$2" @@ -239,12 +270,41 @@ generate_sample_artifacts "${ARTIFACTS_DIR}" SOURCE_BIN="${OUTPUT_DIR}/source/proof" mkdir -p "$(dirname "${SOURCE_BIN}")" run_step "build_source_binary" bash -lc "cd \"${REPO_ROOT}\" && go build -o \"${SOURCE_BIN}\" ./cmd/proof" -run_binary_contract_suite "source" "${SOURCE_BIN}" "${ARTIFACTS_DIR}" +SOURCE_BIN_RESOLVED="$(resolve_binary_path "${SOURCE_BIN}" || true)" +if [[ -z "${SOURCE_BIN_RESOLVED}" ]]; then + log "FAIL source_binary_resolve (${SOURCE_BIN} or ${SOURCE_BIN}.exe not found)" + exit 1 +fi +run_binary_contract_suite "source" "${SOURCE_BIN_RESOLVED}" "${ARTIFACTS_DIR}" GO_INSTALL_DIR="${OUTPUT_DIR}/go_install/bin" mkdir -p "${GO_INSTALL_DIR}" run_step "install_go_binary" bash -lc "cd \"${REPO_ROOT}\" && GOBIN=\"${GO_INSTALL_DIR}\" go install ./cmd/proof" -run_binary_contract_suite "go_install" "${GO_INSTALL_DIR}/proof" "${ARTIFACTS_DIR}" +GO_INSTALL_BIN_RESOLVED="$(resolve_binary_path "${GO_INSTALL_DIR}/proof" || true)" +if [[ -z "${GO_INSTALL_BIN_RESOLVED}" ]]; then + log "FAIL go_install_binary_resolve (${GO_INSTALL_DIR}/proof or ${GO_INSTALL_DIR}/proof.exe not found)" + exit 1 +fi +run_binary_contract_suite "go_install" "${GO_INSTALL_BIN_RESOLVED}" "${ARTIFACTS_DIR}" + +log "==> local_release_archive" +LOCAL_RELEASE_DIR="${OUTPUT_DIR}/local_release" +mkdir -p "${LOCAL_RELEASE_DIR}" +LOCAL_RELEASE_ARCHIVE="$(create_local_release_archive "${SOURCE_BIN_RESOLVED}" "${LOCAL_RELEASE_DIR}" 2>"${OUTPUT_DIR}/logs/local_release_archive.log" || true)" +if [[ -z "${LOCAL_RELEASE_ARCHIVE}" ]]; then + log "FAIL local_release_archive (see ${OUTPUT_DIR}/logs/local_release_archive.log)" + tail -n 80 "${OUTPUT_DIR}/logs/local_release_archive.log" || true + exit 1 +fi +log "PASS local_release_archive" + +LOCAL_RELEASE_EXTRACT_DIR="${OUTPUT_DIR}/local_release_extract" +LOCAL_RELEASE_BIN="$(extract_release_binary "${LOCAL_RELEASE_DIR}" "${LOCAL_RELEASE_EXTRACT_DIR}" || true)" +if [[ -z "${LOCAL_RELEASE_BIN}" ]]; then + log "FAIL local_release_extract (no proof binary found in local archive)" + exit 1 +fi +run_binary_contract_suite "local_release" "${LOCAL_RELEASE_BIN}" "${ARTIFACTS_DIR}" if [[ -n "${RELEASE_VERSION}" ]]; then require_cmd gh