Files
seaweedfs/weed/s3api/s3_sse_s3_kek_passphrase_test.go
T
Chris LuandClaude Opus 5.5 af45ab8f2a s3api: known-answer fixture generator for SSE-S3 and filer-encrypted chunks (#12355)
* s3api: a known-answer fixture generator for SSE-S3 and filer-encrypted chunks

TestGenerateDirectReadFixture writes, from this package's own encryption,
the pieces a reader outside the gateway must handle: SSE-S3 chunks with
their per-chunk metadata, the object-level key, a single-stream ciphertext,
the KEK derived from s3.sse.key and wrapped under a passphrase in both
on-disk formats, and chunks encrypted by the filer, plain and gzipped. It
is skipped unless DIRECT_READ_FIXTURE names the output file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

* s3api: the fixture generator restores the key manager and checks the write

The test set the global key manager's KEK and left it; a later test in
the same run then found a signing key where it expects none. The previous
key is restored with t.Cleanup. The fixture file's close error is checked
too, so a delayed write failure fails the test instead of leaving a short
fixture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK

* s3api: the fixture generator unwraps both KEK formats before writing

The legacy v1 wrap is built by hand in the test; reading v1 and v2 back
through unwrapKEK ties both to the format the key manager accepts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* s3api: pin a legacy v1 wrapped KEK made outside the package

Generating the v1 sample through legacyKEKWrappingSalt let a change to
the salt pass on both sides; a vector wrapped elsewhere under the
historical salt must still unwrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 04:53:57 -08:00

112 lines
3.6 KiB
Go

package s3api
import (
"testing"
)
// TestSetKEKPassphraseEnablesEncryptedRoundTrip exercises the wrap/unwrap
// round-trip after SetKEKPassphrase configures the manager. This is the
// primary behaviour the new env-var wiring relies on: once the passphrase
// is in place, wrapKEK produces v2-format ciphertext that unwrapKEK can
// reverse. Without the SetKEKPassphrase plumbing, deriveWrappingKey would
// fail with "no KEK passphrase configured".
func TestSetKEKPassphraseEnablesEncryptedRoundTrip(t *testing.T) {
km := NewSSES3KeyManager()
km.SetKEKPassphrase("test-passphrase-32-bytes-or-anything")
original := make([]byte, SSES3KeySize)
for i := range original {
original[i] = byte(i)
}
wrapped, err := km.wrapKEK(original)
if err != nil {
t.Fatalf("wrapKEK: %v", err)
}
got, isV2, err := km.unwrapKEK(wrapped)
if err != nil {
t.Fatalf("unwrapKEK: %v", err)
}
if !isV2 {
t.Fatal("wrapped output should be reported as v2 by unwrapKEK")
}
if string(got) != string(original) {
t.Fatalf("round-trip mismatch: got %x want %x", got, original)
}
}
// TestSetKEKPassphraseDifferentInstancesNoCollision proves random salts
// per-installation actually decorrelate two managers using the same
// passphrase: their wrapped payloads must differ even when the input KEK
// matches.
func TestSetKEKPassphraseDifferentInstancesNoCollision(t *testing.T) {
a := NewSSES3KeyManager()
a.SetKEKPassphrase("shared-passphrase")
b := NewSSES3KeyManager()
b.SetKEKPassphrase("shared-passphrase")
kek := make([]byte, SSES3KeySize)
for i := range kek {
kek[i] = 0xAB
}
wa, err := a.wrapKEK(kek)
if err != nil {
t.Fatalf("wrapKEK a: %v", err)
}
wb, err := b.wrapKEK(kek)
if err != nil {
t.Fatalf("wrapKEK b: %v", err)
}
if string(wa) == string(wb) {
t.Fatal("two managers with the same passphrase produced byte-identical wrapped output; salt is not random")
}
// Both must still self-roundtrip.
if got, _, _ := a.unwrapKEK(wa); string(got) != string(kek) {
t.Fatal("manager a failed self roundtrip")
}
if got, _, _ := b.unwrapKEK(wb); string(got) != string(kek) {
t.Fatal("manager b failed self roundtrip")
}
}
// TestNoPassphraseKeepsLegacyHexDecodePath confirms that a manager left
// without a passphrase still drives the historical plaintext-hex path
// (loadSuperKeyFromFiler logs a warning and reads hex). That's the
// fallback InitializeGlobalSSES3KeyManager keeps for upgrades.
func TestNoPassphraseKeepsLegacyHexDecodePath(t *testing.T) {
km := NewSSES3KeyManager()
if km.kekPassphrase != "" {
t.Fatalf("default passphrase should be empty, got %q", km.kekPassphrase)
}
// wrapKEK requires a passphrase; without one it must surface the error
// rather than producing unencrypted output.
if _, err := km.wrapKEK(make([]byte, SSES3KeySize)); err == nil {
t.Fatal("wrapKEK without passphrase should fail; got nil error")
}
}
// A KEK wrapped in the legacy v1 layout (nonce || ciphertext+tag) under the
// fixed salt "seaweedfs-sse-s3-kek-wrapping-v1", produced outside this
// package, so a change to legacyKEKWrappingSalt cannot pass by changing
// both sides.
func TestUnwrapKEKReadsPinnedLegacyV1(t *testing.T) {
km := NewSSES3KeyManager("legacy-passphrase")
got, isV2, err := km.unwrapKEK([]byte("oKGio6SlpqeoqaqrixOn/dNaewpX3OoMjLT3VUFh+zg+Hd1fqbiWa6Xhc3SAg6hZdXb+Pwo520Fc1dfj"))
if err != nil {
t.Fatalf("unwrapKEK: %v", err)
}
if isV2 {
t.Fatal("a legacy payload was reported as v2")
}
want := make([]byte, SSES3KeySize)
for i := range want {
want[i] = byte(i)
}
if string(got) != string(want) {
t.Fatalf("unwrapped %x, want %x", got, want)
}
}