2cbfb5d561
---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.
175 lines
4.6 KiB
Go
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")
|
|
}
|
|
}
|