package crypto import ( "bytes" "crypto/sha256" "encoding/hex" "errors" "strings" "testing" ) const ( tok1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=" tok2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb=" tok3 = "ccccccccccccccccccccccccccccccccccccccccccc=" ) func mustSecret(t *testing.T) *TunnelSecret { t.Helper() s, err := DeriveTunnelSecret(tok1, tok2, tok3) if err != nil { t.Fatalf("DeriveTunnelSecret: %v", err) } return s } func TestDeriveTunnelSecretIsDeterministic(t *testing.T) { a := mustSecret(t) b := mustSecret(t) if !bytes.Equal(a.master, b.master) { t.Error("same tokens produced different secrets") } other, err := DeriveTunnelSecret(tok1, tok2, "different") if err != nil { t.Fatalf("DeriveTunnelSecret: %v", err) } if bytes.Equal(a.master, other.master) { t.Error("different tokens produced the same secret") } } func TestDeriveTunnelSecretRequiresAllTokens(t *testing.T) { for _, tc := range [][3]string{ {"", tok2, tok3}, {tok1, "", tok3}, {tok1, tok2, ""}, } { if _, err := DeriveTunnelSecret(tc[0], tc[1], tc[2]); err == nil { t.Errorf("DeriveTunnelSecret(%q, %q, %q) succeeded, want an error", tc[0], tc[1], tc[2]) } } } // The whole point of the design: the relay knows the group hash, so the // tunnel secret must not be derivable from it. func TestTunnelSecretDiffersFromGroupHash(t *testing.T) { s := mustSecret(t) combined := strings.Join([]string{tok1, tok2, tok3}, ":") sum := sha256.Sum256([]byte(combined)) groupHash := hex.EncodeToString(sum[:]) if hex.EncodeToString(s.master) == groupHash { t.Fatal("tunnel secret equals the group hash — the relay could decrypt everything") } key, err := s.TunnelKey("tunnel-1") if err != nil { t.Fatalf("TunnelKey: %v", err) } if hex.EncodeToString(key) == groupHash { t.Fatal("tunnel key equals the group hash") } if bytes.Equal(key, s.master) { t.Error("tunnel key equals the master secret; it should be derived per tunnel") } } func TestTunnelKeyIsPerTunnel(t *testing.T) { s := mustSecret(t) a, _ := s.TunnelKey("tunnel-a") b, _ := s.TunnelKey("tunnel-b") aAgain, _ := s.TunnelKey("tunnel-a") if bytes.Equal(a, b) { t.Error("different tunnel IDs produced the same key") } if !bytes.Equal(a, aAgain) { t.Error("same tunnel ID produced different keys") } if len(a) != keySize { t.Errorf("key is %d bytes, want %d", len(a), keySize) } } func TestSealOpenRoundTrip(t *testing.T) { s := mustSecret(t) key, _ := s.TunnelKey("t1") sealer, err := NewSealer(key, DirShareToUse) if err != nil { t.Fatalf("NewSealer: %v", err) } opener, err := NewOpener(key, DirShareToUse) if err != nil { t.Fatalf("NewOpener: %v", err) } messages := [][]byte{ []byte("first"), []byte(""), bytes.Repeat([]byte{0xAB}, 65536), []byte("last"), } for i, want := range messages { frame, err := sealer.Seal(want) if err != nil { t.Fatalf("Seal %d: %v", i, err) } if len(frame) != len(want)+FrameOverhead { t.Errorf("frame %d is %d bytes, want %d", i, len(frame), len(want)+FrameOverhead) } // The plaintext must not be visible on the wire. if len(want) > 8 && bytes.Contains(frame, want) { t.Errorf("frame %d contains its plaintext", i) } got, err := opener.Open(frame) if err != nil { t.Fatalf("Open %d: %v", i, err) } if !bytes.Equal(got, want) { t.Errorf("frame %d round-tripped to %q, want %q", i, got, want) } } } // Both ends derive the same tunnel key, so the direction byte is the only // thing keeping their nonce spaces apart. func TestDirectionsUseSeparateNonceSpaces(t *testing.T) { s := mustSecret(t) key, _ := s.TunnelKey("t1") shareToUse, _ := NewSealer(key, DirShareToUse) useToShare, _ := NewSealer(key, DirUseToShare) plaintext := []byte("identical plaintext") a, _ := shareToUse.Seal(plaintext) b, _ := useToShare.Seal(plaintext) if bytes.Equal(a, b) { t.Fatal("both directions produced identical ciphertext — nonce reuse") } // Same counter, so any difference must come from the direction byte. if !bytes.Equal(a[:counterSize], b[:counterSize]) { t.Fatal("test assumption broken: counters differ") } // A frame from one direction must not open with the other direction's opener. wrongWay, _ := NewOpener(key, DirUseToShare) if _, err := wrongWay.Open(a); err == nil { t.Error("a frame opened under the wrong direction") } } func TestOpenRejectsTampering(t *testing.T) { s := mustSecret(t) key, _ := s.TunnelKey("t1") sealer, _ := NewSealer(key, DirShareToUse) original, _ := sealer.Seal([]byte("sensitive usb traffic")) tests := []struct { name string mutate func([]byte) []byte }{ {"flipped ciphertext bit", func(f []byte) []byte { f[counterSize+2] ^= 0x01 return f }}, {"flipped counter bit", func(f []byte) []byte { f[0] ^= 0x80 return f }}, {"truncated tag", func(f []byte) []byte { return f[:len(f)-1] }}, {"appended byte", func(f []byte) []byte { return append(f, 0x00) }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { frame := append([]byte(nil), original...) opener, _ := NewOpener(key, DirShareToUse) if _, err := opener.Open(tt.mutate(frame)); err == nil { t.Error("tampered frame was accepted") } }) } } func TestOpenRejectsWrongKey(t *testing.T) { s := mustSecret(t) good, _ := s.TunnelKey("t1") bad, _ := s.TunnelKey("t2") sealer, _ := NewSealer(good, DirShareToUse) frame, _ := sealer.Seal([]byte("secret")) opener, _ := NewOpener(bad, DirShareToUse) if _, err := opener.Open(frame); err == nil { t.Error("frame opened under a key from a different tunnel") } } func TestOpenRejectsReplay(t *testing.T) { s := mustSecret(t) key, _ := s.TunnelKey("t1") sealer, _ := NewSealer(key, DirShareToUse) opener, _ := NewOpener(key, DirShareToUse) frame, _ := sealer.Seal([]byte("do this once")) if _, err := opener.Open(append([]byte(nil), frame...)); err != nil { t.Fatalf("first delivery: %v", err) } if _, err := opener.Open(append([]byte(nil), frame...)); !errors.Is(err, ErrReplay) { t.Errorf("replayed frame gave %v, want ErrReplay", err) } } // A forged frame carrying a huge counter must not poison the replay window // and lock out the genuine frames that follow. func TestForgedFrameDoesNotAdvanceCounter(t *testing.T) { s := mustSecret(t) key, _ := s.TunnelKey("t1") sealer, _ := NewSealer(key, DirShareToUse) opener, _ := NewOpener(key, DirShareToUse) forged := make([]byte, FrameOverhead+4) for i := range forged[:counterSize] { forged[i] = 0xFF } if _, err := opener.Open(forged); err == nil { t.Fatal("forged frame was accepted") } genuine, _ := sealer.Seal([]byte("real traffic")) got, err := opener.Open(genuine) if err != nil { t.Fatalf("genuine frame rejected after a forgery: %v", err) } if string(got) != "real traffic" { t.Errorf("got %q", got) } } func TestOpenRejectsUndersizedFrame(t *testing.T) { s := mustSecret(t) key, _ := s.TunnelKey("t1") opener, _ := NewOpener(key, DirShareToUse) for _, size := range []int{0, 1, counterSize, FrameOverhead - 1} { if _, err := opener.Open(make([]byte, size)); err == nil { t.Errorf("frame of %d bytes was accepted", size) } } } func TestPeerTokenBindsToContext(t *testing.T) { s := mustSecret(t) a := s.PeerToken("tunnel-1") b := s.PeerToken("tunnel-2") aAgain := s.PeerToken("tunnel-1") if a == b { t.Error("different contexts produced the same token") } if a != aAgain { t.Error("same context produced different tokens") } if len(a) != 64 { t.Errorf("token is %d hex chars, want 64", len(a)) } // A different group must not be able to produce a matching token. other, _ := DeriveTunnelSecret(tok1, tok2, "different") if other.PeerToken("tunnel-1") == a { t.Error("a different group secret produced the same peer token") } }