feat: anchor providers (OpenTimestamps + git + Sigstore Rekor) — PR 3#2
Merged
feat: anchor providers (OpenTimestamps + git + Sigstore Rekor) — PR 3#2
Conversation
…er with swallow-transient-errors contract
Implements plan Task 10 (PR 3).
AnchorProvider interface (providers/provider.go):
- Name(), Anchor(), Verify(), Cost()
- ConfirmationLevel enum: pending | confirmed | finalized
- Verification.Swallowed/ErrorMessage for transient-error contract (§ 3.5c)
OpenTimestamps provider (providers/opentimestamps/):
- Library choice: github.com/opentimestamps/go-opentimestamps does not exist
(repository not found on GitHub). `ots` CLI binary also absent. Implemented
direct HTTP calendar server API instead:
Anchor → POST <calendar>/digest (32 raw bytes)
Verify → GET <calendar>/timestamp/<hash_hex>
Raw calendar receipts stored as base64 JSON in ProofData for future
OTS binary parsing without re-anchoring.
- Partial-success anchor: succeeds if ≥1 calendar accepts; errors only if all fail.
- Swallow contract: network + 5xx errors swallowed (Swallowed=true); 4xx → hard error.
- Confirmed wins: if any calendar returns HTTP 200 upgrade, confirmation advances.
- OTS_TEST=1 gate for real-network tests; all tests compile and pass clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements plan Task 11 (PR 3). Uses github.com/go-git/go-git/v5 for all git operations (no exec subprocess). Behaviour: - Anchor: clones remote into tmpdir, writes anchors/<YYYY-MM-DD>/<hash16>.json, commits with configurable CommitTemplate, pushes. Handles empty remote (first anchor) via initWithRemote (PlainInit + CreateRemote + HEAD ref). Returns Confirmation: Finalized immediately — git push = instant-final. - Verify: ls-remote (no object download) to check remote reachability. Swallows network/unreachable errors per § 3.5c. Hard error on malformed ProofData. Always returns Finalized once push was confirmed. Tests use local bare repos (git init --bare in t.TempDir) for full clone+commit+push round-trips with no network dependency. 13/13 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements plan Task 12 (PR 3).
Uses github.com/sigstore/rekor v1.5.1 Go client (swagger-generated) with
runtimeclient transport configured from Config.RekorURL — making the base
URL injectable for tests (httptest.Server with RekorURL = srv.URL).
Behaviour:
- Anchor: generates ephemeral ECDSA P-256 key per call, signs
SHA256(merkle_root_bytes), submits hashedrekord v0.0.1 entry to Rekor.
Returns Confirmation: Finalized immediately — Rekor is append-only/permanent.
ExternalID = Rekor log entry UUID (key of the response map).
- Verify: GetLogEntryByUUID; classifies errors:
404 → hard error (entry missing from transparency log is tampering)
5xx (IsServerError) → transient, swallowed per § 3.5c
network / unknown → transient, swallowed per § 3.5c
Ephemeral key note: a stable identity key should be used in production
for auditable signing identity; the pilot uses ephemeral keys to avoid
key-management requirements.
All 13 mock tests pass; 2 SIGSTORE_TEST=1 real-network tests compile and
show as SKIP. go build ./... clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes two issues from code-reviewer on commit 63f028c. Critical — context propagation: cloneOrInit now takes ctx context.Context and calls PlainCloneContext instead of PlainClone. Context cancellation / deadline is now respected during the clone phase. Auth also threaded through CloneOptions.Auth. Important — authentication support: Added auth fields to Config: UseSSHAgent, SSHKeyPath/SSHKeyPassword (PEM key file), HTTPUsername/HTTPPassword (Basic Auth / PAT for HTTPS remotes). buildAuth() constructs the transport.AuthMethod at NewProvider time using go-git's ssh.NewSSHAgentAuth / ssh.NewPublicKeysFromFile / http.BasicAuth. Auth passed to CloneOptions.Auth, PushOptions.Auth, and ListOptions.Auth. NewProvider returns error if SSHKeyPath is specified but file is missing. Minor — json.Marshal error no longer silently discarded. New tests: HTTPAuth wires correctly, missing SSH key file errors at NewProvider, no-auth anonymous succeeds. 16/16 pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…xx classification Fixes two issues from code-reviewer on commit 380c986. Critical — double-hash crypto bug: The original code computed digest = sha256.Sum256(hashBytes) and signed that, but declared Data.Hash.Value = root.Hex (= hex(hashBytes)). Rekor's hashedrekord validator verifies ecdsa.VerifyASN1(pubKey, hashBytes, sig), so the signature over sha256(hashBytes) would always fail real validation. Fix: sign hashBytes directly (it is already a 32-byte sha256 digest — a valid ECDSA input). Data.Hash.Value and signature are now consistent. Removed the now-unused crypto/sha256 import. Important — non-404 4xx swallowed instead of hard error: Added defErr.IsClientError() case before defErr.IsServerError() in the Verify error switch. Non-404 4xx from Rekor (e.g., 400 bad UUID) now returns a hard error — malformed ProofData is a data integrity problem, not a transient failure. New test: TestVerify_Rekor400_HardError. Minor — json.Marshal error now handled explicitly. 14/14 tests pass; go build ./... clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
intel352
added a commit
that referenced
this pull request
May 3, 2026
…tion) — PR 4 (#3) * feat(modules): add audit.ledger + audit.anchor_provider.* modules - Add dsn field to LedgerConfig proto + three new anchor provider config proto messages (OpenTimestampsProviderConfig, GitAnchorProviderConfig, SigstoreProviderConfig); regenerate gen/audit.pb.go. - modules/ledger.go: LedgerModule typed with LedgerConfig; Init opens Postgres via sql.Open, creates chain.Appender, registers in ledger registry keyed by partition name; Stop unregisters + closes DB. - modules/anchor_provider.go: anchorProviderModule wraps each provider; typed factory functions (NewOpenTimestampsProviderModule, NewGitAnchorProviderModule, NewSigstoreProviderModule) construct the underlying provider and register it on Init. - 14 unit tests; all pass without a running Postgres instance. - internal/plugin.go: implement TypedModuleProvider (CreateTypedModule) so the gRPC server uses the typed proto path — zero map[string]any at the module creation boundary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(modules): address code-review issues in Task 13 Critical: - NewLedgerModule now validates config.Name is non-empty; silently registering under key "" would corrupt the ledger registry. Important: - Fix AuditChainPlugin doc comment: remove false TypedStepProvider claim (Task 14 adds that interface). Add compile-time assertion `var _ sdk.TypedModuleProvider = (*AuditChainPlugin)(nil)`. - Add 5 CreateTypedModule tests (the primary gRPC path was unexercised): valid LedgerConfig, nil config, mismatched anypb type, deferred providers, unknown type. Minor: - TestLedgerModule_StopUnregisters: add missing t.Cleanup guard. - LedgerModule.Init: guard against double-call to prevent DB pool leak. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(steps): add 7 audit-plugin step types with typed proto contracts Implements Task 14. All step handlers use sdk.TypedStepProvider (zero map[string]any). Key changes: - proto/audit.proto: add ledger field (5) to PollAnchorConfirmationRequest so poll handlers can retrieve the right *sql.DB from the registry. - gen/audit.pb.go: regenerated. - modules/ledger.go: add RegisterDB/GetDB/UnregisterDB registry; Init() and Stop() register/unregister both the chain.Appender and *sql.DB. - modules/anchor_provider.go: add AnchorProviderNames() for step.audit.anchor "all configured providers" fallback. - steps/: new package — AppendHandler, VerifyHandler, MerkleRootHandler, AnchorHandler, PollAnchorConfirmationHandler (swallow-transient-errors contract), ProofHandler (RFC 6962 inclusion proof), PublicReceiptHandler (verifiable receipt JSON + SHA256 hash, optional field redaction). - internal/plugin.go: wire TypedStepProvider; stepFactories slice maps each step type to its sdk.NewTypedStepFactory; legacy CreateStep path returns "not yet implemented via legacy path" for known types. - internal/plugin_test.go: TestTypedStepTypes_Declared, TestCreateTypedStep_UnknownType_ReturnsError, TestCreateTypedStep_KnownType_ReturnsInstance. All packages pass: GOWORK=off go test ./... -race -count=1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(steps): address spec-reviewer feedback on Task 14 Issue 1 — poll_anchor_confirmation transient/hard-error test coverage: - steps/testutil_test.go: add mockAnchorProvider (controls Verify return) and openFakeDB / fakeSQLDriver (fake database/sql driver returning confirmation="pending" for any SELECT without a real Postgres instance). - steps/poll_anchor_confirmation_test.go: add TestPollAnchorConfirmationHandler_TransientError_Swallowed — asserts swallowed=true, transitioned=false, error_message set, no gRPC error. Add TestPollAnchorConfirmationHandler_HardError_PropagatesGRPC — asserts non-nil error when provider.Verify returns a hard error. Issue 2 — public_receipt pseudonym format and replace-not-delete: - ApplyRedactions (now exported for direct testing): assigns "contributor_N" labels (N increments per unique original value within the receipt scope); REPLACES the field value with the pseudonym string instead of deleting the field; deduplicates: identical originals share one label. - steps/public_receipt_test.go: add TestApplyRedactions_TwoDistinctFields, TestApplyRedactions_DuplicateOriginalValue, TestApplyRedactions_NoRedactFields. Issue 3 — map[string]any in public_receipt.go: - Replaced map[string]any receipt document and anchorRecordSlice with typed structs: receiptDocument, receiptEntry, receiptMerkleProof, receiptAnchor. Zero map[string]any in the package. All packages pass: GOWORK=off go test ./... -race -count=1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(task14): address all 4 code-reviewer issues - chain: Append/AppendTx return DB-assigned createdAt via RETURNING created_at; steps/append.go propagates DB timestamp to AppendResponse (not app clock) - chain: update all call sites in append_test.go to 4-return destructuring - poll_anchor_confirmation: guard against downgrade with forward-only confirmationOrder map (pending<confirmed<finalized) - anchor: INSERT ... ON CONFLICT (ledger,provider,range_start,range_end) DO NOTHING for idempotent re-runs; matching unique index added to 004_indexes.sql + down Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(task15): add integration tests for plugin entrypoint - TestIntegration_BinaryBuilds: shells out to go build to verify the cmd/workflow-plugin-audit-chain binary compiles and links end-to-end - TestIntegration_AllStepsAreTyped: creates all 7 step types and verifies Execute() returns the typed-step dispatch error, proving each is a TypedStepInstance (not a legacy step) - TestIntegration_LedgerModuleLifecycle: exercises the full Init→Start→Stop lifecycle, verifying ledger+DB registries are populated after Init and cleared after Stop; also verifies double-Init guard - TestIntegration_AnchorProviderModule_OpenTimestamps: lifecycle smoke test for the OTS anchor provider module (no network calls on Init) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(task15): root-level E2E integration test (append-5, verify, Merkle, proof) integration_test.go at the module root exercises the full audit-chain scenario through the plugin factory chain: 1. Spins up ephemeral Postgres 16 via testcontainers + applies all 4 migrations. 2. Declares audit.ledger via internal.NewPlugin().CreateTypedModule — exercises the typed module factory wiring in internal/plugin.go. 3. Appends 5 entries via step.audit.append; verifies sequence 1-5 and 64-char hashes. 4. Verifies chain integrity over all 5 entries via step.audit.verify; expects Valid=true, EntriesVerified=5. 5. Computes Merkle root over entries 1-5 via step.audit.merkle_root; cross-checks against independently computed chain.MerkleRoot. 6. Records a mock audit_anchors row for range 1-5 with the computed root. 7. Retrieves and cryptographically verifies the Merkle inclusion proof for entry 3 via step.audit.proof + chain.VerifyInclusion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(task15): rewrite E2E integration test to use gRPC subprocess transport Addresses all 3 spec-reviewer rejection issues: 1. Delete internal/integration_test.go (wrong location — was internal package, not repo root). 2. Root integration_test.go now includes the full audit chain scenario: append-5, verify, Merkle root, cross-check, mock anchor, inclusion proof for entry 3, and chain.VerifyInclusion cryptographic check. 3. All step executions go through real gRPC proto serialisation: - testGRPCPlugin wraps go-plugin and dispenses pb.PluginServiceClient directly (bypassing *ext.PluginClient's unexported fields). - buildBinary/startPlugin compile the actual binary and start it as a subprocess via goplugin.NewClient + ext.Handshake. - Each request is packed as anypb.Any, sent over TCP gRPC to the subprocess, and unpacked from the typed response — genuine wire serialisation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(task15): address code-reviewer Important #1, #3 and Minor #4 - Important #1: add testing.Short() guard at top of TestE2E_AuditChainScenario so Docker-dependent test skips cleanly under `go test -short ./...`; remove redundant guard from buildBinary (test-level guard fires first). - Important #3: log StopModule errors in t.Cleanup instead of discarding with blank identifier. - Minor #4: run `GOWORK=off go mod tidy` — promotes testcontainers-go and testcontainers-go/modules/postgres from // indirect to direct deps. Important #2 (direct step handler calls) was already resolved in the prior commit (1aa6765) which rewrote the test to use goplugin.NewClient subprocess + pb.PluginServiceClient gRPC calls. Code-reviewer reviewed an earlier commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: strict-contracts — add kind/mode fields, drop unimplemented providers plugin.contracts.json: rewrote all 12 descriptors with required "kind", "type", and "mode": "strict_proto" fields (previously all had neither "kind" nor "mode", causing the wfctl validator to skip every descriptor and report 14 coverage-gap errors). plugin.json: remove audit.anchor_provider.ethereum and audit.anchor_provider.aws_qldb from capabilities.moduleTypes — those providers are not implemented and must not appear in the advertised contract surface. Validator now passes: OK workflow-plugin-audit-chain v0.1.0 (plugin.json) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct trigger output proto type AuditEntry → Entry The trigger.audit.entry_appended contract referenced a non-existent proto type workflow.plugin.audit.v1.AuditEntry. The actual message is workflow.plugin.audit.v1.Entry (proto/audit.proto line 242). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements plan Task 10/11/12 — the three anchor provider backends for the audit-chain plugin. Each provider implements the
AnchorProviderinterface (providers/provider.go) and the swallow-transient-errors contract from § 3.5c.OpenTimestamps (
providers/opentimestamps/): direct HTTP calendar API (POST /digest,GET /timestamp/<hash>); no Go library exists. Partial-success anchor (≥1 calendar must succeed). Transient: network + 5xx swallowed; 4xx = hard error. ~17 tests (16 mock + 2 real-network gated byOTS_TEST=1).git (
providers/git/): go-git v5; clones remote into tmpdir, writesanchors/<date>/<hash>.json, commits with configurableCommitTemplate, pushes. Handles empty remotes (first anchor). Instant-finalized on push. Auth support: SSH agent, PEM key file, HTTP Basic/PAT. 16 tests using local bare repos (no network).Sigstore Rekor (
providers/sigstore/): rekor swagger client (v1.5.1); ephemeral ECDSA P-256 key per anchor;hashedrekord v0.0.1entry. SignshashBytesdirectly (merkle root IS a sha256 digest). Error classification: 404 = hard (tampering), non-404 4xx = hard (malformed ProofData), 5xx/network = swallowed. 14 tests (12 mock + 2 real-network gated bySIGSTORE_TEST=1).Test plan
GOWORK=off go test ./providers/... -race -count=1— all passGOWORK=off go build ./...— clean🤖 Generated with Claude Code