From 826d804bd9ea034da39b18f43d3c21e9705a969c Mon Sep 17 00:00:00 2001 From: leetcrypt Date: Wed, 15 Jul 2026 11:00:38 -0700 Subject: [PATCH] sink: sealer + framing test coverage 30% -> 85% (M1) The backup gate is only as trustworthy as the sealers; internal/sink was the safety-critical coverage gap blocking all real rotation work. sealer_test.go: round-trip per sealer (age/hmac/openssl) across ascii/binary/ unicode/empty/large payloads, asserting plaintext never leaks into sealed output; tamper fails closed (age & hmac error with zero bytes emitted on a flipped ciphertext or MAC byte); openssl's unauthenticated failure mode pinned as an executable contract (no exact-plaintext recovery only); wrong-passphrase clean-error; short-bundle, bad-binary, and empty-passphrase error paths. framing_test.go: bundle framing (zero-record, clean-EOF, truncated mid-path/ len/secret, oversized path); gopass wrapper Insert/InsertAt/Show/Exists/ Available; secret-never-in-argv leak assertion (Hard Rule #3); gopass-failure paths surface errors instead of silently short backups; end-to-end export -> seal -> open -> count -> restore proving completeness + authenticity together. Co-Authored-By: Claude Opus 4.6 --- internal/sink/framing_test.go | 361 ++++++++++++++++++++++++++++++++++ internal/sink/sealer_test.go | 254 ++++++++++++++++++++++++ 2 files changed, 615 insertions(+) create mode 100644 internal/sink/framing_test.go create mode 100644 internal/sink/sealer_test.go diff --git a/internal/sink/framing_test.go b/internal/sink/framing_test.go new file mode 100644 index 0000000..2b91488 --- /dev/null +++ b/internal/sink/framing_test.go @@ -0,0 +1,361 @@ +package sink + +import ( + "bytes" + "context" + "encoding/binary" + "os" + "path/filepath" + "strings" + "testing" + + "incredigo/internal/discover" + "incredigo/internal/vault" + + "github.com/awnumar/memguard" +) + +// frameRecord builds a single well-formed bundle record: path then one secret chunk +// then the secret end marker. No trailing stream sentinel (callers add it). +func frameRecord(path, secret string) []byte { + var b bytes.Buffer + binary.Write(&b, binary.BigEndian, uint16(len(path))) + b.WriteString(path) + binary.Write(&b, binary.BigEndian, uint32(len(secret))) + b.WriteString(secret) + binary.Write(&b, binary.BigEndian, uint32(0)) // secret end + return b.Bytes() +} + +func streamSentinel() []byte { + var b bytes.Buffer + binary.Write(&b, binary.BigEndian, uint16(0)) + return b.Bytes() +} + +// TestCountBundleRecordsZeroRecord proves an empty bundle (just the sentinel) counts +// as zero records without error. +func TestCountBundleRecordsZeroRecord(t *testing.T) { + n, err := CountBundleRecords(bytes.NewReader(streamSentinel())) + if err != nil { + t.Fatalf("count: %v", err) + } + if n != 0 { + t.Fatalf("zero-record bundle counted %d", n) + } +} + +// TestCountBundleRecordsCleanEOF proves a stream that ends after complete records +// (no explicit sentinel) still counts correctly — ImportFrom tolerates this, so the +// counter must agree. +func TestCountBundleRecordsCleanEOF(t *testing.T) { + var b bytes.Buffer + b.Write(frameRecord("imported/aws/default", "AKIA-secret")) + b.Write(frameRecord("imported/env/api", "token-value")) + // no sentinel — clean EOF + n, err := CountBundleRecords(bytes.NewReader(b.Bytes())) + if err != nil { + t.Fatalf("count: %v", err) + } + if n != 2 { + t.Fatalf("counted %d records, want 2", n) + } +} + +// TestCountBundleRecordsTruncated proves a stream truncated mid-record is reported +// as an error, not silently under-counted — the backup gate must not accept a short +// backup as complete. +func TestCountBundleRecordsTruncated(t *testing.T) { + full := frameRecord("imported/aws/default", "AKIA-secret-value") + cases := map[string]int{ + "mid-path": 3, // inside the path bytes + "mid-chunk-len": 2 + 20 + 2, // partway through the chunk length prefix + "mid-secret": 2 + 20 + 4 + 3, // inside the secret chunk body + } + for name, cut := range cases { + t.Run(name, func(t *testing.T) { + if cut >= len(full) { + t.Skipf("cut %d beyond record length %d", cut, len(full)) + } + _, err := CountBundleRecords(bytes.NewReader(full[:cut])) + if err == nil { + t.Fatal("truncated bundle counted without error") + } + }) + } +} + +// TestWritePathOversized proves a path longer than the uint16 frame field is +// rejected rather than silently wrapping to a short length (which would corrupt the +// stream and hide entries from the backup). +func TestWritePathOversized(t *testing.T) { + var w bytes.Buffer + long := strings.Repeat("a", 0x10000) // 65536 > 0xffff + if err := writePath(&w, long); err == nil { + t.Fatal("oversized path accepted") + } + if w.Len() != 0 { + t.Fatalf("oversized path wrote %d bytes before failing", w.Len()) + } +} + +// TestExportImportSecretNeverInArgv proves Hard Rule #3 at the wrapper boundary: a +// secret streamed through Insert/Import never appears in the child process argv. The +// fake gopass records its full argv; we assert the secret is absent from it. +func TestExportImportSecretNeverInArgv(t *testing.T) { + secret := "s3cr3t-must-not-appear-in-argv" + argvLog := filepath.Join(t.TempDir(), "argv.log") + bin := writeArgvLoggingGopass(t, argvLog) + gp := &Gopass{Bin: bin} + ctx := context.Background() + + store := t.TempDir() + t.Setenv("FAKE_GOPASS_STORE", store) + + // Build a one-record bundle and import it (spawns `gopass insert`). + var bundle bytes.Buffer + bundle.Write(frameRecord("imported/env/api", secret)) + bundle.Write(streamSentinel()) + if _, err := gp.ImportFrom(ctx, &bundle, true); err != nil { + t.Fatalf("import: %v", err) + } + + logged, err := os.ReadFile(argvLog) + if err != nil { + t.Fatalf("read argv log: %v", err) + } + if strings.Contains(string(logged), secret) { + t.Fatalf("secret leaked into gopass argv: %s", logged) + } + // Sanity: the secret did land in the store (via stdin, not argv). + got, err := os.ReadFile(filepath.Join(store, "imported/env/api")) + if err != nil { + t.Fatalf("stored entry missing: %v", err) + } + if string(got) != secret { + t.Fatalf("stored %q, want %q", got, secret) + } +} + +// TestGopassShowAndExists exercises the Show (into vault) and Exists wrapper paths +// against the fake binary. +func TestGopassShowAndExists(t *testing.T) { + bin := writeFakeGopass(t) + gp := &Gopass{Bin: bin} + ctx := context.Background() + + store := t.TempDir() + seed(t, store, map[string]string{"imported/env/api": "token-xyz\n"}) + t.Setenv("FAKE_GOPASS_STORE", store) + + v := vault.New() + defer v.Purge() + h, err := gp.Show(ctx, v, "imported/env/api") + if err != nil { + t.Fatalf("show: %v", err) + } + buf, err := v.Open(h) + if err != nil { + t.Fatalf("vault open: %v", err) + } + if got := string(buf.Bytes()); got != "token-xyz" { // trailing newline trimmed + t.Fatalf("show returned %q, want %q", got, "token-xyz") + } + + if !gp.Exists(ctx, "imported/env/api") { + t.Error("Exists reported false for a present entry") + } + if gp.Exists(ctx, "imported/nope") { + t.Error("Exists reported true for an absent entry") + } +} + +// TestGopassInsert proves the rotation write-back paths (Insert and InsertAt) stream +// a vaulted secret into gopass, that Insert forgets the handle afterwards, and that +// InsertAt writes to an exact path without forgetting. +func TestGopassInsert(t *testing.T) { + bin := writeFakeGopass(t) + gp := &Gopass{Bin: bin} + ctx := context.Background() + + store := t.TempDir() + t.Setenv("FAKE_GOPASS_STORE", store) + + v := vault.New() + defer v.Purge() + + // Insert derives the path from Source+Identity via StorePath. + c := discover.Credential{Source: "env", Identity: "API_TOKEN", Secret: v.Store([]byte("inserted-secret"))} + if err := gp.Insert(ctx, v, c, true); err != nil { + t.Fatalf("insert: %v", err) + } + want := filepath.Join(store, gp.StorePath(c)) + got, err := os.ReadFile(want) + if err != nil { + t.Fatalf("Insert did not write %s: %v", gp.StorePath(c), err) + } + if string(got) != "inserted-secret" { + t.Fatalf("Insert stored %q, want %q", got, "inserted-secret") + } + // Insert must forget the handle (secret is now in gopass). + if _, err := v.Open(c.Secret); err == nil { + t.Error("Insert did not forget the handle") + } + + // InsertAt writes a fresh handle to an exact path and keeps it usable. + h := v.Store([]byte("rotated-value")) + if err := gp.InsertAt(ctx, v, "imported/env/rotated", h, true); err != nil { + t.Fatalf("insertAt: %v", err) + } + got, err = os.ReadFile(filepath.Join(store, "imported/env/rotated")) + if err != nil { + t.Fatalf("InsertAt did not write: %v", err) + } + if string(got) != "rotated-value" { + t.Fatalf("InsertAt stored %q, want %q", got, "rotated-value") + } + if _, err := v.Open(h); err != nil { + t.Error("InsertAt should not forget the handle (caller may re-read)") + } +} + +// TestGopassAvailable proves Available reflects whether the binary resolves on PATH. +func TestGopassAvailable(t *testing.T) { + if (&Gopass{Bin: writeFakeGopass(t)}).Available() != true { + t.Error("Available false for an existing binary") + } + if (&Gopass{Bin: "definitely-not-a-real-binary-xyz"}).Available() != false { + t.Error("Available true for a missing binary") + } +} + +// TestExportSealOpenPipeline proves the whole backup path the rotation gate depends +// on: export a store → seal it (age) → open it → the record count and secrets match. +// This is the completeness+authenticity contract of a verified backup end-to-end. +func TestExportSealOpenPipeline(t *testing.T) { + bin := writeFakeGopass(t) + gp := &Gopass{Bin: bin} + ctx := context.Background() + + src := t.TempDir() + entries := map[string]string{ + "imported/aws/default": "AKIA/secret+with=specials", + "imported/env/api": "unicode-末-\U0001f511", + } + seed(t, src, entries) + t.Setenv("FAKE_GOPASS_STORE", src) + + // export → plaintext bundle + var plain bytes.Buffer + n, err := gp.ExportTo(ctx, &plain, "imported/") + if err != nil { + t.Fatalf("export: %v", err) + } + if n != 2 { + t.Fatalf("exported %d, want 2", n) + } + + // seal → open + sealer := &Age{WorkFactor: 10} + pass := memguard.NewBufferFromBytes([]byte("backup-pass")) + var sealed bytes.Buffer + if err := sealer.Seal(ctx, bytes.NewReader(plain.Bytes()), &sealed, pass); err != nil { + t.Fatalf("seal: %v", err) + } + var opened bytes.Buffer + if err := sealer.Open(ctx, &sealed, &opened, memguard.NewBufferFromBytes([]byte("backup-pass"))); err != nil { + t.Fatalf("open: %v", err) + } + + // completeness: count matches + cnt, err := CountBundleRecords(bytes.NewReader(opened.Bytes())) + if err != nil { + t.Fatalf("count: %v", err) + } + if cnt != 2 { + t.Fatalf("opened bundle has %d records, want 2", cnt) + } + + // authenticity: import into a fresh store restores the exact secrets + dst := t.TempDir() + t.Setenv("FAKE_GOPASS_STORE", dst) + if _, err := gp.ImportFrom(ctx, bytes.NewReader(opened.Bytes()), true); err != nil { + t.Fatalf("import: %v", err) + } + for path, exp := range entries { + got, err := os.ReadFile(filepath.Join(dst, path)) + if err != nil { + t.Fatalf("restored %s: %v", path, err) + } + if string(got) != exp { + t.Errorf("%s: restored %q want %q", path, got, exp) + } + } +} + +// TestExportFailsOnGopassShowError proves ExportTo surfaces a `gopass show` failure +// as an error rather than silently emitting a short (incomplete) backup — the backup +// gate must never treat a partial export as a complete one. +func TestExportFailsOnGopassShowError(t *testing.T) { + bin := filepath.Join(t.TempDir(), "gopass") + // ls lists one path; show always fails. + script := `#!/usr/bin/env bash +case "$1" in + ls) echo "imported/env/api" ;; + show) echo "boom" >&2; exit 1 ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + gp := &Gopass{Bin: bin} + var buf bytes.Buffer + if _, err := gp.ExportTo(context.Background(), &buf, "imported/"); err == nil { + t.Fatal("ExportTo ignored a gopass show failure") + } +} + +// TestImportFailsOnInsertError proves ImportFrom surfaces a `gopass insert` failure +// rather than silently dropping an entry on restore. +func TestImportFailsOnInsertError(t *testing.T) { + bin := filepath.Join(t.TempDir(), "gopass") + script := `#!/usr/bin/env bash +case "$1" in + insert) cat >/dev/null; echo "denied" >&2; exit 1 ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + gp := &Gopass{Bin: bin} + + var bundle bytes.Buffer + bundle.Write(frameRecord("imported/env/api", "value")) + bundle.Write(streamSentinel()) + if _, err := gp.ImportFrom(context.Background(), &bundle, true); err == nil { + t.Fatal("ImportFrom ignored a gopass insert failure") + } +} + +// writeArgvLoggingGopass is like writeFakeGopass but appends the full argv of every +// invocation to logPath, so a test can assert the secret never rode in argv. +func writeArgvLoggingGopass(t *testing.T, logPath string) string { + t.Helper() + bin := filepath.Join(t.TempDir(), "gopass") + script := `#!/usr/bin/env bash +echo "$@" >> "` + logPath + `" +store="$FAKE_GOPASS_STORE" +case "$1" in + ls) (cd "$store" 2>/dev/null && find . -type f | sed 's#^\./##' | sort) ;; + show) cat "$store/$3" ;; + insert) + shift; path="" + for a in "$@"; do case "$a" in --multiline=false|-f) ;; *) path="$a";; esac; done + mkdir -p "$store/$(dirname "$path")"; cat > "$store/$path" ;; +esac +` + if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + return bin +} diff --git a/internal/sink/sealer_test.go b/internal/sink/sealer_test.go new file mode 100644 index 0000000..a10da39 --- /dev/null +++ b/internal/sink/sealer_test.go @@ -0,0 +1,254 @@ +package sink + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/awnumar/memguard" +) + +// newPass returns a fresh locked buffer for s. A fresh buffer is used per Seal/Open +// call because memguard.NewBufferFromBytes wipes its source slice, and reusing one +// buffer across operations would couple unrelated calls. +func newPass(s string) *memguard.LockedBuffer { + return memguard.NewBufferFromBytes([]byte(s)) +} + +// authenticatedSealers fail closed on tamper / wrong passphrase. openssl is +// deliberately excluded — it is unauthenticated (see openssl.go) and its tamper +// behaviour is asserted separately in TestOpenSSLTamperUnauthenticated. +func authenticatedSealers() map[string]Sealer { + return map[string]Sealer{ + "age": &Age{WorkFactor: 10}, // low cost keeps the test fast; correctness is unaffected + "hmac": &HMACSealer{Iter: 1000}, + } +} + +func allSealers() map[string]Sealer { + s := authenticatedSealers() + s["openssl"] = &OpenSSL{Iter: 1000} + return s +} + +// TestSealerRoundTrip proves every sealer is byte-exact: seal → open == original, +// including empty and binary payloads. +func TestSealerRoundTrip(t *testing.T) { + ctx := context.Background() + payloads := map[string][]byte{ + "ascii": []byte("AKIA/secret+with=specials"), + "binary": {0x00, 0xff, 0x0a, 0x0d, 0x1b, 0x7f, 0x80}, + "unicode": []byte("unicode-末-\U0001f511"), + "empty": {}, + "large": bytes.Repeat([]byte("credential-storage-turnover "), 4096), + } + for name, sealer := range allSealers() { + for pname, pt := range payloads { + t.Run(name+"/"+pname, func(t *testing.T) { + var sealed bytes.Buffer + if err := sealer.Seal(ctx, bytes.NewReader(pt), &sealed, newPass("correct horse")); err != nil { + t.Fatalf("seal: %v", err) + } + // Sealed form must not be the plaintext (except that an empty + // plaintext may legitimately produce a tiny header-only blob). + if len(pt) > 0 && bytes.Contains(sealed.Bytes(), pt) { + t.Fatal("plaintext leaked into sealed output") + } + var opened bytes.Buffer + if err := sealer.Open(ctx, &sealed, &opened, newPass("correct horse")); err != nil { + t.Fatalf("open: %v", err) + } + if !bytes.Equal(opened.Bytes(), pt) { + t.Fatalf("round-trip mismatch: got %q want %q", opened.Bytes(), pt) + } + }) + } + } +} + +// TestSealerWrongPassphrase proves that opening with the wrong passphrase fails +// cleanly and yields no plaintext for the authenticated sealers. openssl (CBC, +// unauthenticated) may or may not error, so it is tested for the weaker property: +// it must never reproduce the original plaintext under a wrong passphrase. +func TestSealerWrongPassphrase(t *testing.T) { + ctx := context.Background() + pt := []byte("verify-new-before-revoke-old") + + t.Run("authenticated-fail-closed", func(t *testing.T) { + for name, sealer := range authenticatedSealers() { + var sealed bytes.Buffer + if err := sealer.Seal(ctx, bytes.NewReader(pt), &sealed, newPass("right-pass")); err != nil { + t.Fatalf("%s seal: %v", name, err) + } + var opened bytes.Buffer + err := sealer.Open(ctx, &sealed, &opened, newPass("wrong-pass")) + if err == nil { + t.Errorf("%s: wrong passphrase opened without error", name) + } + if opened.Len() != 0 { + t.Errorf("%s: emitted %d bytes on wrong passphrase (want 0)", name, opened.Len()) + } + } + }) + + t.Run("openssl-no-plaintext-recovery", func(t *testing.T) { + sealer := &OpenSSL{Iter: 1000} + var sealed bytes.Buffer + if err := sealer.Seal(ctx, bytes.NewReader(pt), &sealed, newPass("right-pass")); err != nil { + t.Fatalf("seal: %v", err) + } + var opened bytes.Buffer + // Unauthenticated: an error is acceptable but not guaranteed; the invariant + // is that the original plaintext is never recovered. + _ = sealer.Open(ctx, &sealed, &opened, newPass("wrong-pass")) + if bytes.Equal(opened.Bytes(), pt) { + t.Fatal("openssl recovered plaintext under a wrong passphrase") + } + }) +} + +// TestSealerTamperFailsClosed proves the authenticated sealers reject a bundle +// whose ciphertext has been altered. This is the property the backup gate leans on: +// a corrupt backup must fail to open rather than restore silent garbage. +func TestSealerTamperFailsClosed(t *testing.T) { + ctx := context.Background() + pt := []byte("backup before rotate, always") + + for name, sealer := range authenticatedSealers() { + t.Run(name, func(t *testing.T) { + var sealed bytes.Buffer + if err := sealer.Seal(ctx, bytes.NewReader(pt), &sealed, newPass("pw")); err != nil { + t.Fatalf("seal: %v", err) + } + b := sealed.Bytes() + if len(b) == 0 { + t.Fatal("empty sealed output") + } + // Flip a byte near the end (well inside the ciphertext body for both + // the age payload and the hmac mac||ct layout). + b[len(b)-1] ^= 0x01 + + var opened bytes.Buffer + err := sealer.Open(ctx, bytes.NewReader(b), &opened, newPass("pw")) + if err == nil { + t.Errorf("%s: opened a tampered bundle without error", name) + } + if opened.Len() != 0 { + t.Errorf("%s: emitted %d bytes from a tampered bundle (want 0)", name, opened.Len()) + } + }) + } +} + +// TestHMACTamperInMACHeader proves the HMAC sealer rejects a bundle whose MAC (the +// first 32 bytes) has been altered — verified before any decryption is attempted. +func TestHMACTamperInMACHeader(t *testing.T) { + ctx := context.Background() + pt := []byte("integrity first") + sealer := &HMACSealer{Iter: 1000} + + var sealed bytes.Buffer + if err := sealer.Seal(ctx, bytes.NewReader(pt), &sealed, newPass("pw")); err != nil { + t.Fatalf("seal: %v", err) + } + b := sealed.Bytes() + b[0] ^= 0xff // corrupt the MAC header + + var opened bytes.Buffer + if err := sealer.Open(ctx, bytes.NewReader(b), &opened, newPass("pw")); err == nil { + t.Error("hmac: opened a bundle with a corrupt MAC without error") + } + if opened.Len() != 0 { + t.Errorf("hmac: emitted %d bytes despite MAC corruption", opened.Len()) + } +} + +// TestHMACShortBundle proves the HMAC sealer rejects a bundle too short to contain +// a MAC rather than panicking on the slice. +func TestHMACShortBundle(t *testing.T) { + sealer := &HMACSealer{Iter: 1000} + var opened bytes.Buffer + err := sealer.Open(context.Background(), strings.NewReader("tiny"), &opened, newPass("pw")) + if err == nil { + t.Error("hmac: accepted a bundle shorter than a MAC") + } +} + +// TestSealerNames pins the wire/name identity of each sealer (used in backup +// metadata and CLI selection). +func TestSealerNames(t *testing.T) { + for want, s := range map[string]Sealer{"age": &Age{}, "hmac": &HMACSealer{}, "openssl": &OpenSSL{}} { + if got := s.Name(); got != want { + t.Errorf("Name() = %q, want %q", got, want) + } + } +} + +// TestSealerDefaultConfig exercises the zero-value default paths of the openssl-based +// sealers (default bin "openssl", default iter) with a real round-trip. Age's default +// work factor (18, ~256 MiB scrypt) is intentionally not exercised here for speed. +func TestSealerDefaultConfig(t *testing.T) { + ctx := context.Background() + pt := []byte("default-config round trip") + for name, s := range map[string]Sealer{"openssl": &OpenSSL{}, "hmac": &HMACSealer{}} { + t.Run(name, func(t *testing.T) { + var sealed, opened bytes.Buffer + if err := s.Seal(ctx, bytes.NewReader(pt), &sealed, newPass("pw")); err != nil { + t.Fatalf("seal: %v", err) + } + if err := s.Open(ctx, &sealed, &opened, newPass("pw")); err != nil { + t.Fatalf("open: %v", err) + } + if !bytes.Equal(opened.Bytes(), pt) { + t.Fatalf("round-trip mismatch") + } + }) + } +} + +// TestOpenSSLBadBinary proves a failure to launch the openssl subprocess surfaces +// as an error (the backup gate must not treat a failed seal as success). +func TestOpenSSLBadBinary(t *testing.T) { + s := &OpenSSL{Bin: "/nonexistent/openssl", Iter: 1000} + var out bytes.Buffer + if err := s.Seal(context.Background(), strings.NewReader("x"), &out, newPass("pw")); err == nil { + t.Fatal("seal with a missing openssl binary returned no error") + } +} + +// TestAgeEmptyPassphrase proves age rejects an empty passphrase at seal time rather +// than producing an unopenable or weakly-keyed bundle. +func TestAgeEmptyPassphrase(t *testing.T) { + s := &Age{WorkFactor: 10} + var out bytes.Buffer + if err := s.Seal(context.Background(), strings.NewReader("x"), &out, newPass("")); err == nil { + t.Fatal("age sealed with an empty passphrase") + } +} + +// TestOpenSSLTamperUnauthenticated documents — as an executable contract — that the +// openssl sealer is UNAUTHENTICATED: tampering is NOT guaranteed to be detected. The +// only invariant is that the corrupted bundle does not reproduce the plaintext. This +// is why age is the default and openssl carries a loud warning in its doc comment. +func TestOpenSSLTamperUnauthenticated(t *testing.T) { + ctx := context.Background() + pt := []byte("prefer the age sealer for anything you care about") + sealer := &OpenSSL{Iter: 1000} + + var sealed bytes.Buffer + if err := sealer.Seal(ctx, bytes.NewReader(pt), &sealed, newPass("pw")); err != nil { + t.Fatalf("seal: %v", err) + } + b := sealed.Bytes() + b[len(b)-1] ^= 0x01 + + var opened bytes.Buffer + _ = sealer.Open(ctx, bytes.NewReader(b), &opened, newPass("pw")) + // We assert only the weak property. If this ever starts failing because the + // output equals pt, the sealer would be leaking exact plaintext from a tampered + // bundle, which would be a genuine regression. + if bytes.Equal(opened.Bytes(), pt) { + t.Fatal("openssl reproduced exact plaintext from a tampered bundle") + } +}