Skip to content

Commit 3afa00a

Browse files
myleshortonclaude
andcommitted
Add unit tests for samizdat inbound/outbound validation
Tests cover invalid private/public key hex decoding, short ID validation, TLS cert/key pair combinations (inline, file path, mixed, missing), timeout parsing errors, and the boolDefault helper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c8aaaf7 commit 3afa00a

File tree

1 file changed

+321
-0
lines changed

1 file changed

+321
-0
lines changed

protocol/samizdat/samizdat_test.go

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package samizdat
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/rand"
8+
"crypto/x509"
9+
"crypto/x509/pkix"
10+
"encoding/hex"
11+
"encoding/pem"
12+
"math/big"
13+
"net"
14+
"os"
15+
"path/filepath"
16+
"strings"
17+
"testing"
18+
"time"
19+
20+
"github.com/stretchr/testify/assert"
21+
"github.com/stretchr/testify/require"
22+
23+
"github.com/getlantern/lantern-box/option"
24+
)
25+
26+
// testPrivKeyHex returns a random 64-char hex string (32 bytes) suitable for a private key.
27+
func testPrivKeyHex(t *testing.T) string {
28+
t.Helper()
29+
b := make([]byte, 32)
30+
_, err := rand.Read(b)
31+
require.NoError(t, err)
32+
return hex.EncodeToString(b)
33+
}
34+
35+
// testPubKeyHex returns a random 64-char hex string (32 bytes) suitable for a public key.
36+
func testPubKeyHex(t *testing.T) string {
37+
t.Helper()
38+
return testPrivKeyHex(t) // same format, just random 32 bytes
39+
}
40+
41+
// testShortIDHex returns a random 16-char hex string (8 bytes).
42+
func testShortIDHex(t *testing.T) string {
43+
t.Helper()
44+
b := make([]byte, 8)
45+
_, err := rand.Read(b)
46+
require.NoError(t, err)
47+
return hex.EncodeToString(b)
48+
}
49+
50+
// testCertKeyPEM generates a self-signed ECDSA cert+key pair for testing.
51+
func testCertKeyPEM(t *testing.T) (certPEM, keyPEM string) {
52+
t.Helper()
53+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
54+
require.NoError(t, err)
55+
56+
template := &x509.Certificate{
57+
SerialNumber: big.NewInt(1),
58+
Subject: pkix.Name{CommonName: "test"},
59+
NotBefore: time.Now(),
60+
NotAfter: time.Now().Add(time.Hour),
61+
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
62+
}
63+
64+
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
65+
require.NoError(t, err)
66+
67+
certPEMBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
68+
keyDER, err := x509.MarshalECPrivateKey(key)
69+
require.NoError(t, err)
70+
keyPEMBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
71+
72+
return string(certPEMBytes), string(keyPEMBytes)
73+
}
74+
75+
// validInboundOptions returns a minimal valid SamizdatInboundOptions for testing.
76+
func validInboundOptions(t *testing.T) option.SamizdatInboundOptions {
77+
t.Helper()
78+
cert, key := testCertKeyPEM(t)
79+
return option.SamizdatInboundOptions{
80+
PrivateKey: testPrivKeyHex(t),
81+
ShortIDs: []string{testShortIDHex(t)},
82+
CertPEM: cert,
83+
KeyPEM: key,
84+
MasqueradeDomain: "example.com",
85+
}
86+
}
87+
88+
// --- boolDefault tests ---
89+
90+
func TestBoolDefault(t *testing.T) {
91+
trueVal := true
92+
falseVal := false
93+
94+
assert.True(t, boolDefault(&trueVal, false), "pointer to true should return true")
95+
assert.False(t, boolDefault(&falseVal, true), "pointer to false should return false")
96+
assert.True(t, boolDefault(nil, true), "nil should return default true")
97+
assert.False(t, boolDefault(nil, false), "nil should return default false")
98+
}
99+
100+
// --- NewInbound validation tests ---
101+
102+
func TestNewInbound_InvalidPrivateKey(t *testing.T) {
103+
tests := []struct {
104+
name string
105+
key string
106+
}{
107+
{"not hex", "zz" + strings.Repeat("00", 31)},
108+
{"too short", hex.EncodeToString(make([]byte, 16))},
109+
{"too long", hex.EncodeToString(make([]byte, 64))},
110+
{"empty", ""},
111+
}
112+
for _, tt := range tests {
113+
t.Run(tt.name, func(t *testing.T) {
114+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
115+
PrivateKey: tt.key,
116+
})
117+
require.Error(t, err)
118+
assert.Contains(t, err.Error(), "private_key")
119+
})
120+
}
121+
}
122+
123+
func TestNewInbound_InvalidShortIDs(t *testing.T) {
124+
privKey := testPrivKeyHex(t)
125+
126+
tests := []struct {
127+
name string
128+
ids []string
129+
errContains string
130+
}{
131+
{"empty list", nil, "short_ids must contain at least one element"},
132+
{"empty list explicit", []string{}, "short_ids must contain at least one element"},
133+
{"not hex", []string{"zz" + strings.Repeat("00", 7)}, "short_ids[0]"},
134+
{"too short", []string{hex.EncodeToString(make([]byte, 4))}, "short_ids[0]"},
135+
{"too long", []string{hex.EncodeToString(make([]byte, 16))}, "short_ids[0]"},
136+
{"second invalid", []string{testShortIDHex(t), "bad"}, "short_ids[1]"},
137+
}
138+
for _, tt := range tests {
139+
t.Run(tt.name, func(t *testing.T) {
140+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
141+
PrivateKey: privKey,
142+
ShortIDs: tt.ids,
143+
})
144+
require.Error(t, err)
145+
assert.Contains(t, err.Error(), tt.errContains)
146+
})
147+
}
148+
}
149+
150+
func TestNewInbound_CertKeyValidation(t *testing.T) {
151+
privKey := testPrivKeyHex(t)
152+
shortIDs := []string{testShortIDHex(t)}
153+
cert, key := testCertKeyPEM(t)
154+
155+
t.Run("missing both", func(t *testing.T) {
156+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
157+
PrivateKey: privKey,
158+
ShortIDs: shortIDs,
159+
})
160+
require.Error(t, err)
161+
assert.Contains(t, err.Error(), "TLS certificate and key must be provided")
162+
})
163+
164+
t.Run("inline cert without key", func(t *testing.T) {
165+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
166+
PrivateKey: privKey,
167+
ShortIDs: shortIDs,
168+
CertPEM: cert,
169+
})
170+
require.Error(t, err)
171+
assert.Contains(t, err.Error(), "both cert_pem and key_pem must be provided")
172+
})
173+
174+
t.Run("inline key without cert", func(t *testing.T) {
175+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
176+
PrivateKey: privKey,
177+
ShortIDs: shortIDs,
178+
KeyPEM: key,
179+
})
180+
require.Error(t, err)
181+
assert.Contains(t, err.Error(), "both cert_pem and key_pem must be provided")
182+
})
183+
184+
t.Run("mixed inline and path", func(t *testing.T) {
185+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
186+
PrivateKey: privKey,
187+
ShortIDs: shortIDs,
188+
CertPEM: cert,
189+
KeyPEM: key,
190+
CertPath: "/some/cert.pem",
191+
})
192+
require.Error(t, err)
193+
assert.Contains(t, err.Error(), "cannot mix inline PEM")
194+
})
195+
196+
t.Run("mixed inline and key path", func(t *testing.T) {
197+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
198+
PrivateKey: privKey,
199+
ShortIDs: shortIDs,
200+
CertPEM: cert,
201+
KeyPEM: key,
202+
KeyPath: "/some/key.pem",
203+
})
204+
require.Error(t, err)
205+
assert.Contains(t, err.Error(), "cannot mix inline PEM")
206+
})
207+
208+
t.Run("cert path without key path", func(t *testing.T) {
209+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
210+
PrivateKey: privKey,
211+
ShortIDs: shortIDs,
212+
CertPath: "/some/cert.pem",
213+
})
214+
require.Error(t, err)
215+
assert.Contains(t, err.Error(), "both cert_path and key_path must be provided")
216+
})
217+
218+
t.Run("key path without cert path", func(t *testing.T) {
219+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
220+
PrivateKey: privKey,
221+
ShortIDs: shortIDs,
222+
KeyPath: "/some/key.pem",
223+
})
224+
require.Error(t, err)
225+
assert.Contains(t, err.Error(), "both cert_path and key_path must be provided")
226+
})
227+
228+
t.Run("cert path file not found", func(t *testing.T) {
229+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
230+
PrivateKey: privKey,
231+
ShortIDs: shortIDs,
232+
CertPath: "/nonexistent/cert.pem",
233+
KeyPath: "/nonexistent/key.pem",
234+
})
235+
require.Error(t, err)
236+
assert.Contains(t, err.Error(), "reading cert file")
237+
})
238+
239+
t.Run("key path file not found", func(t *testing.T) {
240+
// Write a valid cert file but no key file
241+
dir := t.TempDir()
242+
certPath := filepath.Join(dir, "cert.pem")
243+
require.NoError(t, os.WriteFile(certPath, []byte(cert), 0o600))
244+
245+
_, err := NewInbound(context.Background(), nil, nil, "test", option.SamizdatInboundOptions{
246+
PrivateKey: privKey,
247+
ShortIDs: shortIDs,
248+
CertPath: certPath,
249+
KeyPath: filepath.Join(dir, "nonexistent.pem"),
250+
})
251+
require.Error(t, err)
252+
assert.Contains(t, err.Error(), "reading key file")
253+
})
254+
}
255+
256+
func TestNewInbound_InvalidTimeouts(t *testing.T) {
257+
opts := validInboundOptions(t)
258+
259+
t.Run("bad masquerade_idle_timeout", func(t *testing.T) {
260+
o := opts
261+
o.MasqueradeIdleTimeout = "not-a-duration"
262+
_, err := NewInbound(context.Background(), nil, nil, "test", o)
263+
require.Error(t, err)
264+
assert.Contains(t, err.Error(), "masquerade_idle_timeout")
265+
})
266+
267+
t.Run("bad masquerade_max_duration", func(t *testing.T) {
268+
o := opts
269+
o.MasqueradeMaxDuration = "not-a-duration"
270+
_, err := NewInbound(context.Background(), nil, nil, "test", o)
271+
require.Error(t, err)
272+
assert.Contains(t, err.Error(), "masquerade_max_duration")
273+
})
274+
}
275+
276+
// --- NewOutbound validation tests ---
277+
278+
func TestNewOutbound_InvalidPublicKey(t *testing.T) {
279+
tests := []struct {
280+
name string
281+
key string
282+
}{
283+
{"not hex", "zz" + strings.Repeat("00", 31)},
284+
{"too short", hex.EncodeToString(make([]byte, 16))},
285+
{"too long", hex.EncodeToString(make([]byte, 64))},
286+
{"empty", ""},
287+
}
288+
for _, tt := range tests {
289+
t.Run(tt.name, func(t *testing.T) {
290+
_, err := NewOutbound(context.Background(), nil, nil, "test", option.SamizdatOutboundOptions{
291+
PublicKey: tt.key,
292+
})
293+
require.Error(t, err)
294+
assert.Contains(t, err.Error(), "public_key")
295+
})
296+
}
297+
}
298+
299+
func TestNewOutbound_InvalidShortID(t *testing.T) {
300+
pubKey := testPubKeyHex(t)
301+
302+
tests := []struct {
303+
name string
304+
id string
305+
}{
306+
{"not hex", "zz" + strings.Repeat("00", 7)},
307+
{"too short", hex.EncodeToString(make([]byte, 4))},
308+
{"too long", hex.EncodeToString(make([]byte, 16))},
309+
{"empty", ""},
310+
}
311+
for _, tt := range tests {
312+
t.Run(tt.name, func(t *testing.T) {
313+
_, err := NewOutbound(context.Background(), nil, nil, "test", option.SamizdatOutboundOptions{
314+
PublicKey: pubKey,
315+
ShortID: tt.id,
316+
})
317+
require.Error(t, err)
318+
assert.Contains(t, err.Error(), "short_id")
319+
})
320+
}
321+
}

0 commit comments

Comments
 (0)