Skip to content

Commit 56908cf

Browse files
committed
Close PRD gaps: custom types, bundle signatures, determinism, and gait fixtures
1 parent e634c2a commit 56908cf

30 files changed

+1217
-104
lines changed

.github/workflows/determinism.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: determinism
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
vectors:
10+
strategy:
11+
matrix:
12+
os: [ubuntu-latest, macos-latest, windows-latest]
13+
runs-on: ${{ matrix.os }}
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-go@v5
17+
with:
18+
go-version-file: go.mod
19+
- name: Determinism vectors
20+
run: go test ./core/signing -run TestDeterministicVector -count=1 -v
21+

IMPLEMENTATION_CHECK.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ Status key:
1616
| ID | Status | Notes |
1717
|---|---|---|
1818
| FR1 Record creation | PASS | Deterministic record creation + validation in `core/record` + `proof.NewRecord()`. |
19-
| FR2 Type registry | PASS | Built-in registry + schema validation + `proof types list/validate`. |
19+
| FR2 Type registry | PASS | Built-in registry + runtime custom type registration + `proof types list/validate`. |
2020
| FR3 Hash chain | PASS | Append + verify + range verify + break-point reporting in `core/chain` and CLI. |
21-
| FR4 Signing | PASS | Ed25519 + cosign signing/verification paths implemented, including cert/identity/issuer verify options and revocation-list verification. |
22-
| FR5 Canonicalization | PASS | JSON/SQL/URL/text/prompt canonicalization in `core/canon`. |
23-
| FR6 Verification CLI | PASS | `verify`, `inspect`, `chain verify`, `types`, `frameworks`; exit code contract and JSON output implemented. |
21+
| FR4 Signing | PASS | Ed25519 + cosign signing/verification paths implemented for records/chains and bundle manifests, including cert/identity/issuer verify options and revocation-list verification. |
22+
| FR5 Canonicalization | PASS | JSON/SQL/URL/text/prompt canonicalization plus digest metadata (`algo_id`, `salt_id`) and HMAC-SHA-256 helpers in `core/canon`. |
23+
| FR6 Verification CLI | PASS | `verify`, `inspect`, `chain verify`, `types`, `frameworks`; bundle signature verification, custom type schema mapping, `--explain`, and exit code contract implemented. |
2424
| FR7 Framework definitions | PASS | 8 frameworks in `frameworks/` and `core/framework/`; list/show implemented. |
2525
| FR8 Go module API | PASS | Primary API surface exported from `proof.go`. |
2626
| FR9 JSON schemas | PASS | Base + type schemas + chain/bundle/framework schemas in `schemas/v1/`. |
@@ -35,11 +35,11 @@ Status key:
3535
| AC4 Cross-product chain | PASS | Mixed record types chain and verify correctly. |
3636
| AC5 Offline guarantee | PASS | Core verification is offline-first; cosign path is explicitly local-binary based, no mandatory network dependency in CLI flow. |
3737
| AC6 Schema validation | PASS | Invalid/missing fields rejected by schema and validation layers. |
38-
| AC7 Custom type | PASS | Custom schema validation is supported through CLI/API (`types validate`, `ValidateCustomTypeSchema`). |
38+
| AC7 Custom type | PASS | Runtime custom type registration is supported through CLI/API (`--custom-type-schema`, `RegisterCustomTypeSchema`), and verification validates base + custom schema. |
3939
| AC8 Framework PR only | PASS | Frameworks are YAML-only; no code change required to add files. |
40-
| AC9 Sigstore parity | PASS | cosign key/cert verification paths, release signing, and release signature verification are wired and tested. |
41-
| AC10 Determinism proof | PASS | Determinism/contract checks are gated (hash-chain integrity, exit-code contract, golden-style deterministic checks in tests/scripts). |
42-
| AC11 Gait backward compatibility | PASS | Native Gait pack and embedded signed-JSON verification with key-id compatibility and signature checks implemented and covered. |
40+
| AC9 Sigstore parity | PASS | cosign key/cert verification paths cover records/chains and Gait `proof_records.jsonl` verification with Sigstore options. |
41+
| AC10 Determinism proof | PASS | Deterministic vector assertions are enforced in tests with a dedicated cross-platform determinism workflow. |
42+
| AC11 Gait backward compatibility | PASS | Native Gait pack + signed-JSON verification with key-id compatibility is covered, including committed compatibility fixtures in `testdata/gait_compat/`. |
4343
| AC12 Exit code contract | PASS | Implemented and validated in unit + contract script. |
4444

4545
## Clyra_DEV Standards Check
@@ -51,7 +51,7 @@ Status key:
5151
| Pre-commit hooks | PASS | `.pre-commit-config.yaml` added. |
5252
| Testing tiers | PASS | Tiered scripts and workflows are present for unit/integration/e2e/acceptance/hardening/chaos/performance/soak/contract. |
5353
| Coverage gates | PASS | Coverage gates enforce package-level `>=85` for core/cmd stack (with narrow allowlist exceptions) and `>=75` package baseline. |
54-
| Main CI pipeline | PASS | PR and main workflows with lint/test/build/contract checks. |
54+
| Main CI pipeline | PASS | PR and main workflows with lint/test/build/contract checks, plus deterministic vector checks in cross-platform CI. |
5555
| Nightly pipelines | PASS | Nightly workflow includes hardening/chaos/soak/acceptance with cross-platform hardening matrix + performance job. |
5656
| Release integrity | PASS | GoReleaser, checksums, SBOM, grype, cosign sign/verify, and provenance attestation are wired in release workflow. |
5757
| Security scanning | PASS | CodeQL + `gosec` + `govulncheck` + release vulnerability scan are present in CI/release gates. |

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Core capabilities:
1212
- Hash-chain append and integrity verification
1313
- Ed25519 signing and verification
1414
- Cosign/Sigstore-backed signature verification support
15+
- Bundle manifest signing and verification (Ed25519 + cosign)
16+
- Runtime custom record type registration (schema-per-type)
1517
- Offline CLI verification and inspection
1618
- Versioned schemas for proof record types
1719
- Gait pack/runpack compatibility verification
@@ -89,9 +91,10 @@ cosign verify-blob \
8991
## What You Get
9092

9193
- **Deterministic canonicalization**: JSON/SQL/URL/text domains with stable digests.
94+
- **Digest metadata**: `algo_id` + optional `salt_id`, including HMAC-SHA-256 helpers.
9295
- **Tamper evidence**: record hashes + chain head continuity checks.
9396
- **Signature options**: Ed25519 and cosign verification support.
94-
- **Schema contracts**: built-in proof record schemas and custom schema validation.
97+
- **Schema contracts**: built-in proof record schemas plus runtime custom type validation.
9598
- **Cross-product compatibility**: Gait pack/runpack verification and migration-friendly compatibility packages.
9699

97100
## CLI Surface
@@ -100,6 +103,7 @@ cosign verify-blob \
100103
proof verify <path> Verify record, chain, bundle, gait pack/runpack/signed JSON
101104
proof verify --signatures --public-key <key> <path>
102105
proof verify --signatures --cosign-key <pubkey-path> <path>
106+
proof verify --custom-type-schema <type>=<schema.json> <path>
103107
proof verify --revocation-list <path> --revocation-key <pubkey> <path>
104108
proof chain verify <path> [--from RFC3339] [--to RFC3339]
105109
proof inspect <path> [--record <record_id>]
@@ -114,7 +118,7 @@ Global flags:
114118

115119
- `--json`
116120
- `--quiet`
117-
- `--explain`
121+
- `--explain` (step diagnostics to stderr)
118122

119123
## Gait Compatibility
120124

@@ -133,6 +137,12 @@ For Gait extraction/migration support, Proof exposes compatibility packages:
133137
- `github.com/Clyra-AI/proof/schema`
134138
- `github.com/Clyra-AI/proof/exitcode`
135139

140+
Compatibility fixtures used by tests:
141+
142+
- `testdata/gait_compat/trace_signed.json`
143+
- `testdata/gait_compat/approval_token_signed.json`
144+
- `testdata/gait_compat/delegation_token_signed.json`
145+
136146
## Library Usage
137147

138148
Primary API:
@@ -159,6 +169,19 @@ _, _ = proof.Sign(&chain.Records[0], key)
159169
_, _ = proof.VerifyChain(chain)
160170
```
161171

172+
Custom type registration + bundle signing:
173+
174+
```go
175+
_ = proof.RegisterCustomTypeSchema("vendor.custom_event", "./custom.schema.json")
176+
177+
manifest, _ := proof.SignBundle("./bundle", key)
178+
_, _ = proof.VerifyBundle("./bundle", proof.BundleVerifyOpts{
179+
VerifySignatures: true,
180+
PublicKey: proof.PublicKey{Public: key.Public},
181+
})
182+
_ = manifest
183+
```
184+
162185
## Contract Commitments
163186

164187
- Deterministic canonicalization and hashing for supported domains
@@ -182,6 +205,7 @@ Exit codes:
182205

183206
![Main](https://github.com/Clyra-AI/proof/actions/workflows/main.yml/badge.svg)
184207
![CodeQL](https://github.com/Clyra-AI/proof/actions/workflows/codeql.yml/badge.svg)
208+
![Determinism](https://github.com/Clyra-AI/proof/actions/workflows/determinism.yml/badge.svg)
185209

186210
```bash
187211
make fmt
@@ -195,6 +219,7 @@ Key automation:
195219

196220
- Main CI: `.github/workflows/main.yml`
197221
- PR CI: `.github/workflows/pr.yml`
222+
- Determinism CI: `.github/workflows/determinism.yml`
198223
- Nightly hardening/perf: `.github/workflows/nightly.yml`
199224
- Release: `.github/workflows/release.yml`
200225

cmd/proof/artifacts.go

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package main
33
import (
44
"archive/zip"
55
"bufio"
6-
"crypto/sha256"
7-
"encoding/hex"
86
"encoding/json"
97
"errors"
108
"fmt"
@@ -30,15 +28,6 @@ const (
3028
artifactGaitSignedJSON artifactKind = "gait_signed_json"
3129
)
3230

33-
type manifest struct {
34-
Files []manifestEntry `json:"files"`
35-
}
36-
37-
type manifestEntry struct {
38-
Path string `json:"path"`
39-
SHA256 string `json:"sha256"`
40-
}
41-
4231
func detectArtifact(path string) (artifactKind, error) {
4332
info, err := os.Stat(path)
4433
if err != nil {
@@ -150,30 +139,21 @@ func loadChain(path string) (*proof.Chain, error) {
150139
return c, nil
151140
}
152141

153-
func verifyBundle(path string) error {
154-
// #nosec G304 -- CLI accepts explicit local artifact paths.
155-
raw, err := os.ReadFile(filepath.Join(path, "manifest.json"))
156-
if err != nil {
157-
return err
158-
}
159-
var m manifest
160-
if err := json.Unmarshal(raw, &m); err != nil {
161-
return err
162-
}
163-
for _, file := range m.Files {
164-
// #nosec G304 -- Bundle manifest drives local file verification.
165-
b, err := os.ReadFile(filepath.Join(path, file.Path))
142+
func verifyBundle(path string, verifySignatures bool, publicKey string, cosignOpts proof.CosignVerifyOpts) error {
143+
var pub proof.PublicKey
144+
if strings.TrimSpace(publicKey) != "" {
145+
decoded, err := decodePublicKey(publicKey)
166146
if err != nil {
167147
return err
168148
}
169-
sum := sha256.Sum256(b)
170-
got := hex.EncodeToString(sum[:])
171-
want := strings.TrimPrefix(file.SHA256, "sha256:")
172-
if got != want {
173-
return fmt.Errorf("bundle hash mismatch for %s", file.Path)
174-
}
149+
pub = decoded
175150
}
176-
return nil
151+
_, err := proof.VerifyBundle(path, proof.BundleVerifyOpts{
152+
VerifySignatures: verifySignatures,
153+
PublicKey: pub,
154+
Cosign: cosignOpts,
155+
})
156+
return err
177157
}
178158

179159
func appendJSONLRecords(c *proof.Chain, path string) error {
@@ -201,22 +181,28 @@ func appendJSONLRecords(c *proof.Chain, path string) error {
201181
return scanner.Err()
202182
}
203183

204-
func verifyGaitPack(path string, verifySignatures bool, publicKey string) (*gait.Result, error) {
184+
func verifyGaitPack(path string, verifySignatures bool, publicKey string, cosignOpts proof.CosignVerifyOpts) (*gait.Result, error) {
205185
var pubKey []byte
206186
if verifySignatures {
207-
if strings.TrimSpace(publicKey) == "" {
187+
if strings.TrimSpace(publicKey) != "" {
188+
pub, err := decodePublicKeyValue(publicKey)
189+
if err != nil {
190+
return nil, err
191+
}
192+
pubKey = pub
193+
}
194+
if len(pubKey) == 0 && strings.TrimSpace(cosignOpts.KeyPath) == "" && strings.TrimSpace(cosignOpts.CertificatePath) == "" {
208195
return nil, fmt.Errorf("--public-key is required for gait pack signature verification")
209196
}
210-
pub, err := decodePublicKeyValue(publicKey)
211-
if err != nil {
212-
return nil, err
213-
}
214-
pubKey = pub
215197
}
216-
return gait.VerifyPack(path, verifySignatures, pubKey)
198+
return gait.VerifyPackWithOptions(path, gait.VerifyOpts{
199+
VerifySignatures: verifySignatures,
200+
PublicKey: pubKey,
201+
Cosign: cosignOpts,
202+
})
217203
}
218204

219-
func verifyGaitRunpack(path string, verifySignatures bool, publicKey string) (*gait.RunpackResult, error) {
205+
func verifyGaitRunpack(path string, verifySignatures bool, publicKey string, _ proof.CosignVerifyOpts) (*gait.RunpackResult, error) {
220206
var pubKey []byte
221207
if verifySignatures {
222208
if strings.TrimSpace(publicKey) == "" {
@@ -228,7 +214,10 @@ func verifyGaitRunpack(path string, verifySignatures bool, publicKey string) (*g
228214
}
229215
pubKey = pub
230216
}
231-
return gait.VerifyRunpack(path, verifySignatures, pubKey)
217+
return gait.VerifyRunpackWithOptions(path, gait.VerifyOpts{
218+
VerifySignatures: verifySignatures,
219+
PublicKey: pubKey,
220+
})
232221
}
233222

234223
func verifyGaitSignedJSON(path, publicKey string) error {

cmd/proof/artifacts_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func TestVerifyBundle(t *testing.T) {
6666
manifestJSON := `{"files":[{"path":"records.jsonl","sha256":"sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356"}]}`
6767
testutil.WriteFile(t, filepath.Join(dir, "manifest.json"), []byte(manifestJSON))
6868

69-
require.NoError(t, verifyBundle(dir))
69+
require.NoError(t, verifyBundle(dir, false, "", proof.CosignVerifyOpts{}))
7070
}
7171

7272
func TestDecodePublicKeyErrors(t *testing.T) {
@@ -144,7 +144,7 @@ func TestDetectAndVerifyGaitPack(t *testing.T) {
144144
require.NoError(t, err)
145145
require.Equal(t, artifactGaitPack, kind)
146146

147-
res, err := verifyGaitPack(zipPath, true, hex.EncodeToString(pub))
147+
res, err := verifyGaitPack(zipPath, true, hex.EncodeToString(pub), proof.CosignVerifyOpts{})
148148
require.NoError(t, err)
149149
require.Equal(t, 2, res.FilesVerified)
150150
require.Equal(t, 1, res.ProofRecordsVerified)
@@ -224,7 +224,7 @@ func TestDetectAndVerifyGaitRunpack(t *testing.T) {
224224
require.NoError(t, err)
225225
require.Equal(t, artifactGaitRunpack, kind)
226226

227-
res, err := verifyGaitRunpack(zipPath, true, base64.StdEncoding.EncodeToString(pub))
227+
res, err := verifyGaitRunpack(zipPath, true, base64.StdEncoding.EncodeToString(pub), proof.CosignVerifyOpts{})
228228
require.NoError(t, err)
229229
require.Equal(t, "run-1", res.RunID)
230230
require.Equal(t, 4, res.FilesVerified)

cmd/proof/chain.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func newChainCmd(opts *globalOpts) *cobra.Command {
1717
Short: "Verify chain integrity",
1818
Args: cobra.ExactArgs(1),
1919
RunE: func(cmd *cobra.Command, args []string) error {
20+
explainf(opts, "chain verify path=%s from=%s to=%s", args[0], fromStr, toStr)
2021
c, err := loadChain(args[0])
2122
if err != nil {
2223
return newCLIError(exitcode.InvalidInput, err.Error())

cmd/proof/explain.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
func explainf(opts *globalOpts, format string, args ...any) {
9+
if opts == nil || !opts.explain || opts.quiet {
10+
return
11+
}
12+
_, _ = fmt.Fprintf(os.Stderr, "explain: "+format+"\n", args...)
13+
}

cmd/proof/frameworks.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func newFrameworksCmd(opts *globalOpts) *cobra.Command {
1414
Use: "list",
1515
Short: "List available frameworks",
1616
RunE: func(cmd *cobra.Command, args []string) error {
17+
explainf(opts, "frameworks list")
1718
list, err := framework.List()
1819
if err != nil {
1920
return newCLIError(exitcode.InternalError, err.Error())
@@ -33,6 +34,7 @@ func newFrameworksCmd(opts *globalOpts) *cobra.Command {
3334
Short: "Show a framework definition",
3435
Args: cobra.ExactArgs(1),
3536
RunE: func(cmd *cobra.Command, args []string) error {
37+
explainf(opts, "frameworks show id=%s", args[0])
3638
f, err := framework.Load(args[0])
3739
if err != nil {
3840
return newCLIError(exitcode.InvalidInput, err.Error())

0 commit comments

Comments
 (0)