Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Reject otherwise valid equal local/remote confirmation tags in both `Finish` methods with `ErrConfirmationFailed` (#308). With forced equal shares and equal associated data, the old responder accepted its own B tag reflected as C before initiator confirmation. No feasible network attack causing this condition with fresh production randomness was established. The correction preserves wire and MAC derivation, releases no C or Session on rejection, and consumes state; it also rejects the negligible honest equal-message collision case. See [the analysis](docs/key-confirmation-reflection.md) for regression coverage and required release-evidence refresh.

## v0.1.3 - 2026-08-01

- Pre-v1 caller-input API change (breaking relative to `v0.1.2`): replace public `Config` with role-local `Input`. Removed fields are `InitiatorID`, `ResponderID`, and `AssociatedData`; callers now use `SelfID`, `PeerID`, and `LocalAssociatedData`. Migration rule: the initiator calls `Start` with `SelfID=initiator, PeerID=responder`, and the responder calls `Respond` with `SelfID=responder, PeerID=initiator`. `Password`, `Context`, `SessionID`, and `AllowEmptySessionID` keep the same semantics, and wire format is unchanged.
Expand Down
2 changes: 2 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func respondWithRandom(input Input, messageA []byte, random io.Reader) (*Respond
// Finish consumes message B, verifies the responder confirmation tag, and
// returns message C plus an authenticated session. The initiator state is
// consumed even when message parsing or confirmation fails.
// A valid peer tag equal to the local tag is rejected with ErrConfirmationFailed.
func (i *Initiator) Finish(messageB []byte) ([]byte, *Session, error) {
core, err := i.finishCore()
if err != nil {
Expand Down Expand Up @@ -122,6 +123,7 @@ func (i *Initiator) Close() error {
// Finish consumes message C, verifies the initiator confirmation tag, and
// returns an authenticated session. The responder state is consumed even when
// message parsing or confirmation fails.
// A valid peer tag equal to the local tag is rejected with ErrConfirmationFailed.
func (r *Responder) Finish(messageC []byte) (*Session, error) {
core, err := r.finishCore()
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func (c *initiatorCore) finish(peerYb, peerAdb, peerTag []byte) ([]byte, *Sessio
return nil, nil, ErrConfirmationFailed
}
tagA := tr.initiatorConfirmationTag(isk, c.sid)
// Reject equal confirmation tags before releasing our tag or a Session.
// CI role binding does not distinguish identical MAC inputs (#308).
if hmac.Equal(tagA, peerTag) {
return nil, nil, ErrConfirmationFailed
}
return tagA, newSession(isk, tr.transcriptID(), peerAdb, c.peerID), nil
}

Expand Down Expand Up @@ -114,6 +119,12 @@ func (c *responderCore) finish(peerTagC []byte) (*Session, error) {
if !hmac.Equal(expectedA, peerTagC) {
return nil, ErrConfirmationFailed
}
// A correct tag is not peer confirmation if it is also our own tag.
// Equal shares and AD otherwise let B's tag be reflected as C (#308).
tagB := c.transcript.responderConfirmationTag(c.isk, c.sid)
if hmac.Equal(tagB, peerTagC) {
return nil, ErrConfirmationFailed
}
return newSession(c.isk, c.transcript.transcriptID(), c.transcript.initiatorAD(), c.peerID), nil
}

Expand Down
2 changes: 2 additions & 0 deletions docs/evidence-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This document is the module for pinned evidence baselines. It names the commit,

## Current Release-Claim State

Issue #308 changes confirmation acceptance to reject equal local/remote tags in both Finish paths; see [the reflection analysis](key-confirmation-reflection.md). The older pinned dependency/SAST, Capslock, security/spec/vector, and paired long-fuzz lanes below do not cover this security-relevant candidate. Refresh those lanes against the exact selected source candidate before any stronger release claim. Local regression checks are implementation validation only, and no new release candidate or production-readiness claim is selected here. Historical `v0.1.3` evidence remains valid only for its recorded candidate contract.

The frozen `v0.1.3` candidate source baseline is `1f19e278112fa037890848ed6c086addeffdca4e`, selected from a clean worktree under Go 1.26.5 after the PR #227 disposition and toolchain update. The dependency, vulnerability, SAST/gosec, Capslock capability, security/spec audit, Go 1.26.4 to Go 1.26.5 vector-stability, paired long-fuzz, and pre-publication GitHub release-control lanes are current for that candidate contract and are assembled in `docs/evidence/1f19e27-20260801-candidate/`. Signed annotated tag `v0.1.3` points to documentation-only descendant `b4174c6bf4bae78f4081c3d6d6baebff5f1cbbf1`; [Release Validation run 30686073480](https://github.com/the-sarge/cpace/actions/runs/30686073480) and the consumer verification recorded on issue #238 complete the publication-only lane without making a production-readiness claim.

The current dependency, vulnerability, and SAST/gosec evidence is the clean-worktree Go 1.26.5 capture in `docs/evidence/1f19e27-20260731/`. Module integrity passed, `govulncheck@v1.3.0` reported no vulnerabilities, and the test-inclusive pinned `gosec@v2.26.1` scan reported zero issues. The current protocol bundle is `docs/evidence/1f19e27-20260731-protocol/`: Capslock classes/counts are unchanged, the focused protocol audit passed, and the normalized 112-line vector event streams are bit-identical under Go 1.26.4 and Go 1.26.5 with shared SHA-256 `05e966e9b4cc8ea6883d5f8a750cc2d91098a3e59aee365ed852e717067efbd9`. These completed lanes are prerelease evidence, not a production-readiness claim.
Expand Down
34 changes: 34 additions & 0 deletions docs/evidence/308-reflection-20260923/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Issue #308 reflection validation

Source candidate: `d2db6eed5923395bf0dfb57e73275ea0a1506c44` (`fix: reject reflected confirmation tags in both roles (#308)`). Base: `87528342d68d26db4a5a69386ae5647baf7d05ed`. Validation ran from a clean dedicated worktree on 2026-09-23 under Go 1.27.1 on Darwin/arm64. This bundle and the link from the analysis are a documentation-only descendant of that source candidate; the candidate contains all implementation, tests, and security-conclusion changes.

## Results

| Command | Result |
| --- | --- |
| `go test -run '^(TestReflection\|TestRistrettoDraft21Vectors\|TestIRTranscriptDraftVectorFlow)' -count=1 -v .` | PASS; 22 reflection subtests, IR transcript flow, and draft-21 vector test with its six generator sid mutation subtests |
| `task --silent check` | PASS; full normal/race suites, Markdown/whitespace, release helper/policy checks, CI classifier/tool/fuzz-input checks, evidence checks, formatting/imports, pinned GolangCI-Lint, ast-grep, and govulncheck |
| `go mod verify` | PASS; all modules verified |

`validation.log` preserves the exact commands, candidate SHA, worktree state, UTC command-start timestamps, environment, raw output, and return codes. `--silent` suppresses Task command echo only; check output is preserved. Go's build/test cache was not globally disabled; the focused regression/vector command and the evidence checker explicitly use `-count=1`, and the root normal/race results in this transcript are uncached (no cached marker). The definition of every constituent `task check` command is pinned by the candidate's `Taskfile.yml`.

Verify the raw transcript from this directory with:

```sh
shasum -a 256 -c SHA256SUMS
```

No detached signature is attached to this local implementation-validation packet; it is not a signed release packet.

## Review

The code-review skill ran independent read-only Standards and Spec agents against the pinned diff `87528342d68d26db4a5a69386ae5647baf7d05ed...d2db6eed5923395bf0dfb57e73275ea0a1506c44`.

- Standards: no material actionable findings. The checks preserve the CPace core boundary and terminal cleanup, use `hmac.Equal`, and add no persistent secret fields. Tests follow helper, assertion, and snapshot conventions; documentation records the narrow finding-driven exception and evidence limits. No actionable baseline smells were reported.
- Spec: no findings against issue #308 and its triage brief. Both roles, the AD matrix, correctly framed authentic-tag reflection, forced equality, honest controls, replay, nil outputs, consumption, and cleanup were covered. The review found no scope creep or incorrect implementation. Its pending validation-capture caveat is satisfied by this bundle.

## Limitations

The [analysis](../../key-confirmation-reflection.md) bounds the security conclusion: the forced equal-message false confirmation was reproduced, but no feasible way for a network attacker to force that condition with fresh production randomness was established. Randomized regression observations do not estimate collision probability, and replay tests do not exhaust concurrent schedules.

These checks and automated reviews are not independent cryptographic review or a refreshed release-evidence packet. Historical dependency/SAST, Capslock, security/spec/vector, and paired long-fuzz evidence remains pinned to its older candidates. Refresh the affected lanes against the exact selected release candidate before any stronger release claim, as recorded in the [evidence baseline](../../evidence-baseline.md). No hosted CI, cross-platform campaign, paired long-fuzz run, or new Capslock capture is claimed here.
1 change: 1 addition & 0 deletions docs/evidence/308-reflection-20260923/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
b98367e6a6f9afe9046509668c3f7d41743b96a166413f8b0d1a53dbab70466c validation.log
122 changes: 122 additions & 0 deletions docs/evidence/308-reflection-20260923/validation.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@

UTC 2026-09-23T18:14:58.755539+00:00
$ git rev-parse HEAD
d2db6eed5923395bf0dfb57e73275ea0a1506c44
exit_code=0

UTC 2026-09-23T18:14:58.766847+00:00
$ git status --short --branch
## codex/308-reflection-regressions
exit_code=0

UTC 2026-09-23T18:14:58.779543+00:00
$ uname -srm
Darwin 27.0.0 arm64
exit_code=0

UTC 2026-09-23T18:14:58.783040+00:00
$ go version
go version go1.27.1 darwin/arm64
exit_code=0

UTC 2026-09-23T18:14:58.790186+00:00
$ task --version
3.53.1
exit_code=0

UTC 2026-09-23T18:14:58.821730+00:00
$ go test -run ^(TestReflection|TestRistrettoDraft21Vectors|TestIRTranscriptDraftVectorFlow) -count=1 -v .
=== RUN TestReflectionResponderRejectsOwnTag
=== RUN TestReflectionResponderRejectsOwnTag/empty/production_randomness
=== RUN TestReflectionResponderRejectsOwnTag/empty/forced_equal_shares
=== RUN TestReflectionResponderRejectsOwnTag/equal_nonempty/production_randomness
=== RUN TestReflectionResponderRejectsOwnTag/equal_nonempty/forced_equal_shares
=== RUN TestReflectionResponderRejectsOwnTag/different/production_randomness
=== RUN TestReflectionResponderRejectsOwnTag/different/forced_equal_shares
--- PASS: TestReflectionResponderRejectsOwnTag (0.00s)
--- PASS: TestReflectionResponderRejectsOwnTag/empty/production_randomness (0.00s)
--- PASS: TestReflectionResponderRejectsOwnTag/empty/forced_equal_shares (0.00s)
--- PASS: TestReflectionResponderRejectsOwnTag/equal_nonempty/production_randomness (0.00s)
--- PASS: TestReflectionResponderRejectsOwnTag/equal_nonempty/forced_equal_shares (0.00s)
--- PASS: TestReflectionResponderRejectsOwnTag/different/production_randomness (0.00s)
--- PASS: TestReflectionResponderRejectsOwnTag/different/forced_equal_shares (0.00s)
=== RUN TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag
=== RUN TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/empty/production_randomness
=== RUN TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/empty/forced_equal_shares
=== RUN TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/equal_nonempty/production_randomness
=== RUN TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/equal_nonempty/forced_equal_shares
=== RUN TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/different/production_randomness
=== RUN TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/different/forced_equal_shares
--- PASS: TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag (0.00s)
--- PASS: TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/empty/production_randomness (0.00s)
--- PASS: TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/empty/forced_equal_shares (0.00s)
--- PASS: TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/equal_nonempty/production_randomness (0.00s)
--- PASS: TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/equal_nonempty/forced_equal_shares (0.00s)
--- PASS: TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/different/production_randomness (0.00s)
--- PASS: TestReflectionInitiatorRejectsReflectedShareAndAuthenticTag/different/forced_equal_shares (0.00s)
=== RUN TestReflectionInitiatorWithoutTagOracle
=== RUN TestReflectionInitiatorWithoutTagOracle/empty
=== RUN TestReflectionInitiatorWithoutTagOracle/equal_nonempty
=== RUN TestReflectionInitiatorWithoutTagOracle/different
--- PASS: TestReflectionInitiatorWithoutTagOracle (0.00s)
--- PASS: TestReflectionInitiatorWithoutTagOracle/empty (0.00s)
--- PASS: TestReflectionInitiatorWithoutTagOracle/equal_nonempty (0.00s)
--- PASS: TestReflectionInitiatorWithoutTagOracle/different (0.00s)
=== RUN TestReflectionHonestExchangeControls
=== RUN TestReflectionHonestExchangeControls/empty
=== RUN TestReflectionHonestExchangeControls/equal_nonempty
=== RUN TestReflectionHonestExchangeControls/different
=== RUN TestReflectionHonestExchangeControls/forced_equal_shares_different_ad
--- PASS: TestReflectionHonestExchangeControls (0.00s)
--- PASS: TestReflectionHonestExchangeControls/empty (0.00s)
--- PASS: TestReflectionHonestExchangeControls/equal_nonempty (0.00s)
--- PASS: TestReflectionHonestExchangeControls/different (0.00s)
--- PASS: TestReflectionHonestExchangeControls/forced_equal_shares_different_ad (0.00s)
=== RUN TestReflectionRejectsTagsFromAnotherExchange
=== RUN TestReflectionRejectsTagsFromAnotherExchange/empty
=== RUN TestReflectionRejectsTagsFromAnotherExchange/equal_nonempty
=== RUN TestReflectionRejectsTagsFromAnotherExchange/different
--- PASS: TestReflectionRejectsTagsFromAnotherExchange (0.00s)
--- PASS: TestReflectionRejectsTagsFromAnotherExchange/empty (0.00s)
--- PASS: TestReflectionRejectsTagsFromAnotherExchange/equal_nonempty (0.00s)
--- PASS: TestReflectionRejectsTagsFromAnotherExchange/different (0.00s)
=== RUN TestIRTranscriptDraftVectorFlow
--- PASS: TestIRTranscriptDraftVectorFlow (0.00s)
=== RUN TestRistrettoDraft21Vectors
=== RUN TestRistrettoDraft21Vectors/generator_sid_mutation_first_byte
=== RUN TestRistrettoDraft21Vectors/generator_sid_mutation_last_byte
=== RUN TestRistrettoDraft21Vectors/generator_sid_mutation_nil
=== RUN TestRistrettoDraft21Vectors/generator_sid_mutation_empty
=== RUN TestRistrettoDraft21Vectors/generator_sid_mutation_appended
=== RUN TestRistrettoDraft21Vectors/generator_sid_mutation_truncated
--- PASS: TestRistrettoDraft21Vectors (0.00s)
--- PASS: TestRistrettoDraft21Vectors/generator_sid_mutation_first_byte (0.00s)
--- PASS: TestRistrettoDraft21Vectors/generator_sid_mutation_last_byte (0.00s)
--- PASS: TestRistrettoDraft21Vectors/generator_sid_mutation_nil (0.00s)
--- PASS: TestRistrettoDraft21Vectors/generator_sid_mutation_empty (0.00s)
--- PASS: TestRistrettoDraft21Vectors/generator_sid_mutation_appended (0.00s)
--- PASS: TestRistrettoDraft21Vectors/generator_sid_mutation_truncated (0.00s)
PASS
ok github.com/the-sarge/cpace 0.203s
exit_code=0

UTC 2026-09-23T18:14:59.205312+00:00
$ task --silent check
release helper smoke tests passed
ok github.com/the-sarge/cpace/tools/releasepolicy 0.225s
release policy checker passed
CI change classifier tests passed
Go tool helper tests passed
Fuzz input validator tests passed
ok github.com/the-sarge/cpace/tools/evidencebaseline 0.338s
evidence baseline checker passed
ok github.com/the-sarge/cpace 0.125s
ok github.com/the-sarge/cpace 1.462s
0 issues.
No vulnerabilities found.
exit_code=0

UTC 2026-09-23T18:15:16.583100+00:00
$ go mod verify
all modules verified
exit_code=0
Loading
Loading