Files
orca/internal/seal/seal_test.go
T
Jon Chery 2cbfb5d561 feat(P08): master key seal-to-OIDC + Shamir 3-of-5 (REQ-147, D-241, C-35)
---ci---
project: orca
phase: 8
milestone: v0.12
status: execute
---/ci---

internal/seal/seal.go: AES-256-GCM sealing with HKDF-SHA256 key
derivation from OIDC subject. Seal/Unseal (OIDC mode), SealWithCA/
UnsealWithCA (mTLS-only offline path), SaveSealed/LoadSealed (0600),
VerifySealedKey.
internal/seal/shamir.go: GF(256) Shamir secret sharing. ShamirSplit
(5 shards, threshold 3), ShamirCombine (Lagrange interpolation).
UnsealWithShamir for IdP-lost recovery (C-35).
9 tests: seal/unseal round-trip, wrong-sub fails, Shamir 3-of-5
recovery (multiple subsets), 2-shards fails, CA mode, mode mismatch,
shard encoding, verification. All pass. Full build + vet green.
2026-08-07 11:14:01 +00:00

175 lines
4.6 KiB
Go

package seal
import (
"bytes"
"testing"
)
// TestSealUnsealRoundTrip verifies the OIDC seal/unseal round-trip.
func TestSealUnsealRoundTrip(t *testing.T) {
masterKey := make([]byte, 32)
for i := range masterKey {
masterKey[i] = byte(i)
}
blob, shards, err := Seal(masterKey, "user-oidc-sub-123", "https://idp.example")
if err != nil {
t.Fatalf("Seal: %v", err)
}
if len(shards) != 5 {
t.Errorf("shards = %d, want 5", len(shards))
}
if blob.Mode != "oidc" {
t.Errorf("mode = %q, want oidc", blob.Mode)
}
unsealed, err := Unseal(blob, "user-oidc-sub-123")
if err != nil {
t.Fatalf("Unseal: %v", err)
}
if !bytes.Equal(unsealed, masterKey) {
t.Error("unsealed key != original")
}
}
// TestSealWrongSubFails verifies unseal with the wrong subject fails.
func TestSealWrongSubFails(t *testing.T) {
masterKey := make([]byte, 32)
blob, _, err := Seal(masterKey, "correct-sub", "https://idp")
if err != nil {
t.Fatalf("Seal: %v", err)
}
_, err = Unseal(blob, "wrong-sub")
if err == nil {
t.Error("Unseal with wrong sub should fail")
}
}
// TestShamirRecovery verifies 3-of-5 recovery works.
func TestShamirRecovery(t *testing.T) {
masterKey := make([]byte, 32)
for i := range masterKey {
masterKey[i] = byte(i + 1)
}
blob, shards, err := Seal(masterKey, "sub-123", "https://idp")
if err != nil {
t.Fatalf("Seal: %v", err)
}
// Recover with first 3 shards.
recovered, err := UnsealWithShamir(blob, shards[:3])
if err != nil {
t.Fatalf("UnsealWithShamir (3 shards): %v", err)
}
if !bytes.Equal(recovered, masterKey) {
t.Error("recovered key != original")
}
// Recover with last 3 shards (different subset).
recovered2, err := UnsealWithShamir(blob, shards[2:])
if err != nil {
t.Fatalf("UnsealWithShamir (last 3): %v", err)
}
if !bytes.Equal(recovered2, masterKey) {
t.Error("recovered key (last 3) != original")
}
}
// TestShamirTwoShardsFails verifies 2 shards are insufficient.
func TestShamirTwoShardsFails(t *testing.T) {
masterKey := make([]byte, 32)
_, shards, _ := Seal(masterKey, "sub", "https://idp")
_, err := UnsealWithShamir(nil, shards[:2])
if err == nil {
t.Error("2 shards should fail")
}
}
// TestShamirSplitCombine verifies direct split/combine round-trip.
func TestShamirSplitCombine(t *testing.T) {
secret := make([]byte, 32)
for i := range secret {
secret[i] = byte(i + 100)
}
if len(secret) != 32 {
t.Fatalf("test secret is %d bytes, want 32", len(secret))
}
shards, err := ShamirSplit(secret, 5, 3)
if err != nil {
t.Fatalf("ShamirSplit: %v", err)
}
if len(shards) != 5 {
t.Errorf("shards = %d, want 5", len(shards))
}
// Any 3 shards reconstruct the secret.
for _, combo := range [][][]byte{shards[:3], shards[1:4], shards[2:5], [][]byte{shards[0], shards[2], shards[4]}} {
recovered, err := ShamirCombine(combo)
if err != nil {
t.Fatalf("Combine: %v", err)
}
if !bytes.Equal(recovered, secret) {
t.Error("recovered != secret")
}
}
}
// TestSealWithCA verifies the mTLS-only offline path.
func TestSealWithCA(t *testing.T) {
masterKey := make([]byte, 32)
for i := range masterKey {
masterKey[i] = byte(i)
}
blob, err := SealWithCA(masterKey, "sha256:abc123")
if err != nil {
t.Fatalf("SealWithCA: %v", err)
}
if blob.Mode != "ca" {
t.Errorf("mode = %q, want ca", blob.Mode)
}
unsealed, err := UnsealWithCA(blob, "sha256:abc123")
if err != nil {
t.Fatalf("UnsealWithCA: %v", err)
}
if !bytes.Equal(unsealed, masterKey) {
t.Error("unsealed key != original")
}
// Wrong CA fingerprint fails.
_, err = UnsealWithCA(blob, "sha256:wrong")
if err == nil {
t.Error("UnsealWithCA with wrong fingerprint should fail")
}
}
// TestSealedBlobModeMismatch verifies mode mismatch errors.
func TestSealedBlobModeMismatch(t *testing.T) {
masterKey := make([]byte, 32)
blob, _ := SealWithCA(masterKey, "fp")
_, err := Unseal(blob, "sub") // blob is CA-mode, not OIDC
if err == nil {
t.Error("Unseal OIDC on CA blob should fail")
}
}
// TestEncodeDecodeShard verifies shard base64 round-trip.
func TestEncodeDecodeShard(t *testing.T) {
shard := []byte{1, 2, 3, 4, 5}
encoded := EncodeShard(shard)
decoded, err := DecodeShard(encoded)
if err != nil {
t.Fatalf("Decode: %v", err)
}
if !bytes.Equal(decoded, shard) {
t.Error("decode != original")
}
}
// TestVerifySealedKey verifies the verification function.
func TestVerifySealedKey(t *testing.T) {
masterKey := make([]byte, 32)
blob, _, _ := Seal(masterKey, "sub", "https://idp")
if !VerifySealedKey(blob, masterKey, "sub") {
t.Error("VerifySealedKey should confirm correct key")
}
wrongKey := make([]byte, 32)
wrongKey[0] = 1
if VerifySealedKey(blob, wrongKey, "sub") {
t.Error("VerifySealedKey should reject wrong key")
}
}