diff --git a/caldav/issue1_crypto_test.go b/caldav/issue1_crypto_test.go new file mode 100644 index 0000000..55eea90 --- /dev/null +++ b/caldav/issue1_crypto_test.go @@ -0,0 +1,205 @@ +package caldav + +import ( + "bytes" + "errors" + "io" + "log" + "net/http" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/emersion/go-ical" + f "github.com/mathewcsims/protoxide/internal/issue1test" + "github.com/mathewcsims/protoxide/protonmail" +) + +func issue1Card(t *testing.T, author *openpgp.Entity, body string) protonmail.CalendarEventCard { + t.Helper() + return protonmail.CalendarEventCard{Type: protonmail.CalendarEventCardSigned, + Data: body, Signature: f.Sign(t, author, body, f.Config(f.Epoch))} +} + +// Semantic comparison deliberately ignores VCALENDAR serialization and property +// ordering. These synthetic fixtures contain one non-recurring UTC event only. +func issue1AssertEvent(t *testing.T, event *ical.Event, summary string) { + t.Helper() + for name, want := range map[string]string{ical.PropUID: "issue1@example.invalid", ical.PropSummary: summary} { + value, err := event.Props.Text(name) + f.Must(t, err) + if value != want { + t.Fatalf("%s semantic mismatch: got %q want %q", name, value, want) + } + } + start, err := event.DateTimeStart(nil) + f.Must(t, err) + end, err := event.DateTimeEnd(nil) + f.Must(t, err) + if !start.Equal(time.Date(2025, 1, 2, 12, 0, 0, 0, time.UTC)) || end.Sub(start) != 30*time.Minute { + t.Fatal("event time semantics changed") + } +} + +func TestIssue1ReadEventCardValidSemantics(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + calendar := f.Key(t, "calendar", f.Config(f.Epoch)) + for _, encrypted := range []bool{false, true} { + card := issue1Card(t, author, f.ICS) + key := "" + if encrypted { + card.Type = protonmail.CalendarEventCardEncryptedAndSigned + key, card.Data = f.Encrypt(t, calendar, f.ICS, false) + } + event := ical.NewEvent() + _, err := readEventCard(event, card, openpgp.EntityList{author}, openpgp.EntityList{calendar}, key) + f.Must(t, err) + issue1AssertEvent(t, event, "Synthetic baseline") + } +} + +func TestIssue1ReadEventCardRejectsCiphertextBeforeMerge(t *testing.T) { + calendar := f.Key(t, "calendar", f.Config(f.Epoch)) + for _, aead := range []bool{false, true} { + key, data := f.Encrypt(t, calendar, f.ICS, aead) + for _, mutation := range []string{"tampered-tail", "truncated", "missing-trailer"} { + event := ical.NewEvent() + event.Props.SetText(ical.PropSummary, "last trustworthy") + card := protonmail.CalendarEventCard{Type: protonmail.CalendarEventCardEncrypted, Data: f.Mutate(t, data, mutation)} + _, err := readEventCard(event, card, nil, openpgp.EntityList{calendar}, key) + if err == nil { + t.Fatalf("corrupt card accepted: aead=%v mutation=%s", aead, mutation) + } + summary, err := event.Props.Text(ical.PropSummary) + f.Must(t, err) + if summary != "last trustworthy" || len(event.Props) != 1 { + t.Fatal("failed card modified the merge target") + } + } + } +} + +func TestIssue1BaselineServesUnverifiedSignatures(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + other := f.Key(t, "other", f.Config(f.Epoch)) + const marker = "ISSUE1_SYNTHETIC_PRIVATE_SUMMARY" + body := strings.Replace(f.ICS, "Synthetic baseline", marker, 1) + for _, tc := range []struct{ name, signature string }{ + {"invalid-mathematics", f.Sign(t, author, f.ICS, f.Config(f.Epoch))}, + {"unknown-issuer", f.Sign(t, other, body, f.Config(f.Epoch))}, + {"malformed", "malformed armored signature"}, + } { + t.Run(tc.name, func(t *testing.T) { + var logs bytes.Buffer + oldWriter := log.Writer() + log.SetOutput(&logs) + defer log.SetOutput(oldWriter) + event := ical.NewEvent() + card := protonmail.CalendarEventCard{Type: protonmail.CalendarEventCardSigned, Data: body, Signature: tc.signature} + _, err := readEventCard(event, card, openpgp.EntityList{author}, nil, "") + f.Must(t, err) + issue1AssertEvent(t, event, marker) + if !strings.Contains(logs.String(), "unverified signature") { + t.Fatal("expected baseline verification warning absent") + } + if strings.Contains(logs.String(), marker) || strings.Contains(logs.String(), "BEGIN PGP") { + t.Fatal("fixture content or key armor leaked to diagnostics") + } + t.Log("KNOWN_GAP: baseline serves rejected signature; successful characterization is not security acceptance") + }) + } +} + +func TestIssue1BaselineMixedCardTrustAndDowngrades(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + good := issue1Card(t, author, f.ICS) + changed := strings.Replace(f.ICS, "Synthetic baseline", "Untrusted override", 1) + for _, tc := range []struct { + name string + kind protonmail.CalendarEventCardType + signature string + }{ + {"invalid-signed-card", protonmail.CalendarEventCardSigned, good.Signature}, + {"type-downgrade-to-clear", protonmail.CalendarEventCardClear, good.Signature}, + {"signature-stripped", protonmail.CalendarEventCardSigned, ""}, + {"unknown-type", protonmail.CalendarEventCardType(99), good.Signature}, + } { + t.Run(tc.name, func(t *testing.T) { + candidate := &protonmail.CalendarEvent{ + SharedEvents: []protonmail.CalendarEventCard{good}, + CalendarEvents: []protonmail.CalendarEventCard{{Type: tc.kind, Data: changed, Signature: tc.signature}}, + } + merged, err := toIcalCalendar(candidate, openpgp.EntityList{author}, nil) + f.Must(t, err) + events := merged.Events() + if len(events) != 1 { + t.Fatal("expected one merged fixture event") + } + issue1AssertEvent(t, &events[0], "Untrusted override") + t.Log("KNOWN_GAP: weaker calendar card overwrites authenticated shared-card properties") + }) + } + t.Run("missing-all-required-cards", func(t *testing.T) { + merged, err := toIcalCalendar(&protonmail.CalendarEvent{}, nil, nil) + f.Must(t, err) + events := merged.Events() + if len(events) != 1 || events[0].Props.Get(ical.PropUID) != nil { + t.Fatal("baseline empty-card behavior changed; review the diagnosis") + } + t.Log("KNOWN_GAP: baseline constructs an event without required-card validation") + }) +} + +type issue1Transport func(*http.Request) (*http.Response, error) + +func (transport issue1Transport) RoundTrip(req *http.Request) (*http.Response, error) { + return transport(req) +} + +func TestIssue1KeyLookupFailureIsNotUnknownIssuer(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + lookupFailure := errors.New("synthetic key lookup unavailable") + for _, tc := range []struct { + name, response string + fail, wantError bool + }{ + {"transport-failure", "", true, true}, + {"empty-successful-lookup", `{"Code":1000,"Keys":[]}`, false, false}, + {"malformed-key", `{"Code":1000,"Keys":[{"PublicKey":"malformed"}]}`, false, true}, + } { + t.Run(tc.name, func(t *testing.T) { + calls := 0 + client := &protonmail.Client{RootURL: "https://issue1.invalid", HTTPClient: &http.Client{ + Transport: issue1Transport(func(req *http.Request) (*http.Response, error) { + calls++ + if req.URL.Host != "issue1.invalid" || req.Method != http.MethodGet || req.URL.Path != "/keys" { + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) + } + if tc.fail { + return nil, lookupFailure + } + return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(tc.response)), Request: req}, nil + }), + }} + b := &backend{c: client, privateKeys: openpgp.EntityList{author}, keyCache: make(map[string]openpgp.EntityList)} + event := &protonmail.CalendarEvent{ID: "synthetic-event", Author: "author@example.invalid", SharedEvents: []protonmail.CalendarEventCard{issue1Card(t, author, f.ICS)}} + obj, err := getCalendarObject(b, "synthetic-calendar", nil, event, protonmail.CalendarSettings{}) + if (err != nil) != tc.wantError { + t.Fatalf("got error=%v wantError=%v", err, tc.wantError) + } + if tc.fail && !errors.Is(err, lookupFailure) { + t.Fatalf("transport error identity lost: %v", err) + } + if calls != 1 { + t.Fatalf("expected one fake key lookup, got %d", calls) + } + if !tc.wantError { + if obj == nil { + t.Fatal("baseline did not serve unknown-issuer object") + } + t.Log("KNOWN_GAP: successful empty lookup becomes warning-only unknown-issuer acceptance") + } + }) + } +} diff --git a/docs/issue-1-baseline.md b/docs/issue-1-baseline.md new file mode 100644 index 0000000..5bbbeef --- /dev/null +++ b/docs/issue-1-baseline.md @@ -0,0 +1,222 @@ +# Issue 1: baseline fixtures and corrected diagnosis + +Status: **partial — synthetic fixtures verified; security acceptance not run**. +This document and the adjacent tests are stage 1 of **one proposed draft PR**, +not a security fix, release, or claim that Issue 1 is resolved. The initial +engineering timebox is 16 hours, separate from all later policy, persistence, +status and enforcement work. Later stages need their own scope and entry gate. + +## Source and evidence boundary + +- [Issue 1](https://github.com/mathewcsims/protoxide/issues/1), captured 2026-09-14. +- Baseline `f332c8bf873d2cfa4314d4f155d1acc2b3af0e42`; PR 2 is not included. +- `github.com/ProtonMail/go-crypto v1.3.0`; dependency locks are unchanged. +- Tests use synthetic identities under `example.invalid`, generated test keys, + fixed test clocks and an in-memory fake HTTP transport. They never authenticate + to Proton or send calendar mutations, email, or external notifications. +- The matrix completed on **2026-09-14 at 16:35:37 UTC**, on Linux amd64 with + Go 1.26.8 and 1.27.1. The three added Go files in this review revision are + byte-identical to those executed; only documentation/evidence is revised. + Tests were **not rerun for this documentation revision**. +- [Sanitized results](issue-1-results.json) contain exact source/dependency and + toolchain identities, recorded commands, original log hashes, test outcomes + and per-run fixture hashes. Host paths, account data, key material and raw log + text are excluded. The original execution receipts/logs remain retained. +- The entry gate for this stage is a pinned source and synthetic-only execution + perimeter. Its exit is reviewable tests, corrected diagnosis and bounded + evidence, not enforcement or release readiness. No GitHub authentication is + required to reproduce the local tests. + +## Corrected diagnosis (source review plus generated-fixture evidence) + +1. `caldav.readEventCard` already calls `io.ReadAll(md.UnverifiedBody)` and + returns read errors before parsing or merging ICS. The extra commented drain + is not evidence that MDC verification is disabled. +2. In pinned go-crypto, `checkReader` and `signatureCheckReader` perform integrity + finalization when consumed to EOF. Protoxide's `detachedSignatureReader` + separately verifies its buffered detached signature at EOF. The tests cover + both reader paths, including partial reads and corrupted trailing bytes. +3. `MessageDetails.UnverifiedBody` is an `io.Reader`, not an `io.ReadCloser`. + Calling its `Close()` is not a valid fix. Reading it again after EOF does not + recreate the plaintext or repair detached verification. +4. `CheckArmoredDetachedSignature` expects original armored input. Passing an + already armor-decoded binary signature to it is incorrect; either keep the + armor or use the binary verifier. A direct-library fixture compares both + correct calls with the proposed incorrect call. +5. Calendar passphrase signatures are not checked before unlocking keys. + `readEventCard` logs and continues after detached signature errors. Unknown + outer card types default to unsigned/clear behavior, and later calendar cards + can overwrite earlier shared-card properties. These are **demonstrated + baseline gaps**, deliberately characterized, not endorsed acceptance behavior. +6. The pinned library separates mathematical signature verification from + expiry/revocation checks using `Config.Now()`. Tests generate genuine + signatures while keys are valid, then verify at controlled later times. + Expiry is not proof of tampering. Library fixtures do not establish Proton's + historical-format policy or trustworthy historical signing time. + +Relevant pinned library sources: [read.go](https://github.com/ProtonMail/go-crypto/blob/v1.3.0/openpgp/read.go), +[packet/config.go](https://github.com/ProtonMail/go-crypto/blob/v1.3.0/openpgp/packet/config.go). + +## Fixture map and interpretation + +| Family | Test coverage | Current evidence | +| --- | --- | --- | +| Passphrase signatures | valid, modified passphrase with old signature, wrong signer, missing/malformed armor and packets; unlock compared with direct verifier | verified: baseline unlocks despite rejected signatures | +| Event signatures | clear/encrypted valid signatures, changed content, wrong signer, malformed signature; CalDAV warning-only acceptance | verified: errors detected, then ignored by the tested CalDAV merge path | +| Ciphertext integrity | valid MDC/SEIPDv2-AEAD controls, tampered trailer, truncation and removed trailer; library/card/CalDAV paths | verified: tested corruption rejected before ICS merge | +| EOF/partial reads | short reads, detached verification only at EOF, repeat EOF, inline-signed library reader | verified for pinned reader fixtures | +| Key availability | unknown issuer, synthetic transport failure, successful empty lookup, malformed returned key | verified: errors propagate, but empty successful lookup leads to unverified acceptance | +| Historical policy | key/signature expiry before/at/after boundary; genuine signed-at-valid-time fixture; invalid mathematics and revocation combined with expiry | verified library classifications, not Proton policy acceptance | +| Types/downgrades | all four card types, unknown type, stripped signature, changed signed content relabeled clear | verified baseline unsigned/downgrade behavior | +| Aggregation | one-event semantic checks, conflicting shared/calendar properties, missing required cards, corrupt card leaves merge target unchanged | verified gaps and local pre-merge protection, not persistent cache safety | + +Tests named `Baseline...` and messages marked `KNOWN_GAP` document existing +unsafe behavior. A green characterization result is **not security acceptance**. +Convert these expectations deliberately when reviewed enforcement changes land; +do not preserve the vulnerability just to preserve a green baseline suite. +An unsigned format's lack of author assurance is not itself proof of corruption +or an unsupported format. The type fixtures record that assurance boundary; +legitimate unsigned roles and mutation eligibility still require policy. + +In the pinned historical fixtures, the expiry second itself is accepted and +the following second reports key/signature expiry. Modified content returns a +mathematical signature error even after expiry; revocation takes precedence +over expiry for the genuine signature; an empty keyring yields unknown issuer. +These are observed outcomes for these constructions, not a complete taxonomy +for every packet combination. No production clock is changed. + +The test helper is an internal package imported only by `_test.go` files. Its +fixed session key is test-only; generated RSA private keys stay in memory. +Each run logs SHA-256 hashes of length-delimited concrete fixture components, +not plaintext, signatures or key armor. Random key generation means hashes can +differ between runs. Reproduction means the pinned generator/dependencies, +recorded clocks, classifications and semantic outcomes, not deterministic RSA +bytes. The result receipt retains 70 fixture-hash observations from each +toolchain's focused run. These are observations, not 70 independent scenarios. + +SEIPDv2 AEAD fixtures prove only the pinned library format, not Proton wire +parity. Removing a trailer may cause framing failure before an MDC/AEAD check; +tests must not relabel every decryption error as a cryptographic tag mismatch. +Tampered-tail controls specifically require the expected body-integrity error. +Semantic ICS checks cover one non-recurring UTC event's UID, summary, start and +duration, not a general-purpose equivalence algorithm. + +## Recorded results and reproduction boundary + +| Check | Go 1.26.8 | Go 1.27.1 | +| --- | --- | --- | +| Untouched baseline and contribution root tests | verified | verified | +| Focused fixtures: 14 top-level tests and 35 subtests; zero failed/skipped | verified | verified | +| Nested go-webdav tests | verified | verified | +| Focused race checks in protonmail/caldav (`TestIssue1` only) | verified | verified | +| Full-root vet | failed: existing warnings | failed: existing warnings | + +The four baseline/contribution vet outputs are byte-identical: unkeyed +`go-message/mail.Header` and `go-message.Header` literals at +`exports/messages.go:30`. These are pre-existing findings, not a clean vet run. +The race result is **not** a full-repository concurrency audit. + +Recheck the [official Go support window](https://go.dev/doc/devel/release) before +a new execution. The recorded matrix used **Go 1.26.8 and Go 1.27.1**. +There is no supported Go 1.23 lane. This stage leaves `go.mod` and `go.sum` +unchanged; language minimum and future release-builder pinning are separate +decisions, not an inferred `toolchain` directive change. + +The recorded focused invocation was: + +```sh +go test -vet=off -json -count=1 -run '^TestIssue1' ./protonmail ./caldav +``` + +Disabling vet in the focused fixture invocation separates test execution from +inherited vet failures; **vet was also run separately**. The JSON receipt lists +all 17 original child commands, their subjects, exit codes and output hashes, +including root/nested suites, focused race checks and the scanner. Reproduction +requires an approved build environment with the matching sources/toolchains; +no production bridge or live account is needed. + +`govulncheck v1.8.0` ran in source/symbol mode on Go 1.27.1. Its captured database +was last modified at `2026-09-10T14:48:42Z`. JSON-mode exit 0 is **not scan-clean**: +109 finding records cover 23 advisory IDs (23 module, 12 package, 74 symbol +records). All symbol records concern GO-2026-4550. CIRCL v1.6.1 is below the +v1.6.3 fixed threshold, but the captured Go advisory is unreviewed and broad. +The maintainer advisory identifies `ecc/p384.CombinedMult`; this scan has no +`ecc/p384` trace. Its `CombinedMult` trace is for `ecc/goldilocks`, not P-384. + +Provisional assessment, moderate confidence: version exposure is verified; +reachability of that specific defect is **not demonstrated**. Revisit with a +symbol-specific advisory or reviewed import/call-graph evidence. The other 22 +advisories have module-only records here; this is not proof of safety. See the +[Go advisory](https://pkg.go.dev/vuln/GO-2026-4550) and +[maintainer advisory](https://github.com/cloudflare/circl/security/advisories/GHSA-q9hv-hpm4-hj6x). +Dependency remediation and a fresh scan remain separate work, not this patch. + +Keep commands, toolchain checksums, per-vector hashes and sanitized logs in the +external evidence receipt. Use `verified`, `failed`, `blocked` and `not run` +literally. A failed matrix or uncovered fixture remains partial; do not remove +the README's security caveat or advertise this stage as hardened. + +## Gates retained for later commits in the same draft + +The following are **proposed policy**, not implemented behavior: + +- Keep cryptographic assurance, synchronization freshness and mutation + eligibility separate. Old verified bytes do not establish current Proton + state or current acceptability if new findings concern that snapshot itself. +- Unknown issuer does not establish a historical legitimate author. Retain an + acceptable verified snapshot or fail closed; new unverified reads require a + separate maintainer-approved opt-in. Expired, revoked, unknown-issuer, + malformed and mathematically invalid signatures stay distinct. Never rewind + production verification time to an untrusted timestamp. Historical-expiry + allowances require explicit policy and Proton-format evidence. +- Legitimate clear/unsigned and encrypted/unsigned formats have **no author + signature assurance**. Integrity is not author authentication. Outer type + changes cannot upgrade trust or bypass required checks; the weakest required + contributing card bounds the aggregate. Corruption/invalid cryptographic + signatures and malformed passphrase signatures remain hard failures. +- Persist event snapshot, committed cursor and restrictions crash-consistently. + A rejected page immediately persists restrictions while data/cursor remain + unchanged. Interrupted persistence leaves recovery write-blocked. Later + failures and restarts cannot remove restrictions; only a validated replacement + or confirmed, committed deletion can clear them. Legacy state requires validated, + gap-free reconciliation rather than reseeding at `latest`. +- Transient retries are bounded (30/60/120/240/300 seconds), followed by a visible + pause; deterministic corruption pauses immediately. No skipped poison pages + or freshness promises for objects behind them. Test all commit-boundary + crashes, refresh races, concurrent PUT/DELETE, and deletion then corruption + within one page. Candidate state needs versioned migration, durable writes + and rollback that cannot discard restrictions. +- Proposed authenticated, account-scoped `/status`, `/status.json` and extended + `protoxide status` must expose verification assessment, last successful + reconciliation, stale/degraded state, restrictions and retry status. Keep + integrity metadata outside VEVENT; never inject `X-PROTOXIDE-INTEGRITY`. + Unchanged ETag/CTag and sidecar metadata alone are not a Thunderbird warning. + Real HTTP GET/REPORT/multiget/PUT/DELETE tests must distinguish retained stale + reads, collection failures, integrity-blocked writes (403), temporary + reconciliation failures (503), and genuine deletion versus false 404 or empty + success, with zero Proton event mutations from blocked requests. Healthy + individual objects remain readable even when collection synchronization fails. +- The maintainer must decide unsigned roles, historical-expiry policy, any new + unknown-issuer compatibility and status/recovery behavior. One draft PR is + the default; discuss any split rather than presume it. No strict-verification + release until dependent no-data-loss safeguards pass. + +The intended sequence within the same draft is: (1) these fixtures/diagnosis; +(2) an explicit policy and acceptance decision record; (3) dependent +no-data-loss safeguards, recovery and status; (4) demonstrated verification +fixes and acceptance evidence. Stage 1's passing tests do not open the later +implementation gates or commit the maintainer to this proposed policy. + +Live Proton account operations and macOS Thunderbird stable/ESR testing require +separate user authorization and a dedicated synthetic test calendar. Public +fixtures do not imply live acceptance. This Linux test execution does not add +Linux product support. PR 2, recurrence, attendees/invitations, cache encryption, +API migration and release automation are outside this contribution stage. +Posting and cutover also require separate +authorization. Registration, installation, runtime checks and end-to-end +acceptance remain separate. This stage changes none of them. + +Rollback: remove/revert only this stage's test/helper/documentation additions. +There are no production state migrations, credentials or deployed artifacts to +roll back. Later stages have no estimate or implementation authorization until +their decision record and entry criteria are resolved. diff --git a/docs/issue-1-results.json b/docs/issue-1-results.json new file mode 100644 index 0000000..978cea1 --- /dev/null +++ b/docs/issue-1-results.json @@ -0,0 +1,1153 @@ +{ + "schema_version": 1, + "status": "partial", + "scope": "Synthetic characterization, not security acceptance", + "observed_at": "2026-09-14T16:35:37.397772+00:00", + "source": { + "repository": "https://github.com/mathewcsims/protoxide", + "issue": "https://github.com/mathewcsims/protoxide/issues/1", + "base_sha": "f332c8bf873d2cfa4314d4f155d1acc2b3af0e42", + "go_crypto": "v1.3.0", + "test_source_sha256": { + "caldav/issue1_crypto_test.go": "e69c4079f56ec4ed0ba0efbe90d512313a12369baf7835cb6e15aa675e051de3", + "protonmail/issue1_crypto_test.go": "c274044d92cd32ce0f2037d71c1cc6ea730242527f2fbbf4193b35e4a2591a3c", + "internal/issue1test/fixtures.go": "cb32975840bae4b7708d9f2504ce24c2e3d4b1e70ebd94dc1f6fa21306be988e" + }, + "dependency_lock_sha256": { + "go.mod": "532463dc8aff7bb22bf72ed46bc2574d55f2b210d71c8672e634eaf53a3f1200", + "go.sum": "f4599f4d9c044d74269c80bb7633e7aa3306a662682de4ef4899160fd9b180a6" + } + }, + "platform": "linux/amd64", + "toolchains": [ + { + "version": "1.26.8", + "archive_sha256": "d0f743b33e8d8945e6b1f432edd15785c70507121d6e2a723b21285eddf8b57b" + }, + { + "version": "1.27.1", + "archive_sha256": "63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445" + } + ], + "runtime_rerun_for_documentation_revision": "not run", + "evidence_reuse": "The three Go additions and all production files are byte-identical to the executed matrix; documentation is updated separately.", + "records": [ + { + "toolchain": "1.26.8", + "check": "baseline-root", + "recorded_command": [ + "go", + "test", + "-json", + "./..." + ], + "working_directory": ".", + "subject": "untouched baseline", + "exit_code": 0, + "original_log_sha256": "892bd587bc9f59102bad10cb9b9eb6336072887e64a501329496e5e91ae8898c" + }, + { + "toolchain": "1.26.8", + "check": "baseline-vet", + "recorded_command": [ + "go", + "vet", + "./..." + ], + "working_directory": ".", + "subject": "untouched baseline", + "exit_code": 1, + "original_log_sha256": "b750c553d702c8be152f412729cbf0755bfeb0ad1bcc206c88af3f5321e00790" + }, + { + "toolchain": "1.26.8", + "check": "focused", + "recorded_command": [ + "go", + "test", + "-vet=off", + "-json", + "-count=1", + "-run", + "^TestIssue1", + "./protonmail", + "./caldav" + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "777b9f6a6c64bd230eab0b40e8d2f8e434e0a8fef7096726cc28493674aea186" + }, + { + "toolchain": "1.26.8", + "check": "root", + "recorded_command": [ + "go", + "test", + "-json", + "./..." + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "48ff975f1a6f2ad09360b3c08b9123c0a6a1c3bfd22c6e6f83d77e33c1a06213" + }, + { + "toolchain": "1.26.8", + "check": "nested", + "recorded_command": [ + "go", + "test", + "-json", + "./..." + ], + "working_directory": "third_party/go-webdav", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "113832c1045f8549854954293ed77760431b590870d20218a8daa7421ff6fbc0" + }, + { + "toolchain": "1.26.8", + "check": "race", + "recorded_command": [ + "go", + "test", + "-vet=off", + "-race", + "-json", + "-count=1", + "-run", + "^TestIssue1", + "./protonmail", + "./caldav" + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "6639229cbd59bbb99e1594cf3789755e97bee7bb82c06cefa0684b6369eb40c1" + }, + { + "toolchain": "1.26.8", + "check": "vet", + "recorded_command": [ + "go", + "vet", + "./..." + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 1, + "original_log_sha256": "b750c553d702c8be152f412729cbf0755bfeb0ad1bcc206c88af3f5321e00790" + }, + { + "toolchain": "1.27.1", + "check": "baseline-root", + "recorded_command": [ + "go", + "test", + "-json", + "./..." + ], + "working_directory": ".", + "subject": "untouched baseline", + "exit_code": 0, + "original_log_sha256": "18c8dc1d782320403b1fb9114d8d6518b67c005187fd2dccb60bbf84f706ac3c" + }, + { + "toolchain": "1.27.1", + "check": "baseline-vet", + "recorded_command": [ + "go", + "vet", + "./..." + ], + "working_directory": ".", + "subject": "untouched baseline", + "exit_code": 1, + "original_log_sha256": "b750c553d702c8be152f412729cbf0755bfeb0ad1bcc206c88af3f5321e00790" + }, + { + "toolchain": "1.27.1", + "check": "focused", + "recorded_command": [ + "go", + "test", + "-vet=off", + "-json", + "-count=1", + "-run", + "^TestIssue1", + "./protonmail", + "./caldav" + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "d43431a3418c122340df2e09f356da2ad7d295e6786d3542fc09169041ef1856" + }, + { + "toolchain": "1.27.1", + "check": "root", + "recorded_command": [ + "go", + "test", + "-json", + "./..." + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "23f7561c49ad2b76fb612e5677dd756f604544bc3bdde13ddf6a76068ada0f4a" + }, + { + "toolchain": "1.27.1", + "check": "nested", + "recorded_command": [ + "go", + "test", + "-json", + "./..." + ], + "working_directory": "third_party/go-webdav", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "32b5e94da3327c8ceb00d9b873f0157a2433bf6bdf3cac92bedd4492b9ea7140" + }, + { + "toolchain": "1.27.1", + "check": "race", + "recorded_command": [ + "go", + "test", + "-vet=off", + "-race", + "-json", + "-count=1", + "-run", + "^TestIssue1", + "./protonmail", + "./caldav" + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "3aad30a87cd44ad5a2670d870670cf12fec838e0c7e243cc44e38c027388fbd1" + }, + { + "toolchain": "1.27.1", + "check": "vet", + "recorded_command": [ + "go", + "vet", + "./..." + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 1, + "original_log_sha256": "b750c553d702c8be152f412729cbf0755bfeb0ad1bcc206c88af3f5321e00790" + }, + { + "toolchain": "1.27.1", + "check": "scanner-install", + "recorded_command": [ + "go", + "install", + "golang.org/x/vuln/cmd/govulncheck@v1.8.0" + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "cbd69eb082c7ac566c1e6b11c3537ac96523413ef704e13e6eb4c2c6923bd806" + }, + { + "toolchain": "1.27.1", + "check": "scanner-build-info", + "recorded_command": [ + "go", + "version", + "-m", + "" + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "c102647075f4a815f62ceab8d9e2ff24927762652315ab77332cc336363d3bb0" + }, + { + "toolchain": "1.27.1", + "check": "govulncheck", + "recorded_command": [ + "govulncheck", + "-json", + "./..." + ], + "working_directory": ".", + "subject": "tested contribution", + "exit_code": 0, + "original_log_sha256": "d43d7ad43f36e668d3d764ee821243511ddb8ab656702877cf513c34e7e62cb3" + } + ], + "focused_results": [ + { + "toolchain": "1.26.8", + "top_level_pass": 14, + "subtests_pass": 35, + "failed": 0, + "skipped": 0, + "passed_tests": [ + "TestIssue1PassphraseBaselineIgnoresSignature/valid", + "TestIssue1PassphraseBaselineIgnoresSignature/modified-passphrase-with-old-signature", + "TestIssue1PassphraseBaselineIgnoresSignature/wrong-signer", + "TestIssue1PassphraseBaselineIgnoresSignature/malformed-armor", + "TestIssue1PassphraseBaselineIgnoresSignature/malformed-packet", + "TestIssue1PassphraseBaselineIgnoresSignature/missing-signature", + "TestIssue1PassphraseBaselineIgnoresSignature", + "TestIssue1ArmoredVerifierExpectsOriginalArmor", + "TestIssue1ReadEventCardValidSemantics", + "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "TestIssue1EventSignatureOutcomes/clear/valid", + "TestIssue1EventSignatureOutcomes/clear/modified-content", + "TestIssue1EventSignatureOutcomes/clear/wrong-signer", + "TestIssue1EventSignatureOutcomes/clear/malformed", + "TestIssue1EventSignatureOutcomes/encrypted/valid", + "TestIssue1EventSignatureOutcomes/encrypted/modified-content", + "TestIssue1EventSignatureOutcomes/encrypted/wrong-signer", + "TestIssue1EventSignatureOutcomes/encrypted/malformed", + "TestIssue1EventSignatureOutcomes", + "TestIssue1CiphertextIntegrityAtEOF/mdc/valid", + "TestIssue1CiphertextIntegrityAtEOF/mdc/tampered-tail", + "TestIssue1CiphertextIntegrityAtEOF/mdc/truncated", + "TestIssue1CiphertextIntegrityAtEOF/mdc/missing-trailer", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/valid", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/tampered-tail", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/truncated", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/missing-trailer", + "TestIssue1CiphertextIntegrityAtEOF", + "TestIssue1BaselineServesUnverifiedSignatures/invalid-mathematics", + "TestIssue1BaselineServesUnverifiedSignatures/unknown-issuer", + "TestIssue1BaselineServesUnverifiedSignatures/malformed", + "TestIssue1BaselineServesUnverifiedSignatures", + "TestIssue1DetachedReaderChecksOnlyAtEOF", + "TestIssue1BaselineMixedCardTrustAndDowngrades/invalid-signed-card", + "TestIssue1BaselineMixedCardTrustAndDowngrades/type-downgrade-to-clear", + "TestIssue1BaselineMixedCardTrustAndDowngrades/signature-stripped", + "TestIssue1BaselineMixedCardTrustAndDowngrades/unknown-type", + "TestIssue1BaselineMixedCardTrustAndDowngrades/missing-all-required-cards", + "TestIssue1BaselineMixedCardTrustAndDowngrades", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer/transport-failure", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer/empty-successful-lookup", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer/malformed-key", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer", + "TestIssue1InlineSignedReaderIntegrity", + "TestIssue1HistoricalExpiryClockSemantics/key", + "TestIssue1HistoricalExpiryClockSemantics/signature", + "TestIssue1HistoricalExpiryClockSemantics", + "TestIssue1ClockDefaultsAndSecondPrecision", + "TestIssue1CardTypeBaselineHasNoUnsignedAssurance" + ], + "fixture_hashes": [ + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "author-public-key", + "sha256": "88d510e05c2c83d90fab74ab6892cecd1707283b0a686b8f43c29390c1b83fdf" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "other-public-key", + "sha256": "ae0ba597eab6af976137cc5ebefdb858694dd4bd61dfda6262a58a42c912f11a" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "calendar-public-key", + "sha256": "7fc166b5b9c52f516561c77e6c4e0b4034b24731034c5e62f4a9b894477ed3ba" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "detached-signature", + "sha256": "6a1d2b027b054e04c9a771a566b9d9c55808576f85b6210e6cb91d22845081ff" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "detached-signature", + "sha256": "d5de3c252ed3299893b9b9e79bf3ad86addd8466c2e05adbb05571e13f166197" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/valid", + "label": "encrypted-card", + "sha256": "fb3c285e5a66436eedc8ed93ea3f44971997c6279774391dcff5e03b58d41d3a" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/modified-passphrase-with-old-signature", + "label": "encrypted-card", + "sha256": "b4b3cdff549b908d609b5825730290fbc2426efd32a745194122164dfec8aab2" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "author-public-key", + "sha256": "27b2aaa0f5564b01b15a01e2eacade77d8a48ebb03326a8cd8eb817a8ebf950a" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/wrong-signer", + "label": "encrypted-card", + "sha256": "eac18bbc6b1bfb08f876664fc30025b03cfcdbb63c47a81bc9edaa61d23fc542" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/malformed-armor", + "label": "encrypted-card", + "sha256": "56136f52c3695cdf3615a73ff98baa067d6fbbd6118ce7b98f31a2c51cceda7c" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/malformed-packet", + "label": "encrypted-card", + "sha256": "257038c61e70ba25bdb2fceada6385b52479fdf70c7b43f8097b485fb03fe183" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/missing-signature", + "label": "encrypted-card", + "sha256": "8007f3d14d420459e239ead8ffe24d69e2ba2b40ab7f26178b94c66bc0a1f82f" + }, + { + "test": "TestIssue1ArmoredVerifierExpectsOriginalArmor", + "label": "author-public-key", + "sha256": "7fa05e7b88046591afc058ede37ed8ede2cee7578e1bf688ab8d145aea6e72fa" + }, + { + "test": "TestIssue1ArmoredVerifierExpectsOriginalArmor", + "label": "detached-signature", + "sha256": "4274f49fd5c57788a57bb3262182e3622e6fbd65acf7a55d9c4c53f2319ce58c" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "calendar-public-key", + "sha256": "5cdb15629c6618b91d7c849e2353b60a5688eb6038532b64c57ac6fab19dce2d" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "detached-signature", + "sha256": "f2e2e4a3726bfc0321fb468b7022913dd0818ef89a4822effd3789e5ba3db70a" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "detached-signature", + "sha256": "b4da06236c1bdad301c57fa1af717f837b964138db435d5a6e15cb3a854c84fa" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "encrypted-card", + "sha256": "4aed38d5404a6213de4634590194548c60bbb4de469004eaf8b730cf8d1c495a" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "author-public-key", + "sha256": "739c37a8a6a7663843f663d39e3f43fc4b8bc90430827f1fc30693949f749476" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "other-public-key", + "sha256": "5136689ec73cb59f4c7b347bb85fd3ebd1bc919b72e082fa1fe23ab22b21b9d7" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "calendar-public-key", + "sha256": "22f8be9e2f7acb29c85514855f9224210d8a1cec50fa0034de10edb0f224d110" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "encrypted-card", + "sha256": "993559604af021e5b6a538bf74607ce86e45c89e5c9521469d7f2aad438881c3" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "tampered-tail", + "sha256": "721c444d33d30352790881e96dfca9e74869272263fcb8efec4effe8f520b9a1" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "truncated", + "sha256": "7b5350c7c603a967af6fca255dad58d1869531681459c6a7b2c45a7664e4033d" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "missing-trailer", + "sha256": "8dc9852dd66f6112bce940ff725c47d031aed6fcd41f48f033f21e01da72483c" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "encrypted-card", + "sha256": "84c58a2ddbc2081c5dd3b2499565e7bf4cc7066bdee4d5a9ce3f0926db3b75c8" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "tampered-tail", + "sha256": "1233ef2710b2059475bc3e781b184b98736da16335d2b87547192e1f0164a39d" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "truncated", + "sha256": "41faacaead6ba61636d0ad3418cfb63b996a255b1add362944053f8bb40b1cbf" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "missing-trailer", + "sha256": "d147e511746af4411347a9fd20aea539f86fc2b15ec4796d2a47418ef3fb37b4" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "calendar-public-key", + "sha256": "6ab16e041bd36b9e35685d55b540a0ac46ca5d91e85ec6d94d6dfbf95076945c" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "detached-signature", + "sha256": "0994235bf6ddf580df61aa9d4e646ea76c01b7e80af0653040667e963ee51fad" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "detached-signature", + "sha256": "340f8caf56ac34d7611b6b6c89799e27817c33f8162a7cc6da7a2d4c06cdad3b" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "detached-signature", + "sha256": "d9fa1401ce8c7931bb70e012f2794dcc35239df545904df24c61ead10f7942f4" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/valid", + "label": "encrypted-card", + "sha256": "6e1894a1f5c0448d9787c0fe3582992998821d7739632a905ce071e63ae4f9c7" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/modified-content", + "label": "encrypted-card", + "sha256": "86d15fcd93684244a0e4504ecd5744326573abc159c249c7e5be7988e66f74e6" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/wrong-signer", + "label": "encrypted-card", + "sha256": "b19a974fc31cfb39d06085d89b611197b8fb31fe68d7da4ef62237b1a017b35d" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/malformed", + "label": "encrypted-card", + "sha256": "20b723be000a75e64184b774848c861905f2f8102f5d513e249dedef16e4ccf1" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "author-public-key", + "sha256": "a72bf5bc21f2bb714d4a8c06af6870669af8c26959bfa04902ad5553905f2a8b" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF", + "label": "calendar-public-key", + "sha256": "180bb523ae41cc31da9a09458dd4eaacca305f16fe25e56bb3d32145f73ab983" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF", + "label": "encrypted-card", + "sha256": "dc19d5b123f4d3fcc04e330cb3a81d1f324c89fa4b5ca760944900f0929983d2" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/mdc/tampered-tail", + "label": "tampered-tail", + "sha256": "112cf5b8c24a3108d66d68dc9817418a07569b03bb54bfb3418d70656bb87c6a" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/mdc/truncated", + "label": "truncated", + "sha256": "284664b92deb6f8b257568fd3ae33045226664a50f3bdf446434132db452a676" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/mdc/missing-trailer", + "label": "missing-trailer", + "sha256": "a7af1a268a56504f0c943d5763ee6af8b8b16d5e801cc82639beda239447778e" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF", + "label": "encrypted-card", + "sha256": "63d5650f36108ad7efd15e234e899694ca87635366c1be08b0e18e1555d0682d" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/tampered-tail", + "label": "tampered-tail", + "sha256": "d99bc051a2870c38738370ff633937daab8d76f730e88a829b303a7776b979c3" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/truncated", + "label": "truncated", + "sha256": "bbaa4ec44d8eaf36cfd346f649856ff1472733a416c72d2cdafba56aa9be9aac" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/missing-trailer", + "label": "missing-trailer", + "sha256": "e0d08100bc48ca146f0c4a1c2df501efc3165351eac70fe39d43c85abab7e0fb" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "other-public-key", + "sha256": "26c0b3c2fad654672e8177f2776192d3fd1ae5e0295bcc1216d89311a0a8da1a" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "detached-signature", + "sha256": "521770cbe6775b551a9570c220b1bb0499c100a8ddd1c85aa27357635cb4ff55" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "detached-signature", + "sha256": "4e3acdaec520d8502f8c09e7c256246e95585145ddcc17b5053fb5d343e5e9d8" + }, + { + "test": "TestIssue1DetachedReaderChecksOnlyAtEOF", + "label": "author-public-key", + "sha256": "25cabe655dae7ce3349829a42299c579d481393f5b9ef88aa8448a0d152e6301" + }, + { + "test": "TestIssue1DetachedReaderChecksOnlyAtEOF", + "label": "detached-signature", + "sha256": "b926ee85655253952fd4a96847294e7ad6176544cf5b9bb947d1ac3eabe5a5bf" + }, + { + "test": "TestIssue1InlineSignedReaderIntegrity", + "label": "author-public-key", + "sha256": "4b7b9470263ec553bda142e890687382bc229873fd5a8047c322d2e3e4759d68" + }, + { + "test": "TestIssue1BaselineMixedCardTrustAndDowngrades", + "label": "author-public-key", + "sha256": "df3dc7b563273262d687b6437c4dab9982175fd13d2715503fe26fe7f86de4a8" + }, + { + "test": "TestIssue1BaselineMixedCardTrustAndDowngrades", + "label": "detached-signature", + "sha256": "2b1ff88d8b3b66d3eefd2f3be39f42a98b19dd1f4c00b98cab2b2868dd07d28e" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer", + "label": "author-public-key", + "sha256": "a38cf4a47e8a449f5c3c2a95724c5ce5e606365aa90c066154158c12c8a19277" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer/transport-failure", + "label": "detached-signature", + "sha256": "8cf046318be13d44dc487e2183c3684c69a0ef42251a6acb71f868c78a18e904" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer/empty-successful-lookup", + "label": "detached-signature", + "sha256": "22bab33740dfee07d8877656678e832dffe109058d67d68f4de21c4f5ae0ca34" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer/malformed-key", + "label": "detached-signature", + "sha256": "273f668b0a8b740a25b2326eb5803980adc50011ba3aca2408d8743c62aba3fd" + }, + { + "test": "TestIssue1InlineSignedReaderIntegrity", + "label": "calendar-public-key", + "sha256": "0eb0098496562097905b73447471634f0ef258a056806ab8bb542e97afc560eb" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/key", + "label": "historical-public-key", + "sha256": "b83fccd57ceaf5d032a1d67b9254b9d07ea1be8e883fb980bcb2c3f6c3b1faed" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/key", + "label": "detached-signature", + "sha256": "f2d7a594e2fffadabb64fbfe9b0f738f24908241400d21ebbae9f7b98cf2e48b" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/signature", + "label": "historical-public-key", + "sha256": "4b375b6452ab183aa577fe755d972ee1ce4b035ccf5be19ca9183d557ec16f6d" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/signature", + "label": "detached-signature", + "sha256": "cbd1789b7be2bbab65352164d56012b43e6bfedf7fc2f38fa5c24f17f33ce7b3" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "author-public-key", + "sha256": "05923a5a044c552e21f36e484a814b41573dc88a085d20aecd5e65e8630589ef" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "calendar-public-key", + "sha256": "65b1f615c277c58e0084bfbccc6038a9ac5f8b067729fe12433c1bc60e33097a" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "encrypted-card", + "sha256": "c01a004a814f478689303804cc485c5f6a7199a0cd4d15307c1794a0fe7ac5ba" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "detached-signature", + "sha256": "c8a51b54feb65da391ed7830868b4f641ee4b851c04680acced842323b61ee48" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "encrypted-card", + "sha256": "997f8243edeef8081f6203353bed8bc88032a354f7423f414f8e597dd0bcd585" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "detached-signature", + "sha256": "5be0c9635c77d315e48598f275fa610c0db1a05c819188e38f9789b726f65950" + } + ] + }, + { + "toolchain": "1.27.1", + "top_level_pass": 14, + "subtests_pass": 35, + "failed": 0, + "skipped": 0, + "passed_tests": [ + "TestIssue1PassphraseBaselineIgnoresSignature/valid", + "TestIssue1PassphraseBaselineIgnoresSignature/modified-passphrase-with-old-signature", + "TestIssue1PassphraseBaselineIgnoresSignature/wrong-signer", + "TestIssue1PassphraseBaselineIgnoresSignature/malformed-armor", + "TestIssue1PassphraseBaselineIgnoresSignature/malformed-packet", + "TestIssue1PassphraseBaselineIgnoresSignature/missing-signature", + "TestIssue1PassphraseBaselineIgnoresSignature", + "TestIssue1ReadEventCardValidSemantics", + "TestIssue1ArmoredVerifierExpectsOriginalArmor", + "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "TestIssue1EventSignatureOutcomes/clear/valid", + "TestIssue1EventSignatureOutcomes/clear/modified-content", + "TestIssue1EventSignatureOutcomes/clear/wrong-signer", + "TestIssue1EventSignatureOutcomes/clear/malformed", + "TestIssue1EventSignatureOutcomes/encrypted/valid", + "TestIssue1EventSignatureOutcomes/encrypted/modified-content", + "TestIssue1EventSignatureOutcomes/encrypted/wrong-signer", + "TestIssue1EventSignatureOutcomes/encrypted/malformed", + "TestIssue1EventSignatureOutcomes", + "TestIssue1BaselineServesUnverifiedSignatures/invalid-mathematics", + "TestIssue1BaselineServesUnverifiedSignatures/unknown-issuer", + "TestIssue1BaselineServesUnverifiedSignatures/malformed", + "TestIssue1BaselineServesUnverifiedSignatures", + "TestIssue1CiphertextIntegrityAtEOF/mdc/valid", + "TestIssue1CiphertextIntegrityAtEOF/mdc/tampered-tail", + "TestIssue1CiphertextIntegrityAtEOF/mdc/truncated", + "TestIssue1CiphertextIntegrityAtEOF/mdc/missing-trailer", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/valid", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/tampered-tail", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/truncated", + "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/missing-trailer", + "TestIssue1CiphertextIntegrityAtEOF", + "TestIssue1DetachedReaderChecksOnlyAtEOF", + "TestIssue1BaselineMixedCardTrustAndDowngrades/invalid-signed-card", + "TestIssue1BaselineMixedCardTrustAndDowngrades/type-downgrade-to-clear", + "TestIssue1BaselineMixedCardTrustAndDowngrades/signature-stripped", + "TestIssue1BaselineMixedCardTrustAndDowngrades/unknown-type", + "TestIssue1BaselineMixedCardTrustAndDowngrades/missing-all-required-cards", + "TestIssue1BaselineMixedCardTrustAndDowngrades", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer/transport-failure", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer/empty-successful-lookup", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer/malformed-key", + "TestIssue1KeyLookupFailureIsNotUnknownIssuer", + "TestIssue1InlineSignedReaderIntegrity", + "TestIssue1HistoricalExpiryClockSemantics/key", + "TestIssue1HistoricalExpiryClockSemantics/signature", + "TestIssue1HistoricalExpiryClockSemantics", + "TestIssue1ClockDefaultsAndSecondPrecision", + "TestIssue1CardTypeBaselineHasNoUnsignedAssurance" + ], + "fixture_hashes": [ + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "author-public-key", + "sha256": "5006cded4f394dcd881b09a343ccc15fd8b4d88f830ba464c4374a06492473b1" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "other-public-key", + "sha256": "1d4a17f59dda292f8307b69840e29ce0e08cc10bd33955ff5f8f605425e03247" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "calendar-public-key", + "sha256": "345831574a988ace683944e837d135fdba4cb3d263df5620e053a20ba4aa4ac4" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "detached-signature", + "sha256": "84897f8ff2f29ba7af02dfc0c5111f51bddd2c1a44d6615bba0d0ddd3b7a5465" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature", + "label": "detached-signature", + "sha256": "5acefd420d450af9d874421929ae1c07317e504064e0453abcd15ef9516c3d91" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/valid", + "label": "encrypted-card", + "sha256": "c0b6f1ea99d1c51e28b7bf04af5b3287aeb7442bdf9dae2535a1cb2184e2f4c7" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/modified-passphrase-with-old-signature", + "label": "encrypted-card", + "sha256": "9a00a5a74cedde283eba06a5744ab4e08fc76454bfaae3bf9a5ea255a2b0de3f" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "author-public-key", + "sha256": "e5076bbc2a6781bfca4d70ae2843ad881ab3e68b1f97ff3d67697f57582db2b5" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/wrong-signer", + "label": "encrypted-card", + "sha256": "bb71589a9c62b3790245472885201eabd0756fa5700bc8997e824342c73485a4" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/malformed-armor", + "label": "encrypted-card", + "sha256": "03062fbf4146d1e684e43b95114ba59c2ba4882bdc3091e4a73ddb8f437970e7" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/malformed-packet", + "label": "encrypted-card", + "sha256": "4daf82bfd398e9cecd4ac9c40036f7071d5ae72fe20ea735c1bfe0b283f28fa8" + }, + { + "test": "TestIssue1PassphraseBaselineIgnoresSignature/missing-signature", + "label": "encrypted-card", + "sha256": "06ca88a0ef6596f21126747eb27350d90167d2cdcf77b631788beb3d60a82940" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "calendar-public-key", + "sha256": "d4b076fe07e6d52a45dc0131df2d262498681a305f8325d9b9d3ada45ead245c" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "detached-signature", + "sha256": "519416385ea19292fe06b0557471375c300951cb9edac0896a698e9f000676f8" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "detached-signature", + "sha256": "3a394754e2a28bf2afc919bc09b2b1d1968e812841c86b3a1d721531ea533d5a" + }, + { + "test": "TestIssue1ReadEventCardValidSemantics", + "label": "encrypted-card", + "sha256": "ffa4889c58747417e1ebf27b36c2cfcf51085f725a7534e43b3bb2f7ccd2d892" + }, + { + "test": "TestIssue1ArmoredVerifierExpectsOriginalArmor", + "label": "author-public-key", + "sha256": "6b3879caa013d3f529b7c3539a4d183709e691fea0fa5571c4d9abec9919dfe1" + }, + { + "test": "TestIssue1ArmoredVerifierExpectsOriginalArmor", + "label": "detached-signature", + "sha256": "9fd71b7a8eafd6a86a2cac139ff2c08786729af1359adcd0dae936a74407a651" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "calendar-public-key", + "sha256": "6286da991af63a3ef802a02a09c5a31e7837b50333c8b3d616c63e36ab645b3a" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "encrypted-card", + "sha256": "be1d9e019d86e4be2a1193280b66e1b1180a8981905dde1715605a2c06eed167" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "tampered-tail", + "sha256": "da3988436156782f6e468a3cb8d3961c586df16906f37bf6b81bbf56261259a4" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "truncated", + "sha256": "6fe73bc793985a0b666b696c952cb1bca3557fff7ac173c11f7092e81152863e" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "missing-trailer", + "sha256": "6bc7ee7b9caf5cc6efd73eca74ad47cc3e5fa9e028f3302f66357ac7ecafbed5" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "encrypted-card", + "sha256": "3bbfc12e184ea46837ce5c2fd23c721189dcde07b6c36cafd07945934ae7b93d" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "tampered-tail", + "sha256": "b865d006a5695eec759bd39aa85a8e7dcd47e9461c047aa33a91845d57367fc6" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "truncated", + "sha256": "04fd23155bfb2d4810825a7f88559d6169e20890c3f440f9b73285e9327e6302" + }, + { + "test": "TestIssue1ReadEventCardRejectsCiphertextBeforeMerge", + "label": "missing-trailer", + "sha256": "1006c0735335882e6c771ea6b4a3988ca8c14853c9143e1a03dbc8fe1b1c3062" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "author-public-key", + "sha256": "8714f715c77c0bef7ee823b33a8b06fa2f60ed0428174636a29d95fa1dc99eac" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "other-public-key", + "sha256": "438cf5c18859fba1cd4872f0b5486d5b0a502be8532000a6462407a50d46df39" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "author-public-key", + "sha256": "ff2feaf1109cd06cc42e96ec0eaececbef3e8e5d6f51a16cf836c5eb999e79d0" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "calendar-public-key", + "sha256": "01ff9dc760bf287d426245dfd563c3d316ef93f13b438ee748948e282423b011" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "detached-signature", + "sha256": "9d4abf5e02e0018f6eae10cadbffc210882a4bb5bff5e5f91616de2e524ed154" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "detached-signature", + "sha256": "a7747d3959244177d49c66457958ba5a6119dd5ae1b90c5148ecafb1308e7c01" + }, + { + "test": "TestIssue1EventSignatureOutcomes", + "label": "detached-signature", + "sha256": "0f60c9469c694c2fec8885fa09584ea3473ee39f0a0c9aae0ccbca9b6ef028df" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/valid", + "label": "encrypted-card", + "sha256": "72e1851d0bdb53dfc5bfa83d7e83aeb114e1b4c839fcfbd8213cd9b89a2a000c" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/modified-content", + "label": "encrypted-card", + "sha256": "ab36910def66f9b6f0c6f4dc774e706eff64cc01e9a6667662e85efa76b80dd5" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/wrong-signer", + "label": "encrypted-card", + "sha256": "4e09309a18920052418311e20eede3db46035b0eefe0d022c8612eb614d647a6" + }, + { + "test": "TestIssue1EventSignatureOutcomes/encrypted/malformed", + "label": "encrypted-card", + "sha256": "fc6e3e7a1b2f3b3feb01e641aee4e033437b09718537b6374a08a203db54b5c6" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "other-public-key", + "sha256": "5b9897599e6906e9a72aa41ab36cd65a59c6798d3fde47d2ab3a25aefcbce8b9" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "detached-signature", + "sha256": "e63d683c3796bfadfb8d8263dfda243db979741c1c72183ddfb7e5dddaa07c84" + }, + { + "test": "TestIssue1BaselineServesUnverifiedSignatures", + "label": "detached-signature", + "sha256": "9fdd5c01b5aff92ba0e4f15bc860b5bfb4b71de08af6103ae6edbe022e231736" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF", + "label": "calendar-public-key", + "sha256": "5ac8c99b5a06f517c03a632f364dd7abbd35ebf013f58736d6115f3ec85a5be8" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF", + "label": "encrypted-card", + "sha256": "43dedade7f9bddf7bfbee8b6be65cdccd6bc0d11706eee0ea8b54244e9d366d0" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/mdc/tampered-tail", + "label": "tampered-tail", + "sha256": "dbc3c6fb2c7f75142d803edd0a5ea8059679c8a3d9553e82c71a661070305413" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/mdc/truncated", + "label": "truncated", + "sha256": "7cbd439c36e88c8a3e10be3328175cc72784d58924944c97b690a250f9e34924" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/mdc/missing-trailer", + "label": "missing-trailer", + "sha256": "d83383269d459ef819feeaa788e759dfb2c228e3ebc854ef2717e3417ff4e55e" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF", + "label": "encrypted-card", + "sha256": "333466cdd9f3cea47db209d144e066e4c016cad8a6ba669a9998f0c92a69c169" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/tampered-tail", + "label": "tampered-tail", + "sha256": "246a1b2f816f2ae41bb263185fc6d8c5e255e2236d35aea0eacdc043e05c3415" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/truncated", + "label": "truncated", + "sha256": "09e2245827fe4ed996163252d9e2d31207598fe505e806e9754f499d34e67b8e" + }, + { + "test": "TestIssue1CiphertextIntegrityAtEOF/aead-seipdv2/missing-trailer", + "label": "missing-trailer", + "sha256": "22be59e7c3440e230f3b8dd1b6dac0a53b624f535d8e9085d94404b833eb6d19" + }, + { + "test": "TestIssue1DetachedReaderChecksOnlyAtEOF", + "label": "author-public-key", + "sha256": "74de565f5afe74f222686650f8c0923009b5fa45be41fb569542b82733b8d50a" + }, + { + "test": "TestIssue1DetachedReaderChecksOnlyAtEOF", + "label": "detached-signature", + "sha256": "140b66b39ec08fa66a556b094dfa758dc3e7d963eed6485556ce4059a27c4a7d" + }, + { + "test": "TestIssue1BaselineMixedCardTrustAndDowngrades", + "label": "author-public-key", + "sha256": "b95c680ee86baea8e6679cafad0dee2895a2d55220c2d06775f357cc7026c4f4" + }, + { + "test": "TestIssue1BaselineMixedCardTrustAndDowngrades", + "label": "detached-signature", + "sha256": "8e1a718e7b9e4fffc0f8983d5a0b40ad9055563f17611fad19a00cb3694696e7" + }, + { + "test": "TestIssue1InlineSignedReaderIntegrity", + "label": "author-public-key", + "sha256": "b1cfe2ed43477358985092f3fa210949ca318c6cbcdaa7861987449bd4cace28" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer", + "label": "author-public-key", + "sha256": "9f684e883321dd06ab5229a7053986dc0e5c671ccf25284e9b5bb7615dbe8051" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer/transport-failure", + "label": "detached-signature", + "sha256": "7b978d68208dfdff817633317388da0960eef74e6e6d801942f85c91ded3d07e" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer/empty-successful-lookup", + "label": "detached-signature", + "sha256": "fe20b83f48842bb998a931b92b8c5ff8cfbf82bce25dc5cbef86e292bf4213b5" + }, + { + "test": "TestIssue1KeyLookupFailureIsNotUnknownIssuer/malformed-key", + "label": "detached-signature", + "sha256": "0a706c40dc247add019d72c7260a46cfb88b6c78f2228afab689a68e4e963b85" + }, + { + "test": "TestIssue1InlineSignedReaderIntegrity", + "label": "calendar-public-key", + "sha256": "bf0e048a1030914e92b4c2739d081df8d1f039f672fb1a25450996b7f089d975" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/key", + "label": "historical-public-key", + "sha256": "e0bb2c06adea4e832efd0bfdd4b72d77fe1dbfc1a424eb63ec69f1048041f448" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/key", + "label": "detached-signature", + "sha256": "7dbd29b9a66a94a28914fd9c1ac90529a3291a8f9eb3fc87366e17fcecc28e29" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/signature", + "label": "historical-public-key", + "sha256": "362b9803b86de9c9281c38e56a3f86a1fe2e1d65923469a28f2bbff233e673ec" + }, + { + "test": "TestIssue1HistoricalExpiryClockSemantics/signature", + "label": "detached-signature", + "sha256": "91226b0fcddd68fb763d2af85468763d66b9bbcd975d8aa0ba668a5a7003fd8c" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "author-public-key", + "sha256": "47fe7bf1806f4253de9ff917fa13eda2d475fdde27dcb99f9192460a659fb54b" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "calendar-public-key", + "sha256": "69eb08cd297bac0c49629aa28fb7ca10bd27e4c7daf16cad10a799f0441fb1c6" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "encrypted-card", + "sha256": "6839aa9c7d5b29c773746d0a2f40b6095cf12e0798cbb4201fa81c13a8394f54" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "detached-signature", + "sha256": "ed500838eb04820ec83b775217ecb57cdb0fd0f56475309457a2fee31d314e8f" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "encrypted-card", + "sha256": "e148d42913a91b1083d2bebec5080231591f6ff8b11e39711393763b6ec1782e" + }, + { + "test": "TestIssue1CardTypeBaselineHasNoUnsignedAssurance", + "label": "detached-signature", + "sha256": "06db334a5845fc784cd3a562610194660b44174d341cf4d3ffd1ed93b36fa058" + } + ] + } + ], + "vet": { + "status": "failed", + "all_four_logs_identical": true, + "classification": "Pre-existing unkeyed struct literals at exports/messages.go:30; reproduced in untouched baseline on both toolchains." + }, + "scanner": { + "name": "govulncheck", + "version": "v1.8.0", + "go_version": "go1.27.1", + "mode": "source", + "level": "symbol", + "db": "https://vuln.go.dev", + "db_last_modified": "2026-09-10T14:48:42Z", + "exit_code": 0, + "log_sha256": "d43d7ad43f36e668d3d764ee821243511ddb8ab656702877cf513c34e7e62cb3", + "finding_records": 109, + "advisories": 23, + "module_records": 23, + "package_records": 12, + "symbol_records": 74, + "all_symbol_records_advisory": "GO-2026-4550", + "affected_circl_version": "v1.6.1", + "first_fixed_circl_version": "v1.6.3", + "go_advisory_review_status": "UNREVIEWED", + "p384_trace_records": 0, + "interpretation": "Broad unreviewed module-level advisory generates symbol findings for CIRCL functions. The actual ecc/p384 CombinedMult defect is not demonstrated reachable by this scan. The similarly named reported CombinedMult is ecc/goldilocks, a different package. Remaining 22 advisory IDs have module-only findings, not a proof of safety." + }, + "diagnosis": { + "mdc_aead_eof": "verified for generated fixtures", + "corruption_before_ics_merge": "verified for generated fixtures", + "passphrase_signature_bypass": "demonstrated baseline gap", + "warning_only_event_signature_acceptance": "demonstrated baseline gap", + "unknown_type_downgrade_and_mixed_card_overwrite": "demonstrated baseline gaps", + "historical_clock_error_distinctions": "verified for pinned library fixtures; not Proton policy acceptance" + }, + "live_proton": "not run", + "thunderbird": "not run", + "http_status_and_cache_recovery": "not run", + "security_acceptance": "not run", + "raw_logs": "Original log hashes are commitments to retained local evidence; raw logs are not included in this public-safe summary.", + "sanitization": "Allowlisted test names, counts, hashes, normalized recorded commands and reviewed classifications; no raw Output strings, host paths, account data or key material." +} diff --git a/internal/issue1test/fixtures.go b/internal/issue1test/fixtures.go new file mode 100644 index 0000000..469b0f5 --- /dev/null +++ b/internal/issue1test/fixtures.go @@ -0,0 +1,149 @@ +// Package issue1test constructs synthetic fixtures for Issue 1 tests only. +// No production package imports it. Never use these fixed test session keys or +// test clocks for application encryption or historical verification policy. +package issue1test + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +var Epoch = time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + +const ICS = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Issue1 Synthetic//EN\r\nBEGIN:VEVENT\r\nUID:issue1@example.invalid\r\nDTSTAMP:20250101T000000Z\r\nDTSTART:20250102T120000Z\r\nDTEND:20250102T123000Z\r\nSUMMARY:Synthetic baseline\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" + +func Config(at time.Time) *packet.Config { + return &packet.Config{Time: func() time.Time { return at }, RSABits: 2048} +} + +func Must(t testing.TB, err error) { + t.Helper() + if err != nil { + t.Fatalf("fixture construction failed: %v", err) + } +} + +// Record identifies this run's concrete bytes without logging key material or +// plaintext. CSPRNG-generated keys mean hashes may differ between runs; test +// source, configuration, dependencies and semantic outcomes are reproducible. +func Record(t testing.TB, label string, parts ...[]byte) { + t.Helper() + h := sha256.New() + for _, p := range parts { + fmt.Fprintf(h, "%d:", len(p)) + h.Write(p) + } + t.Logf("fixture_sha256 label=%s hash=%x", label, h.Sum(nil)) +} + +func Key(t testing.TB, name string, config *packet.Config) *openpgp.Entity { + t.Helper() + e, err := openpgp.NewEntity(name, "synthetic Issue 1 fixture", name+"@example.invalid", config) + Must(t, err) + var public bytes.Buffer + Must(t, e.Serialize(&public)) + Record(t, name+"-public-key", public.Bytes()) + return e +} + +func Sign(t testing.TB, signer *openpgp.Entity, body string, config *packet.Config) string { + t.Helper() + var out bytes.Buffer + Must(t, openpgp.ArmoredDetachSign(&out, signer, bytes.NewBufferString(body), config)) + Record(t, "detached-signature", []byte(body), out.Bytes()) + return out.String() +} + +func Armored(t testing.TB, kind string, raw []byte) string { + t.Helper() + var out bytes.Buffer + w, err := armor.Encode(&out, kind, nil) + Must(t, err) + _, err = w.Write(raw) + Must(t, err) + Must(t, w.Close()) + return out.String() +} + +// Encrypt emits separate session-key and data packets, like the card API. +// AEAD uses the library's SEIPDv2 format; this is not Proton format-parity proof. +func Encrypt(t testing.TB, recipient *openpgp.Entity, body string, aead bool) (string, string) { + t.Helper() + config := Config(Epoch.Add(time.Minute)) + config.AEADConfig = &packet.AEADConfig{DefaultMode: packet.AEADModeGCM, ChunkSize: 64} + key := bytes.Repeat([]byte{0x42}, packet.CipherAES128.KeySize()) // TEST ONLY + var keyPacket, data bytes.Buffer + Must(t, packet.SerializeEncryptedKeyAEAD(&keyPacket, recipient.Subkeys[0].PublicKey, + packet.CipherAES128, aead, key, config)) + w, err := packet.SerializeSymmetricallyEncrypted(&data, packet.CipherAES128, aead, + packet.CipherSuite{Cipher: packet.CipherAES128, Mode: packet.AEADModeGCM}, key, config) + Must(t, err) + literal, err := packet.SerializeLiteral(w, true, "", uint32(Epoch.Unix())) + Must(t, err) + _, err = io.WriteString(literal, body) + Must(t, err) + Must(t, literal.Close()) // also closes the enclosing encrypted packet + Record(t, "encrypted-card", keyPacket.Bytes(), data.Bytes()) + return base64.StdEncoding.EncodeToString(keyPacket.Bytes()), base64.StdEncoding.EncodeToString(data.Bytes()) +} + +func Decode(t testing.TB, value string) []byte { + t.Helper() + data, err := base64.StdEncoding.DecodeString(value) + Must(t, err) + return data +} + +func Message(t testing.TB, keyPacket, data string, keys openpgp.KeyRing) (*openpgp.MessageDetails, error) { + t.Helper() + raw := append(Decode(t, keyPacket), Decode(t, data)...) + return openpgp.ReadMessage(bytes.NewReader(raw), keys, nil, Config(Epoch.Add(time.Minute))) +} + +func Mutate(t testing.TB, data, mutation string) string { + t.Helper() + raw := Decode(t, data) + if len(raw) < 24 { + t.Fatal("fixture ciphertext unexpectedly short") + } + switch mutation { + case "tampered-tail": + raw[len(raw)-1] ^= 1 + case "truncated": + raw = raw[:len(raw)-1] + case "missing-trailer": + raw = raw[:len(raw)-22] + default: + t.Fatalf("unknown fixture mutation %q", mutation) + } + Record(t, mutation, raw) + return base64.StdEncoding.EncodeToString(raw) +} + +func ProtectedPrivateKey(t testing.TB, key *openpgp.Entity, passphrase string) string { + t.Helper() + // Clone before encryption so subtests never mutate a shared entity. + var private bytes.Buffer + Must(t, key.SerializePrivateWithoutSigning(&private, Config(Epoch))) + cloned, err := openpgp.ReadKeyRing(bytes.NewReader(private.Bytes())) + Must(t, err) + Must(t, cloned[0].EncryptPrivateKeys([]byte(passphrase), Config(Epoch))) + private.Reset() + Must(t, cloned[0].SerializePrivateWithoutSigning(&private, Config(Epoch))) + return Armored(t, openpgp.PrivateKeyType, private.Bytes()) +} + +func EncryptedPassphrase(t testing.TB, recipient *openpgp.Entity, passphrase string) string { + t.Helper() + key, data := Encrypt(t, recipient, passphrase, false) + return Armored(t, "PGP MESSAGE", append(Decode(t, key), Decode(t, data)...)) +} diff --git a/protonmail/issue1_crypto_test.go b/protonmail/issue1_crypto_test.go new file mode 100644 index 0000000..59c8254 --- /dev/null +++ b/protonmail/issue1_crypto_test.go @@ -0,0 +1,345 @@ +package protonmail + +import ( + "bytes" + "errors" + "io" + "strings" + "testing" + "time" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" + f "github.com/mathewcsims/protoxide/internal/issue1test" +) + +// These are characterization tests for the pinned baseline, not an assertion +// that acceptance of unverified signatures is safe or should be preserved. +func TestIssue1PassphraseBaselineIgnoresSignature(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + other := f.Key(t, "other", f.Config(f.Epoch)) + calendar := f.Key(t, "calendar", f.Config(f.Epoch)) + const original = "synthetic-passphrase-original" + valid := f.Sign(t, author, original, f.Config(f.Epoch)) + for _, tc := range []struct { + name, passphrase, signature string + oracleValid bool + }{ + {"valid", original, valid, true}, + {"modified-passphrase-with-old-signature", "synthetic-passphrase-modified", valid, false}, + {"wrong-signer", original, f.Sign(t, other, original, f.Config(f.Epoch)), false}, + {"malformed-armor", original, "not an armored signature", false}, + {"malformed-packet", original, f.Armored(t, openpgp.SignatureType, []byte{1, 2, 3}), false}, + {"missing-signature", original, "", false}, + } { + t.Run(tc.name, func(t *testing.T) { + _, oracleErr := openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{author}, + strings.NewReader(tc.passphrase), strings.NewReader(tc.signature), f.Config(f.Epoch)) + if (oracleErr == nil) != tc.oracleValid { + t.Fatalf("fixture oracle valid=%v, want %v", oracleErr == nil, tc.oracleValid) + } + // Re-encrypt the synthetic calendar key with the candidate passphrase. + // Merely changing a password while retaining the old encrypted key + // would test private-key decryption, not the missing signature check. + bootstrap := CalendarBootstrap{ + Keys: []CalendarKey{{PrivateKey: f.ProtectedPrivateKey(t, calendar, tc.passphrase)}}, + Members: []CalendarMemberView{{ID: "member", Email: "author@example.invalid"}}, + Passphrase: CalendarPassphrase{MemberPassphrases: []CalendarMemberPassphrase{{ + MemberID: "member", Passphrase: f.EncryptedPassphrase(t, author, tc.passphrase), Signature: tc.signature, + }}}, + } + keys, err := bootstrap.DecryptKeyring(openpgp.EntityList{author}) + f.Must(t, err) + if len(keys.DecryptionKeys()) == 0 { + t.Fatal("baseline did not unlock the synthetic calendar key") + } + if !tc.oracleValid { + t.Log("KNOWN_GAP: baseline unlocks calendar key despite rejected passphrase signature") + } + }) + } +} + +func TestIssue1ArmoredVerifierExpectsOriginalArmor(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + sig := f.Sign(t, author, f.ICS, f.Config(f.Epoch)) + keys := openpgp.EntityList{author} + _, err := openpgp.CheckArmoredDetachedSignature(keys, strings.NewReader(f.ICS), strings.NewReader(sig), f.Config(f.Epoch)) + f.Must(t, err) + decoded, err := armor.Decode(strings.NewReader(sig)) + f.Must(t, err) + binarySignature, err := io.ReadAll(decoded.Body) + f.Must(t, err) + _, err = openpgp.CheckDetachedSignature(keys, strings.NewReader(f.ICS), bytes.NewReader(binarySignature), f.Config(f.Epoch)) + f.Must(t, err) + _, err = openpgp.CheckArmoredDetachedSignature(keys, strings.NewReader(f.ICS), bytes.NewReader(binarySignature), f.Config(f.Epoch)) + if err == nil { + t.Fatal("decoded binary unexpectedly accepted as armored signature") + } +} + +func TestIssue1EventSignatureOutcomes(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + other := f.Key(t, "other", f.Config(f.Epoch)) + calendar := f.Key(t, "calendar", f.Config(f.Epoch)) + valid := f.Sign(t, author, f.ICS, f.Config(f.Epoch)) + for _, encrypted := range []bool{false, true} { + for _, tc := range []struct { + name, body, signature string + wantUnknown, wantInvalid bool + }{ + {"valid", f.ICS, valid, false, false}, + {"modified-content", strings.Replace(f.ICS, "Synthetic baseline", "Changed baseline", 1), valid, false, true}, + {"wrong-signer", f.ICS, f.Sign(t, other, f.ICS, f.Config(f.Epoch)), true, false}, + {"malformed", f.ICS, "malformed signature", false, true}, + } { + prefix := "clear/" + if encrypted { + prefix = "encrypted/" + } + t.Run(prefix+tc.name, func(t *testing.T) { + card := CalendarEventCard{Type: CalendarEventCardSigned, Data: tc.body, Signature: tc.signature} + key := "" + if encrypted { + card.Type = CalendarEventCardEncryptedAndSigned + key, card.Data = f.Encrypt(t, calendar, tc.body, false) + } + md, err := card.Read(openpgp.EntityList{author}, openpgp.EntityList{calendar}, key) + f.Must(t, err) + body, err := io.ReadAll(md.UnverifiedBody) + f.Must(t, err) + if string(body) != tc.body { + t.Fatal("plaintext differs from fixture") + } + if !md.IsSigned { + t.Fatal("signed card not classified signed after EOF") + } + if tc.wantUnknown && !errors.Is(md.SignatureError, pgperrors.ErrUnknownIssuer) { + t.Fatalf("want unknown issuer, got %v", md.SignatureError) + } + if tc.wantInvalid && (md.SignatureError == nil || errors.Is(md.SignatureError, pgperrors.ErrUnknownIssuer)) { + t.Fatalf("want non-unknown verification failure, got %v", md.SignatureError) + } + if !tc.wantUnknown && !tc.wantInvalid { + f.Must(t, md.SignatureError) + } + }) + } + } +} + +func TestIssue1CiphertextIntegrityAtEOF(t *testing.T) { + calendar := f.Key(t, "calendar", f.Config(f.Epoch)) + for _, aead := range []bool{false, true} { + format := "mdc" + if aead { + format = "aead-seipdv2" + } + key, data := f.Encrypt(t, calendar, f.ICS, aead) + for _, mutation := range []string{"valid", "tampered-tail", "truncated", "missing-trailer"} { + t.Run(format+"/"+mutation, func(t *testing.T) { + candidate := data + if mutation != "valid" { + candidate = f.Mutate(t, data, mutation) + } + // The direct-library oracle is independent of CalendarEventCard.Read. + oracle, oracleErr := f.Message(t, key, candidate, openpgp.EntityList{calendar}) + oracleStage := "open" + if oracleErr == nil { + oracleStage = "body" + _, oracleErr = io.ReadAll(oracle.UnverifiedBody) + } + card := CalendarEventCard{Type: CalendarEventCardEncrypted, Data: candidate} + md, err := card.Read(nil, openpgp.EntityList{calendar}, key) + stage := "open" + var body []byte + if err == nil { + stage = "body" + body, err = io.ReadAll(md.UnverifiedBody) + } + if mutation == "valid" { + f.Must(t, oracleErr) + f.Must(t, err) + if string(body) != f.ICS { + t.Fatal("valid encrypted plaintext differs") + } + } else if oracleErr == nil || err == nil { + t.Fatalf("corrupt fixture accepted: oracle=%v application=%v", oracleErr, err) + } + if mutation == "tampered-tail" { + want := pgperrors.ErrMDCHashMismatch + if aead { + want = pgperrors.ErrAEADTagVerification + } + if oracleStage != "body" || stage != "body" || !errors.Is(oracleErr, want) || !errors.Is(err, want) { + t.Fatalf("fixture must reach body integrity check: oracle=%s/%v application=%s/%v", oracleStage, oracleErr, stage, err) + } + } + // Truncation may fail framing before reaching the integrity trailer. + // Do not label every failure ErrMDCHashMismatch or an AEAD tag failure. + t.Logf("body_read_outcome oracle_stage=%s oracle=%T application_stage=%s application=%T", oracleStage, oracleErr, stage, err) + }) + } + } +} + +type issue1ShortReader struct{ r io.Reader } + +func (r issue1ShortReader) Read(p []byte) (int, error) { + if len(p) > 1 { + p = p[:1] + } + return r.r.Read(p) +} + +func TestIssue1DetachedReaderChecksOnlyAtEOF(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + md := &openpgp.MessageDetails{} + r := &detachedSignatureReader{md: md, signature: strings.NewReader(f.Sign(t, author, f.ICS, f.Config(f.Epoch))), keyring: openpgp.EntityList{author}} + r.body = io.TeeReader(issue1ShortReader{strings.NewReader(f.ICS)}, &r.signed) + first := make([]byte, 1) + _, err := r.Read(first) + f.Must(t, err) + if md.IsSigned || r.eof { + t.Fatal("verification claimed before EOF") + } + rest, err := io.ReadAll(r) + f.Must(t, err) + if string(append(first, rest...)) != f.ICS { + t.Fatal("short-read data lost") + } + if !md.IsSigned || !r.eof { + t.Fatal("EOF did not finalize detached verification") + } + f.Must(t, md.SignatureError) + _, err = r.Read(first) + if err != io.EOF { + t.Fatalf("repeat EOF: %v", err) + } + f.Must(t, md.SignatureError) +} + +func TestIssue1InlineSignedReaderIntegrity(t *testing.T) { + // Exercise go-crypto's signatureCheckReader separately from Protoxide's + // detached-signature wrapper; this is a library-format fixture only. + author := f.Key(t, "author", f.Config(f.Epoch)) + calendar := f.Key(t, "calendar", f.Config(f.Epoch)) + var encrypted bytes.Buffer + w, err := openpgp.Encrypt(&encrypted, []*openpgp.Entity{calendar}, author, nil, f.Config(f.Epoch.Add(time.Minute))) + f.Must(t, err) + _, err = io.WriteString(w, f.ICS) + f.Must(t, err) + f.Must(t, w.Close()) + for _, corrupt := range []bool{false, true} { + raw := append([]byte(nil), encrypted.Bytes()...) + if corrupt { + raw[len(raw)-1] ^= 1 + } + md, err := openpgp.ReadMessage(bytes.NewReader(raw), openpgp.EntityList{calendar, author}, nil, f.Config(f.Epoch.Add(time.Minute))) + if err == nil { + _, err = io.ReadAll(md.UnverifiedBody) + } + if corrupt { + if err == nil { + t.Fatal("inline-signed ciphertext integrity failure ignored") + } + } else { + f.Must(t, err) + f.Must(t, md.SignatureError) + if !md.IsSigned || md.SignedBy == nil { + t.Fatal("inline signature not verified") + } + } + } +} + +func TestIssue1HistoricalExpiryClockSemantics(t *testing.T) { + for _, lifetime := range []string{"key", "signature"} { + t.Run(lifetime, func(t *testing.T) { + keyConfig := f.Config(f.Epoch) + if lifetime == "key" { + keyConfig.KeyLifetimeSecs = 120 + } + author := f.Key(t, "historical", keyConfig) + signedAt := f.Epoch.Add(60 * time.Second) + signConfig := f.Config(signedAt) + if lifetime == "signature" { + signConfig.SigLifetimeSecs = 60 + } + signature := f.Sign(t, author, f.ICS, signConfig) + for _, offset := range []int{60, 119, 120, 121} { + at := f.Epoch.Add(time.Duration(offset) * time.Second) + _, err := openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{author}, strings.NewReader(f.ICS), strings.NewReader(signature), f.Config(at)) + if offset <= 120 { + f.Must(t, err) // v1.3.0 treats the expiry second itself as valid. + } else { + want := pgperrors.ErrSignatureExpired + if lifetime == "key" { + if !errors.Is(err, pgperrors.ErrKeyExpired) { + t.Fatalf("key expiry: %v", err) + } + } else if !errors.Is(err, want) { + t.Fatalf("signature expiry: %v", err) + } + } + } + // Invalid mathematics must not be reclassified as an expiry allowance. + _, err := openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{author}, strings.NewReader(f.ICS+"changed"), strings.NewReader(signature), f.Config(f.Epoch.Add(121*time.Second))) + var invalid pgperrors.SignatureError + if !errors.As(err, &invalid) { + t.Fatalf("modified expired signature must fail cryptography: %v", err) + } + f.Must(t, author.RevokeKey(packet.KeyCompromised, "synthetic test", f.Config(f.Epoch.Add(90*time.Second)))) + _, err = openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{author}, strings.NewReader(f.ICS), strings.NewReader(signature), f.Config(f.Epoch.Add(121*time.Second))) + if !errors.Is(err, pgperrors.ErrKeyRevoked) { + t.Fatalf("revocation must not become an expiry allowance: %v", err) + } + _, err = openpgp.CheckArmoredDetachedSignature(openpgp.EntityList{}, strings.NewReader(f.ICS), strings.NewReader(signature), f.Config(f.Epoch.Add(121*time.Second))) + if !errors.Is(err, pgperrors.ErrUnknownIssuer) { + t.Fatalf("missing issuer: %v", err) + } + }) + } +} + +func TestIssue1ClockDefaultsAndSecondPrecision(t *testing.T) { + // Verify the pinned library clock contract without changing the process clock + // or treating a signature's claimed timestamp as trusted production time. + at := f.Epoch.Add(120*time.Second + 999*time.Millisecond) + if !f.Config(at).Now().Equal(f.Epoch.Add(120 * time.Second)) { + t.Fatal("configured clock no longer uses second precision; review boundary fixtures") + } + before := time.Now().Truncate(time.Second) + now := (*packet.Config)(nil).Now() + after := time.Now().Truncate(time.Second) + if now.Before(before) || now.After(after) { + t.Fatal("nil config did not use the current process wall clock") + } +} + +func TestIssue1CardTypeBaselineHasNoUnsignedAssurance(t *testing.T) { + author := f.Key(t, "author", f.Config(f.Epoch)) + calendar := f.Key(t, "calendar", f.Config(f.Epoch)) + for _, kind := range []CalendarEventCardType{CalendarEventCardClear, CalendarEventCardEncrypted, CalendarEventCardSigned, CalendarEventCardEncryptedAndSigned, CalendarEventCardType(99)} { + card := CalendarEventCard{Type: kind, Data: f.ICS, Signature: "invalid but ignored by unsigned types"} + key := "" + if kind.Encrypted() { + key, card.Data = f.Encrypt(t, calendar, f.ICS, false) + } + if kind.Signed() { + card.Signature = f.Sign(t, author, f.ICS, f.Config(f.Epoch)) + } + md, err := card.Read(openpgp.EntityList{author}, openpgp.EntityList{calendar}, key) + f.Must(t, err) + _, err = io.ReadAll(md.UnverifiedBody) + f.Must(t, err) + if md.IsSigned != kind.Signed() { + t.Fatalf("type %d has unexpected signing state", kind) + } + if !kind.Signed() { + t.Logf("KNOWN_GAP: type %d accepted with no author-signature assurance", kind) + } + } +}