mirror of
https://github.com/luxfi/zwing.git
synced 2026-09-24 00:24:38 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers, and SHA3-256 now comes from the standard library's crypto/sha3; omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
1448 lines
43 KiB
Go
1448 lines
43 KiB
Go
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
// Coverage tests — these explicitly exercise every error branch and
|
||
// edge case in the PQ + crypto + wire layers so that 100% statement
|
||
// coverage is enforced for the security-critical paths.
|
||
|
||
package zwing
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"errors"
|
||
"io"
|
||
"net"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/luxfi/api/zap"
|
||
"github.com/luxfi/crypto/ed25519"
|
||
"github.com/luxfi/crypto/mldsa"
|
||
"golang.org/x/crypto/curve25519"
|
||
)
|
||
|
||
// curve25519Basepoint returns the X25519 base point.
|
||
func curve25519Basepoint() []byte { return curve25519.Basepoint }
|
||
|
||
// curve25519X25519 wraps the curve25519.X25519 helper so the test code
|
||
// can call it without re-importing.
|
||
func curve25519X25519(priv, base []byte) ([]byte, error) {
|
||
return curve25519.X25519(priv, base)
|
||
}
|
||
|
||
// ─── identity.go ─────────────────────────────────────────────────────
|
||
|
||
// failingReader stops returning bytes after `limit` bytes have been read.
|
||
type failingReader struct {
|
||
r io.Reader
|
||
limit int64
|
||
read int64
|
||
}
|
||
|
||
func newFailingReader(r io.Reader, limit int64) *failingReader {
|
||
return &failingReader{r: r, limit: limit}
|
||
}
|
||
|
||
func (f *failingReader) Read(p []byte) (int, error) {
|
||
if f.read >= f.limit {
|
||
return 0, io.ErrUnexpectedEOF
|
||
}
|
||
remaining := f.limit - f.read
|
||
if int64(len(p)) > remaining {
|
||
p = p[:remaining]
|
||
}
|
||
n, err := f.r.Read(p)
|
||
f.read += int64(n)
|
||
return n, err
|
||
}
|
||
|
||
func TestGenerateIdentityFromShortReader(t *testing.T) {
|
||
// Reader that yields nothing.
|
||
_, err := GenerateIdentityFrom(bytes.NewReader(nil))
|
||
if err == nil {
|
||
t.Fatal("expected ed25519 keygen failure on empty reader")
|
||
}
|
||
|
||
// Reader long enough for ed25519 only — ML-DSA fails next.
|
||
_, err = GenerateIdentityFrom(newFailingReader(bytes.NewReader(make([]byte, 1024)), 64))
|
||
if err == nil {
|
||
t.Fatal("expected ML-DSA keygen failure when reader exhausts")
|
||
}
|
||
|
||
// Reader long enough for ed25519 + ML-DSA — X-Wing fails next.
|
||
// Note: ML-DSA-65 keygen consumes ~64 bytes; ML-KEM-768 keygen consumes
|
||
// ~64 bytes; the X25519 secret is the final 32 bytes. We feed exactly
|
||
// enough for ed25519 (64), ML-DSA (64), ML-KEM (64) but not the X25519.
|
||
// The exact thresholds depend on the underlying impl, so we probe:
|
||
// at 80 bytes either ML-DSA or X-Wing must fail.
|
||
if _, err := GenerateIdentityFrom(newFailingReader(bytes.NewReader(make([]byte, 1<<20)), 80)); err == nil {
|
||
// Implementation may pre-buffer enough to succeed; if so, that's
|
||
// fine — the goal of this test is to drive the error paths in
|
||
// GenerateIdentityFrom, which the empty/64-byte cases above already
|
||
// covered.
|
||
t.Log("80-byte reader unexpectedly produced a full identity — implementation buffers internally")
|
||
}
|
||
}
|
||
|
||
func TestSignErrorPaths(t *testing.T) {
|
||
id, err := GenerateIdentity()
|
||
if err != nil {
|
||
t.Fatalf("identity: %v", err)
|
||
}
|
||
|
||
// Sign with valid identity is the happy path.
|
||
sig := id.Sign([]byte("ctx"), []byte("msg"))
|
||
if len(sig) != ed25519.SignatureSize+mldsa.MLDSA65SignatureSize {
|
||
t.Fatalf("unexpected sig size: %d", len(sig))
|
||
}
|
||
}
|
||
|
||
func TestVerifyErrorPaths(t *testing.T) {
|
||
id, err := GenerateIdentity()
|
||
if err != nil {
|
||
t.Fatalf("identity: %v", err)
|
||
}
|
||
pub := id.Public()
|
||
|
||
// Wrong-size signature.
|
||
if err := pub.Verify([]byte("ctx"), []byte("msg"), []byte("too short")); !errors.Is(err, ErrSignatureInvalid) {
|
||
t.Fatalf("expected ErrSignatureInvalid for short sig, got %v", err)
|
||
}
|
||
|
||
// Valid sig modified in the Ed25519 half.
|
||
sig := id.Sign([]byte("ctx"), []byte("msg"))
|
||
bad := make([]byte, len(sig))
|
||
copy(bad, sig)
|
||
bad[0] ^= 0xFF
|
||
if err := pub.Verify([]byte("ctx"), []byte("msg"), bad); !errors.Is(err, ErrSignatureInvalid) {
|
||
t.Fatal("expected Ed25519 verify failure")
|
||
}
|
||
|
||
// Valid Ed25519 sig but corrupted ML-DSA half.
|
||
bad = make([]byte, len(sig))
|
||
copy(bad, sig)
|
||
bad[ed25519.SignatureSize] ^= 0xFF
|
||
if err := pub.Verify([]byte("ctx"), []byte("msg"), bad); !errors.Is(err, ErrSignatureInvalid) {
|
||
t.Fatal("expected ML-DSA verify failure")
|
||
}
|
||
|
||
// Different message must fail.
|
||
if err := pub.Verify([]byte("ctx"), []byte("other"), sig); !errors.Is(err, ErrSignatureInvalid) {
|
||
t.Fatal("expected verify failure on different message")
|
||
}
|
||
|
||
// Different ctx must fail.
|
||
if err := pub.Verify([]byte("other"), []byte("msg"), sig); !errors.Is(err, ErrSignatureInvalid) {
|
||
t.Fatal("expected verify failure on different ctx")
|
||
}
|
||
}
|
||
|
||
func TestParseIdentityPublicErrors(t *testing.T) {
|
||
if _, err := ParseIdentityPublic(nil); err == nil {
|
||
t.Fatal("expected size error for nil")
|
||
}
|
||
if _, err := ParseIdentityPublic(make([]byte, 4)); err == nil {
|
||
t.Fatal("expected size error for short slice")
|
||
}
|
||
// Right size but garbage ML-DSA pubkey.
|
||
garbage := make([]byte, IdentityPublicSize)
|
||
for i := range garbage {
|
||
garbage[i] = 0xFF
|
||
}
|
||
if _, err := ParseIdentityPublic(garbage); err == nil {
|
||
t.Fatal("expected ML-DSA parse failure")
|
||
}
|
||
|
||
// Right size, valid ML-DSA, but garbage X-Wing.
|
||
id, _ := GenerateIdentity()
|
||
good := id.Public().MarshalBinary()
|
||
mutated := make([]byte, len(good))
|
||
copy(mutated, good)
|
||
for i := ed25519.PublicKeySize + mldsa.MLDSA65PublicKeySize; i < len(mutated); i++ {
|
||
mutated[i] = 0xFF
|
||
}
|
||
// Tamper with the X-Wing portion to break it. ParseXWingPublicKey may
|
||
// accept arbitrary bytes for the curve point but the ML-KEM half can fail.
|
||
mutatedAt := ed25519.PublicKeySize + mldsa.MLDSA65PublicKeySize
|
||
for i := mutatedAt; i < mutatedAt+10; i++ {
|
||
mutated[i] = 0
|
||
}
|
||
// Even if the parse succeeds with weird material, the round trip via
|
||
// MarshalBinary must produce identical bytes.
|
||
pub, err := ParseIdentityPublic(good)
|
||
if err != nil {
|
||
t.Fatalf("parse roundtrip: %v", err)
|
||
}
|
||
if !bytes.Equal(pub.MarshalBinary(), good) {
|
||
t.Fatal("MarshalBinary not idempotent")
|
||
}
|
||
}
|
||
|
||
func TestIdentityEqual(t *testing.T) {
|
||
a, _ := GenerateIdentity()
|
||
b, _ := GenerateIdentity()
|
||
|
||
pa := a.Public()
|
||
pb := b.Public()
|
||
if pa.Equal(pb) {
|
||
t.Fatal("distinct identities should not be Equal")
|
||
}
|
||
if !pa.Equal(pa) {
|
||
t.Fatal("identity should equal itself")
|
||
}
|
||
if pa.Equal(nil) {
|
||
t.Fatal("Equal(nil) should return false")
|
||
}
|
||
var nilPub *IdentityPublic
|
||
if nilPub.Equal(pa) {
|
||
t.Fatal("nil receiver should return false")
|
||
}
|
||
|
||
// Same Ed25519, mutated DSA half.
|
||
mod := *pa
|
||
if !pa.Equal(&mod) {
|
||
t.Fatal("same fields should equal")
|
||
}
|
||
|
||
// Mutate a single bit in the Ed pub of a copy.
|
||
edCopy := append(ed25519.PublicKey{}, pa.EdPub...)
|
||
edCopy[0] ^= 0x01
|
||
mod = IdentityPublic{EdPub: edCopy, DSAPub: pa.DSAPub, XWingPub: pa.XWingPub}
|
||
if pa.Equal(&mod) {
|
||
t.Fatal("Ed mismatch should not equal")
|
||
}
|
||
}
|
||
|
||
// ─── kem.go ──────────────────────────────────────────────────────────
|
||
|
||
func TestParseXWingPublicKeyErrors(t *testing.T) {
|
||
if _, err := ParseXWingPublicKey(nil); err == nil {
|
||
t.Fatal("expected size error")
|
||
}
|
||
if _, err := ParseXWingPublicKey(make([]byte, XWingPublicKeySize-1)); err == nil {
|
||
t.Fatal("expected size error for truncated input")
|
||
}
|
||
// Right size but invalid ML-KEM bytes.
|
||
bad := make([]byte, XWingPublicKeySize)
|
||
// Leave all-zero — ML-KEM-768 may reject all-zero pubkeys depending on impl.
|
||
// At minimum the round-trip must work for valid material.
|
||
id, _ := GenerateIdentity()
|
||
good := id.XWing.Public().MarshalBinary()
|
||
if pk, err := ParseXWingPublicKey(good); err != nil {
|
||
t.Fatalf("valid pk failed to parse: %v", err)
|
||
} else if !bytes.Equal(pk.MarshalBinary(), good) {
|
||
t.Fatal("round trip mismatch")
|
||
}
|
||
_ = bad
|
||
}
|
||
|
||
func TestGenerateXWingKeyShortReader(t *testing.T) {
|
||
// Reader that gives ML-KEM enough but not the X25519 32 bytes after.
|
||
// ML-KEM-768 keypair generation pulls 64 bytes of seed from the reader,
|
||
// so a 64-byte reader should pass ML-KEM and then fail on X25519.
|
||
_, err := GenerateXWingKey(newFailingReader(bytes.NewReader(make([]byte, 1<<20)), 64))
|
||
if err == nil {
|
||
t.Fatal("expected X25519 read failure")
|
||
}
|
||
// Empty reader → ML-KEM keygen fails first.
|
||
if _, err := GenerateXWingKey(bytes.NewReader(nil)); err == nil {
|
||
t.Fatal("expected ML-KEM keygen failure")
|
||
}
|
||
}
|
||
|
||
func TestXWingEncapsulateShortReader(t *testing.T) {
|
||
id, _ := GenerateIdentity()
|
||
pub := id.XWing.Public()
|
||
// Reader that fails after the ML-KEM encap consumes its bytes — there's
|
||
// no clean way to know the exact split, but a reader yielding 0 bytes
|
||
// fails immediately.
|
||
_, _, err := XWingEncapsulate(bytes.NewReader(nil), pub)
|
||
if err == nil {
|
||
t.Fatal("expected encapsulate to fail with empty reader")
|
||
}
|
||
|
||
// Reader that gives ML-KEM all it needs but not the X25519 32 bytes.
|
||
// The ML-KEM keygen pulls 64 random bytes; encapsulation pulls 32. So
|
||
// roughly 32–64 bytes lets ML-KEM proceed and X25519 fail.
|
||
_, _, err = XWingEncapsulate(newFailingReader(bytes.NewReader(make([]byte, 1<<20)), 32), pub)
|
||
if err == nil {
|
||
t.Fatal("expected X25519 ephemeral read failure")
|
||
}
|
||
}
|
||
|
||
func TestXWingDecapsulateErrors(t *testing.T) {
|
||
id, _ := GenerateIdentity()
|
||
|
||
// Wrong size.
|
||
if _, err := XWingDecapsulate(id.XWing, nil); err == nil {
|
||
t.Fatal("expected size error")
|
||
}
|
||
if _, err := XWingDecapsulate(id.XWing, make([]byte, XWingCiphertextSize-1)); err == nil {
|
||
t.Fatal("expected size error")
|
||
}
|
||
// Right size, garbage ML-KEM ciphertext.
|
||
bad := make([]byte, XWingCiphertextSize)
|
||
if _, err := XWingDecapsulate(id.XWing, bad); err != nil {
|
||
// Some ML-KEM impls accept and produce a "junk" shared secret — that's
|
||
// allowed by the spec (implicit rejection). We just don't pin a
|
||
// particular outcome here, but exercising the path is enough.
|
||
_ = err
|
||
}
|
||
}
|
||
|
||
// ─── wire.go ─────────────────────────────────────────────────────────
|
||
|
||
type writerErr struct{ err error }
|
||
|
||
func (w *writerErr) Write(p []byte) (int, error) { return 0, w.err }
|
||
|
||
func TestWriteFrameErrors(t *testing.T) {
|
||
if err := writeFrame(io.Discard, make([]byte, MaxFrameSize+1)); !errors.Is(err, ErrMessageTooLarge) {
|
||
t.Fatalf("expected ErrMessageTooLarge, got %v", err)
|
||
}
|
||
// Failing writer must propagate.
|
||
w := &writerErr{err: errors.New("boom")}
|
||
if err := writeFrame(w, []byte("x")); err == nil {
|
||
t.Fatal("expected error from failing writer")
|
||
}
|
||
}
|
||
|
||
type readerErr struct{ err error }
|
||
|
||
func (r *readerErr) Read(p []byte) (int, error) { return 0, r.err }
|
||
|
||
func TestReadFrameErrors(t *testing.T) {
|
||
// Header read fails.
|
||
if _, err := readFrame(&readerErr{err: io.EOF}); err == nil {
|
||
t.Fatal("expected EOF on header")
|
||
}
|
||
// Length too large.
|
||
hdr := []byte{0xFF, 0xFF, 0xFF, 0xFF}
|
||
if _, err := readFrame(bytes.NewReader(hdr)); !errors.Is(err, ErrMessageTooLarge) {
|
||
t.Fatalf("expected ErrMessageTooLarge, got %v", err)
|
||
}
|
||
// Length is zero.
|
||
hdr = []byte{0, 0, 0, 0}
|
||
if _, err := readFrame(bytes.NewReader(hdr)); !errors.Is(err, ErrInvalidWireFormat) {
|
||
t.Fatalf("expected ErrInvalidWireFormat, got %v", err)
|
||
}
|
||
// Header reads but body underflows.
|
||
hdr = append(append([]byte(nil), 0, 0, 0, 8), []byte("ab")...)
|
||
if _, err := readFrame(bytes.NewReader(hdr)); err == nil {
|
||
t.Fatal("expected body read failure")
|
||
}
|
||
}
|
||
|
||
// ─── handshake.go small helpers ─────────────────────────────────────
|
||
|
||
func TestSliceReader(t *testing.T) {
|
||
r := newSliceReader([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
|
||
v, err := r.readU16()
|
||
if err != nil {
|
||
t.Fatalf("readU16: %v", err)
|
||
}
|
||
if v != 0x0102 {
|
||
t.Fatalf("got %#x want 0x0102", v)
|
||
}
|
||
chunk, err := r.readN(2)
|
||
if err != nil {
|
||
t.Fatalf("readN: %v", err)
|
||
}
|
||
if !bytes.Equal(chunk, []byte{0x03, 0x04}) {
|
||
t.Fatalf("readN chunk wrong: %x", chunk)
|
||
}
|
||
if r.empty() {
|
||
t.Fatal("should not be empty yet")
|
||
}
|
||
if _, err := r.readN(2); err == nil {
|
||
t.Fatal("expected short read")
|
||
}
|
||
if _, err := r.readN(-1); err == nil {
|
||
t.Fatal("negative readN should error")
|
||
}
|
||
|
||
// readU16 with too-short buffer.
|
||
short := newSliceReader([]byte{0x01})
|
||
if _, err := short.readU16(); !errors.Is(err, ErrShortRead) {
|
||
t.Fatalf("expected ErrShortRead, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestAppendU16Panics(t *testing.T) {
|
||
defer func() {
|
||
if r := recover(); r == nil {
|
||
t.Fatal("expected panic on out-of-range u16")
|
||
}
|
||
}()
|
||
appendU16(nil, 0x10000)
|
||
}
|
||
|
||
func TestParseHandshakeInitErrors(t *testing.T) {
|
||
if _, err := parseHandshakeInit(nil); err == nil {
|
||
t.Fatal("expected error on empty")
|
||
}
|
||
// idLen short read (only 1 byte available).
|
||
if _, err := parseHandshakeInit([]byte{0xFF}); err == nil {
|
||
t.Fatal("expected short read for idLen")
|
||
}
|
||
// idLen larger than remaining buffer.
|
||
if _, err := parseHandshakeInit([]byte{0xFF, 0xFF}); err == nil {
|
||
t.Fatal("expected short read for id")
|
||
}
|
||
// idLen=1 succeeds, then sigLen short read.
|
||
if _, err := parseHandshakeInit([]byte{0, 1, 0xAA}); err == nil {
|
||
t.Fatal("expected short read for sigLen")
|
||
}
|
||
// idLen=0, sigLen larger than rest.
|
||
if _, err := parseHandshakeInit([]byte{0, 0, 0xFF, 0xFF}); err == nil {
|
||
t.Fatal("expected short read for sig")
|
||
}
|
||
// Trailing garbage.
|
||
bad := []byte{0, 1, 0xAA, 0, 1, 0xBB, 0xCC}
|
||
if _, err := parseHandshakeInit(bad); !errors.Is(err, ErrInvalidWireFormat) {
|
||
t.Fatalf("expected ErrInvalidWireFormat, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestParseHandshakeResponseErrors(t *testing.T) {
|
||
if _, err := parseHandshakeResponse(nil); !errors.Is(err, ErrInvalidWireFormat) {
|
||
t.Fatalf("expected ErrInvalidWireFormat, got %v", err)
|
||
}
|
||
// Constructed by hand: too short to fit XWingCiphertextSize.
|
||
if _, err := parseHandshakeResponse(make([]byte, 100)); !errors.Is(err, ErrInvalidWireFormat) {
|
||
t.Fatalf("expected ErrInvalidWireFormat for short, got %v", err)
|
||
}
|
||
// Right size for ct but encLen > rest.
|
||
bad := make([]byte, XWingCiphertextSize+2)
|
||
bad[XWingCiphertextSize] = 0xFF
|
||
bad[XWingCiphertextSize+1] = 0xFF
|
||
if _, err := parseHandshakeResponse(bad); err == nil {
|
||
t.Fatal("expected short enc read")
|
||
}
|
||
// Trailing garbage.
|
||
bad = make([]byte, XWingCiphertextSize+3)
|
||
if _, err := parseHandshakeResponse(bad); !errors.Is(err, ErrInvalidWireFormat) {
|
||
t.Fatalf("expected ErrInvalidWireFormat, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestSplitRespIDPayloadErrors(t *testing.T) {
|
||
if _, _, err := splitRespIDPayload(nil); err == nil {
|
||
t.Fatal("expected error on empty")
|
||
}
|
||
bad := []byte{0xFF, 0xFF}
|
||
if _, _, err := splitRespIDPayload(bad); err == nil {
|
||
t.Fatal("expected short id read")
|
||
}
|
||
bad = []byte{0, 0, 0xFF, 0xFF}
|
||
if _, _, err := splitRespIDPayload(bad); err == nil {
|
||
t.Fatal("expected short sig read")
|
||
}
|
||
bad = []byte{0, 1, 0xAA, 0, 1, 0xBB, 0xCC}
|
||
if _, _, err := splitRespIDPayload(bad); !errors.Is(err, ErrInvalidWireFormat) {
|
||
t.Fatalf("expected ErrInvalidWireFormat, got %v", err)
|
||
}
|
||
}
|
||
|
||
// ─── handshake error paths ──────────────────────────────────────────
|
||
|
||
func TestRunInitiatorMissingConfig(t *testing.T) {
|
||
// nil config
|
||
if _, err := runInitiator(nopRW{}, nil); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
if _, err := runInitiator(nopRW{}, &Config{}); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestRunResponderMissingConfig(t *testing.T) {
|
||
if _, err := runResponder(nopRW{}, nil); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
if _, err := runResponder(nopRW{}, &Config{}); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
}
|
||
|
||
type nopRW struct{}
|
||
|
||
func (nopRW) Read(p []byte) (int, error) { return 0, io.EOF }
|
||
func (nopRW) Write(p []byte) (int, error) { return len(p), nil }
|
||
|
||
// controlledRW lets a test inject failures at specific I/O steps. It
|
||
// wraps a real net.Conn but can return a chosen error on the Nth read
|
||
// or write call.
|
||
type controlledRW struct {
|
||
r io.Reader
|
||
w io.Writer
|
||
failRead int // 1-indexed; 0 = never
|
||
failWrite int
|
||
readN int
|
||
writeN int
|
||
readErr error
|
||
writeErr error
|
||
}
|
||
|
||
func (c *controlledRW) Read(p []byte) (int, error) {
|
||
c.readN++
|
||
if c.failRead != 0 && c.readN == c.failRead {
|
||
return 0, c.readErr
|
||
}
|
||
return c.r.Read(p)
|
||
}
|
||
|
||
func (c *controlledRW) Write(p []byte) (int, error) {
|
||
c.writeN++
|
||
if c.failWrite != 0 && c.writeN == c.failWrite {
|
||
return 0, c.writeErr
|
||
}
|
||
return c.w.Write(p)
|
||
}
|
||
|
||
func TestRunInitiatorWriteFails(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
rw := &controlledRW{
|
||
r: bytes.NewReader(nil),
|
||
w: io.Discard,
|
||
failWrite: 1,
|
||
writeErr: errors.New("write boom"),
|
||
}
|
||
if _, err := runInitiator(rw, &Config{LocalIdentity: clientID}); err == nil {
|
||
t.Fatal("expected write failure to surface")
|
||
}
|
||
}
|
||
|
||
func TestRunInitiatorReadFails(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
// Discard the init bytes the initiator writes; fail the response read.
|
||
rw := &controlledRW{
|
||
r: bytes.NewReader(nil), // empty reader → readFrame fails on header
|
||
w: io.Discard,
|
||
}
|
||
if _, err := runInitiator(rw, &Config{LocalIdentity: clientID}); err == nil {
|
||
t.Fatal("expected response readFrame failure")
|
||
}
|
||
}
|
||
|
||
func TestRunInitiatorBadResponseParse(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
// Frame whose payload is too short to be a HandshakeResponse.
|
||
garbage := []byte{0, 0, 0, 4, 'b', 'a', 'd', '!'}
|
||
rw := &controlledRW{
|
||
r: bytes.NewReader(garbage),
|
||
w: io.Discard,
|
||
}
|
||
if _, err := runInitiator(rw, &Config{LocalIdentity: clientID}); !errors.Is(err, ErrInvalidWireFormat) {
|
||
t.Fatalf("expected ErrInvalidWireFormat, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestRunInitiatorDecapFails(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
// Build a HandshakeResponse with a junk ML-KEM ciphertext but a
|
||
// valid-looking X25519 ephemeral. The ML-KEM decap returns implicit
|
||
// rejection (junk shared); the AEAD open over EncryptedID then fails,
|
||
// which is the ErrCiphertextCorrupted branch.
|
||
junkCT := make([]byte, XWingCiphertextSize)
|
||
for i := range junkCT {
|
||
junkCT[i] = 0xCC
|
||
}
|
||
// Replace the X25519 portion with a valid ephemeral pubkey (any 32
|
||
// non-low-order bytes; we generate a fresh one).
|
||
var ephPriv [32]byte
|
||
for i := range ephPriv {
|
||
ephPriv[i] = 0x42
|
||
}
|
||
ephPub, err := curve25519X25519(ephPriv[:], curve25519Basepoint())
|
||
if err != nil {
|
||
t.Fatalf("ephPub: %v", err)
|
||
}
|
||
copy(junkCT[len(junkCT)-32:], ephPub)
|
||
resp := &HandshakeResponse{
|
||
XWingCiphertext: junkCT,
|
||
EncryptedID: []byte("not real ciphertext"),
|
||
}
|
||
respWire := resp.MarshalBinary()
|
||
frame := make([]byte, 4+len(respWire))
|
||
frame[0] = byte(len(respWire) >> 24)
|
||
frame[1] = byte(len(respWire) >> 16)
|
||
frame[2] = byte(len(respWire) >> 8)
|
||
frame[3] = byte(len(respWire))
|
||
copy(frame[4:], respWire)
|
||
rw := &controlledRW{r: bytes.NewReader(frame), w: io.Discard}
|
||
|
||
if _, err := runInitiator(rw, &Config{LocalIdentity: clientID}); !errors.Is(err, ErrCiphertextCorrupted) {
|
||
t.Fatalf("expected ErrCiphertextCorrupted, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestRunResponderReadFails(t *testing.T) {
|
||
_, serverID := newPair(t)
|
||
rw := &controlledRW{
|
||
r: bytes.NewReader(nil), // immediate EOF
|
||
w: io.Discard,
|
||
}
|
||
if _, err := runResponder(rw, &Config{LocalIdentity: serverID}); err == nil {
|
||
t.Fatal("expected init readFrame failure")
|
||
}
|
||
}
|
||
|
||
func TestRunResponderWriteFails(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
|
||
// Build a real HandshakeInit framed and stuff it through the reader.
|
||
idPub := clientID.Public().MarshalBinary()
|
||
sig := clientID.Sign([]byte(hsLabelInit), idPub)
|
||
hi := &HandshakeInit{IdentityPub: idPub, Signature: sig}
|
||
wire := hi.MarshalBinary()
|
||
frame := make([]byte, 4+len(wire))
|
||
frame[0] = byte(len(wire) >> 24)
|
||
frame[1] = byte(len(wire) >> 16)
|
||
frame[2] = byte(len(wire) >> 8)
|
||
frame[3] = byte(len(wire))
|
||
copy(frame[4:], wire)
|
||
|
||
// Fail on the FIRST write so HandshakeResponse send fails.
|
||
rw := &controlledRW{
|
||
r: bytes.NewReader(frame),
|
||
w: io.Discard,
|
||
failWrite: 1,
|
||
writeErr: errors.New("write boom"),
|
||
}
|
||
if _, err := runResponder(rw, &Config{LocalIdentity: serverID}); err == nil {
|
||
t.Fatal("expected response writeFrame failure")
|
||
}
|
||
}
|
||
|
||
func TestServerSidePinning(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
_, otherID := newPair(t)
|
||
|
||
_, _, _, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID},
|
||
&Config{LocalIdentity: serverID, ExpectedRemote: otherID.Public()},
|
||
)
|
||
if !errors.Is(sErr, ErrIdentityMismatch) {
|
||
t.Fatalf("server: want ErrIdentityMismatch, got %v", sErr)
|
||
}
|
||
}
|
||
|
||
func TestInitiatorReceivesGarbageResponse(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
left, right := net.Pipe()
|
||
defer left.Close()
|
||
defer right.Close()
|
||
|
||
doneCh := make(chan error, 1)
|
||
go func() {
|
||
_, err := runInitiator(left, &Config{LocalIdentity: clientID})
|
||
doneCh <- err
|
||
}()
|
||
|
||
// Drain the init frame.
|
||
if _, err := readFrame(right); err != nil {
|
||
t.Fatalf("read init: %v", err)
|
||
}
|
||
// Send garbage that's the right size for parseHandshakeResponse but
|
||
// fails X-Wing decap.
|
||
resp := &HandshakeResponse{
|
||
XWingCiphertext: make([]byte, XWingCiphertextSize),
|
||
EncryptedID: []byte("garbage"),
|
||
}
|
||
// Make the ciphertext look "valid-shape" so it gets to decap; decap
|
||
// should yield a junk shared, then AEAD open over the junk fails.
|
||
if err := writeFrame(right, resp.MarshalBinary()); err != nil {
|
||
t.Fatalf("write resp: %v", err)
|
||
}
|
||
|
||
select {
|
||
case err := <-doneCh:
|
||
if err == nil {
|
||
t.Fatal("expected initiator failure")
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("initiator hung")
|
||
}
|
||
}
|
||
|
||
func TestResponderReceivesGarbageInit(t *testing.T) {
|
||
_, serverID := newPair(t)
|
||
left, right := net.Pipe()
|
||
defer left.Close()
|
||
defer right.Close()
|
||
|
||
doneCh := make(chan error, 1)
|
||
go func() {
|
||
_, err := runResponder(right, &Config{LocalIdentity: serverID})
|
||
doneCh <- err
|
||
}()
|
||
|
||
// Send a frame whose contents fail parseHandshakeInit.
|
||
if err := writeFrame(left, []byte{0xFF, 0xFF}); err != nil {
|
||
t.Fatalf("write garbage: %v", err)
|
||
}
|
||
|
||
select {
|
||
case err := <-doneCh:
|
||
if err == nil {
|
||
t.Fatal("expected responder failure")
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("responder hung")
|
||
}
|
||
}
|
||
|
||
func TestResponderInvalidIdentityPub(t *testing.T) {
|
||
_, serverID := newPair(t)
|
||
left, right := net.Pipe()
|
||
defer left.Close()
|
||
defer right.Close()
|
||
|
||
doneCh := make(chan error, 1)
|
||
go func() {
|
||
_, err := runResponder(right, &Config{LocalIdentity: serverID})
|
||
doneCh <- err
|
||
}()
|
||
|
||
// Send a HandshakeInit whose ID is the right size but garbage.
|
||
garbage := make([]byte, IdentityPublicSize)
|
||
for i := range garbage {
|
||
garbage[i] = 0xFF
|
||
}
|
||
hi := &HandshakeInit{IdentityPub: garbage, Signature: make([]byte, ed25519.SignatureSize+mldsa.MLDSA65SignatureSize)}
|
||
if err := writeFrame(left, hi.MarshalBinary()); err != nil {
|
||
t.Fatalf("write garbage init: %v", err)
|
||
}
|
||
|
||
select {
|
||
case err := <-doneCh:
|
||
if err == nil {
|
||
t.Fatal("expected responder failure on garbage identity")
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("responder hung")
|
||
}
|
||
}
|
||
|
||
func TestResponderBadSignature(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
left, right := net.Pipe()
|
||
defer left.Close()
|
||
defer right.Close()
|
||
|
||
doneCh := make(chan error, 1)
|
||
go func() {
|
||
_, err := runResponder(right, &Config{LocalIdentity: serverID})
|
||
doneCh <- err
|
||
}()
|
||
|
||
idPub := clientID.Public().MarshalBinary()
|
||
// Build a syntactically valid sig over a different message so verify fails.
|
||
sig := clientID.Sign([]byte("wrong"), idPub)
|
||
hi := &HandshakeInit{IdentityPub: idPub, Signature: sig}
|
||
if err := writeFrame(left, hi.MarshalBinary()); err != nil {
|
||
t.Fatalf("write: %v", err)
|
||
}
|
||
|
||
select {
|
||
case err := <-doneCh:
|
||
if !errors.Is(err, ErrSignatureInvalid) {
|
||
t.Fatalf("expected ErrSignatureInvalid, got %v", err)
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("responder hung")
|
||
}
|
||
}
|
||
|
||
// ─── listen.go / dial.go ─────────────────────────────────────────────
|
||
|
||
func TestListenMissingConfig(t *testing.T) {
|
||
if _, err := Listen(":0", nil); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
if _, err := Listen(":0", &Config{}); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestListenBadAddr(t *testing.T) {
|
||
id, _ := GenerateIdentity()
|
||
if _, err := Listen("not-a-valid-host:99999999", &Config{LocalIdentity: id}); err == nil {
|
||
t.Fatal("expected listen failure on bogus addr")
|
||
}
|
||
}
|
||
|
||
func TestDialMissingConfig(t *testing.T) {
|
||
if _, err := Dial(context.Background(), "127.0.0.1:0", nil); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
if _, err := Dial(context.Background(), "127.0.0.1:0", &Config{}); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestDialUnreachable(t *testing.T) {
|
||
id, _ := GenerateIdentity()
|
||
// Port 1 is privileged and unlikely to accept; expect dial error.
|
||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||
defer cancel()
|
||
if _, err := Dial(ctx, "127.0.0.1:1", &Config{LocalIdentity: id}); err == nil {
|
||
t.Fatal("expected dial failure")
|
||
}
|
||
}
|
||
|
||
func TestAcceptHandshakeFails(t *testing.T) {
|
||
_, serverID := newPair(t)
|
||
ln, err := Listen("127.0.0.1:0", &Config{LocalIdentity: serverID})
|
||
if err != nil {
|
||
t.Fatalf("listen: %v", err)
|
||
}
|
||
defer ln.Close()
|
||
|
||
doneCh := make(chan error, 1)
|
||
go func() {
|
||
_, err := ln.Accept()
|
||
doneCh <- err
|
||
}()
|
||
|
||
conn, err := net.Dial("tcp", ln.Addr().String())
|
||
if err != nil {
|
||
t.Fatalf("dial: %v", err)
|
||
}
|
||
// Send garbage so the server-side handshake fails.
|
||
conn.Write([]byte{0, 0, 0, 8, 'g', 'a', 'r', 'b', 'a', 'g', 'e', '!'})
|
||
conn.Close()
|
||
|
||
select {
|
||
case err := <-doneCh:
|
||
if err == nil {
|
||
t.Fatal("expected handshake failure")
|
||
}
|
||
case <-time.After(2 * time.Second):
|
||
t.Fatal("Accept did not return")
|
||
}
|
||
}
|
||
|
||
func TestAcceptUnderlyingError(t *testing.T) {
|
||
_, serverID := newPair(t)
|
||
ln, err := Listen("127.0.0.1:0", &Config{LocalIdentity: serverID})
|
||
if err != nil {
|
||
t.Fatalf("listen: %v", err)
|
||
}
|
||
if err := ln.Close(); err != nil {
|
||
t.Fatalf("close: %v", err)
|
||
}
|
||
if _, err := ln.Accept(); err == nil {
|
||
t.Fatal("expected error after close")
|
||
}
|
||
}
|
||
|
||
// ─── channel.go ──────────────────────────────────────────────────────
|
||
|
||
func TestChannelMaxFrameSplit(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
cConn, sConn, cErr, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID},
|
||
&Config{LocalIdentity: serverID},
|
||
)
|
||
if cErr != nil || sErr != nil {
|
||
t.Fatalf("handshake: %v / %v", cErr, sErr)
|
||
}
|
||
defer cConn.Close()
|
||
defer sConn.Close()
|
||
|
||
// Write one-and-a-bit frames worth of plaintext to force the split path.
|
||
const sz = (MaxFrameSize - 16) + 100 // chacha20poly1305.Overhead = 16
|
||
payload := bytes.Repeat([]byte("Q"), sz)
|
||
|
||
wErr := make(chan error, 1)
|
||
go func() { _, e := cConn.Write(payload); wErr <- e }()
|
||
|
||
got := make([]byte, 0, sz)
|
||
buf := make([]byte, sz)
|
||
for len(got) < sz {
|
||
n, err := sConn.Read(buf)
|
||
if err != nil {
|
||
t.Fatalf("read: %v", err)
|
||
}
|
||
got = append(got, buf[:n]...)
|
||
}
|
||
if err := <-wErr; err != nil {
|
||
t.Fatalf("write: %v", err)
|
||
}
|
||
if !bytes.Equal(got, payload) {
|
||
t.Fatal("split-frame payload corrupted")
|
||
}
|
||
}
|
||
|
||
func TestChannelReadOverflowBuffer(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
cConn, sConn, cErr, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID},
|
||
&Config{LocalIdentity: serverID},
|
||
)
|
||
if cErr != nil || sErr != nil {
|
||
t.Fatalf("handshake: %v / %v", cErr, sErr)
|
||
}
|
||
defer cConn.Close()
|
||
defer sConn.Close()
|
||
|
||
payload := []byte("0123456789")
|
||
wErr := make(chan error, 1)
|
||
go func() { _, e := cConn.Write(payload); wErr <- e }()
|
||
|
||
// Read into a smaller buffer so overflow goes through readBuf.
|
||
small := make([]byte, 4)
|
||
n, err := sConn.Read(small)
|
||
if err != nil {
|
||
t.Fatalf("read1: %v", err)
|
||
}
|
||
if n != 4 || string(small) != "0123" {
|
||
t.Fatalf("read1 got %q", small[:n])
|
||
}
|
||
if err := <-wErr; err != nil {
|
||
t.Fatalf("write: %v", err)
|
||
}
|
||
// Subsequent reads must serve the buffered remainder.
|
||
rest := make([]byte, 6)
|
||
n2, err := sConn.Read(rest)
|
||
if err != nil {
|
||
t.Fatalf("read2: %v", err)
|
||
}
|
||
if n2 != 6 || string(rest) != "456789" {
|
||
t.Fatalf("read2 got %q", rest[:n2])
|
||
}
|
||
}
|
||
|
||
func TestChannelCorruptedCiphertext(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
left, right := net.Pipe()
|
||
|
||
resCh := make(chan *handshakeResult, 1)
|
||
go func() {
|
||
r, err := runResponder(right, &Config{LocalIdentity: serverID})
|
||
if err != nil {
|
||
t.Errorf("responder: %v", err)
|
||
}
|
||
resCh <- r
|
||
}()
|
||
cRes, err := runInitiator(left, &Config{LocalIdentity: clientID})
|
||
if err != nil {
|
||
t.Fatalf("initiator: %v", err)
|
||
}
|
||
sRes := <-resCh
|
||
|
||
// Build initiator-side conn manually to keep raw access.
|
||
cConn, err := newConn(left, cRes, true)
|
||
if err != nil {
|
||
t.Fatalf("init conn: %v", err)
|
||
}
|
||
|
||
// Manually craft a bad ciphertext frame and send it via the pipe.
|
||
go func() {
|
||
_ = writeFrame(right, bytes.Repeat([]byte{0xFF}, 64))
|
||
}()
|
||
|
||
buf := make([]byte, 1)
|
||
if _, err := cConn.Read(buf); !errors.Is(err, ErrCiphertextCorrupted) {
|
||
t.Fatalf("expected ErrCiphertextCorrupted, got %v", err)
|
||
}
|
||
_ = sRes
|
||
cConn.Close()
|
||
}
|
||
|
||
func TestChannelCloseAfterClose(t *testing.T) {
|
||
c := &Conn{}
|
||
if err := c.Close(); !errors.Is(err, ErrChannelClosed) {
|
||
t.Fatalf("expected ErrChannelClosed, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestChannelDeadlineGetters(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
cConn, sConn, cErr, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID},
|
||
&Config{LocalIdentity: serverID},
|
||
)
|
||
if cErr != nil || sErr != nil {
|
||
t.Fatalf("handshake: %v / %v", cErr, sErr)
|
||
}
|
||
defer cConn.Close()
|
||
defer sConn.Close()
|
||
|
||
now := time.Now().Add(time.Second)
|
||
if err := cConn.SetDeadline(now); err != nil {
|
||
t.Fatalf("SetDeadline: %v", err)
|
||
}
|
||
if err := cConn.SetReadDeadline(now); err != nil {
|
||
t.Fatalf("SetReadDeadline: %v", err)
|
||
}
|
||
if err := cConn.SetWriteDeadline(now); err != nil {
|
||
t.Fatalf("SetWriteDeadline: %v", err)
|
||
}
|
||
_ = cConn.LocalAddr()
|
||
_ = cConn.RemoteAddr()
|
||
}
|
||
|
||
func TestNonceFor(t *testing.T) {
|
||
n := nonceFor(0)
|
||
for _, b := range n {
|
||
if b != 0 {
|
||
t.Fatal("zero seq should produce zero nonce")
|
||
}
|
||
}
|
||
n = nonceFor(1)
|
||
if n[11] != 1 {
|
||
t.Fatalf("seq=1 low byte should be 1, got %x", n[11])
|
||
}
|
||
n = nonceFor(^uint64(0))
|
||
for i := 4; i < 12; i++ {
|
||
if n[i] != 0xFF {
|
||
t.Fatalf("max seq should fill low 8 bytes, got %x", n)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── zap.go integration ─────────────────────────────────────────────
|
||
|
||
func TestDialZAPListenZAP(t *testing.T) {
|
||
_, serverID := newPair(t)
|
||
clientID, _ := newPair(t)
|
||
|
||
ln, err := ListenZAP("127.0.0.1:0", &Config{LocalIdentity: serverID})
|
||
if err != nil {
|
||
t.Fatalf("ListenZAP: %v", err)
|
||
}
|
||
defer ln.Close()
|
||
|
||
// Run a tiny ZAP server.
|
||
srv := zap.NewServer(ln, zap.HandlerFunc(func(_ context.Context, mt zap.MessageType, p []byte) (zap.MessageType, []byte, error) {
|
||
return mt, p, nil
|
||
}))
|
||
srvDone := make(chan error, 1)
|
||
go func() { srvDone <- srv.Serve(context.Background()) }()
|
||
defer func() {
|
||
srv.Close()
|
||
<-srvDone
|
||
}()
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
conn, err := DialZAP(ctx, ln.Addr().String(), &Config{
|
||
LocalIdentity: clientID,
|
||
ExpectedRemote: serverID.Public(),
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("DialZAP: %v", err)
|
||
}
|
||
defer conn.Close()
|
||
|
||
mt, payload, err := conn.Call(ctx, zap.MsgBuildBlock, []byte("ping"))
|
||
if err != nil {
|
||
t.Fatalf("Call: %v", err)
|
||
}
|
||
if mt&zap.MsgResponseFlag == 0 {
|
||
t.Fatal("response flag missing")
|
||
}
|
||
if !bytes.Equal(payload, []byte("ping")) {
|
||
t.Fatalf("payload mismatch: %q", payload)
|
||
}
|
||
}
|
||
|
||
func TestDialZAPFailures(t *testing.T) {
|
||
// Missing identity.
|
||
_, err := DialZAP(context.Background(), "127.0.0.1:1", nil)
|
||
if !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
|
||
// Unreachable.
|
||
id, _ := GenerateIdentity()
|
||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||
defer cancel()
|
||
if _, err := DialZAP(ctx, "127.0.0.1:1", &Config{LocalIdentity: id}); err == nil {
|
||
t.Fatal("expected dial failure")
|
||
}
|
||
|
||
// ListenZAP with missing identity.
|
||
if _, err := ListenZAP(":0", nil); !errors.Is(err, ErrConfigMissingID) {
|
||
t.Fatalf("expected ErrConfigMissingID, got %v", err)
|
||
}
|
||
// ListenZAP with bogus addr.
|
||
if _, err := ListenZAP("not-a-host:99999999", &Config{LocalIdentity: id}); err == nil {
|
||
t.Fatal("expected listen failure")
|
||
}
|
||
}
|
||
|
||
// ─── targeted reachable-branch coverage ─────────────────────────────
|
||
|
||
func TestMustChaCha20PanicsOnBadKey(t *testing.T) {
|
||
defer func() {
|
||
if r := recover(); r == nil {
|
||
t.Fatal("expected panic for wrong-size key")
|
||
}
|
||
}()
|
||
mustChaCha20([]byte{0, 1, 2, 3}) // not 32 bytes
|
||
}
|
||
|
||
func TestReadSequenceExhausted(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
cConn, sConn, cErr, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID},
|
||
&Config{LocalIdentity: serverID},
|
||
)
|
||
if cErr != nil || sErr != nil {
|
||
t.Fatalf("handshake: %v / %v", cErr, sErr)
|
||
}
|
||
defer cConn.Close()
|
||
defer sConn.Close()
|
||
|
||
cc := cConn.(*Conn)
|
||
cc.readSeq = ^uint64(0)
|
||
buf := make([]byte, 1)
|
||
go func() { _, _ = sConn.Write([]byte("x")) }()
|
||
if _, err := cc.Read(buf); !errors.Is(err, ErrSequenceExhausted) {
|
||
t.Fatalf("expected ErrSequenceExhausted, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestWriteSequenceExhausted(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
cConn, sConn, cErr, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID},
|
||
&Config{LocalIdentity: serverID},
|
||
)
|
||
if cErr != nil || sErr != nil {
|
||
t.Fatalf("handshake: %v / %v", cErr, sErr)
|
||
}
|
||
defer cConn.Close()
|
||
defer sConn.Close()
|
||
|
||
cc := cConn.(*Conn)
|
||
cc.writeSeq = ^uint64(0)
|
||
if _, err := cc.Write([]byte("y")); !errors.Is(err, ErrSequenceExhausted) {
|
||
t.Fatalf("expected ErrSequenceExhausted, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestWriteFrameUnderlyingError(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
left, right := net.Pipe()
|
||
|
||
type res struct {
|
||
r *handshakeResult
|
||
e error
|
||
}
|
||
ch := make(chan res, 1)
|
||
go func() {
|
||
r, e := runResponder(right, &Config{LocalIdentity: serverID})
|
||
ch <- res{r, e}
|
||
}()
|
||
cRes, err := runInitiator(left, &Config{LocalIdentity: clientID})
|
||
if err != nil {
|
||
t.Fatalf("init: %v", err)
|
||
}
|
||
<-ch
|
||
cConn, err := newConn(left, cRes, true)
|
||
if err != nil {
|
||
t.Fatalf("newConn: %v", err)
|
||
}
|
||
right.Close()
|
||
if _, err := cConn.Write([]byte("won't go anywhere")); err == nil {
|
||
t.Fatal("expected write failure after pipe close")
|
||
}
|
||
cConn.Close()
|
||
}
|
||
|
||
func TestHandshakeTimeoutSetsDeadline(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
cConn, sConn, cErr, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID, HandshakeTimeout: 5 * time.Second},
|
||
&Config{LocalIdentity: serverID, HandshakeTimeout: 5 * time.Second},
|
||
)
|
||
if cErr != nil || sErr != nil {
|
||
t.Fatalf("handshake: %v / %v", cErr, sErr)
|
||
}
|
||
cConn.Close()
|
||
sConn.Close()
|
||
}
|
||
|
||
func TestEqualMismatchedDSA(t *testing.T) {
|
||
a, _ := GenerateIdentity()
|
||
b, _ := GenerateIdentity()
|
||
pa := a.Public()
|
||
mod := IdentityPublic{
|
||
EdPub: pa.EdPub,
|
||
DSAPub: b.Public().DSAPub,
|
||
XWingPub: pa.XWingPub,
|
||
}
|
||
if pa.Equal(&mod) {
|
||
t.Fatal("DSA mismatch should not equal")
|
||
}
|
||
}
|
||
|
||
func TestEqualMismatchedXWing(t *testing.T) {
|
||
a, _ := GenerateIdentity()
|
||
b, _ := GenerateIdentity()
|
||
pa := a.Public()
|
||
mod := IdentityPublic{
|
||
EdPub: pa.EdPub,
|
||
DSAPub: pa.DSAPub,
|
||
XWingPub: b.Public().XWingPub,
|
||
}
|
||
if pa.Equal(&mod) {
|
||
t.Fatal("XWing mismatch should not equal")
|
||
}
|
||
}
|
||
|
||
func TestGenerateXWingKeyNilReader(t *testing.T) {
|
||
sk, err := GenerateXWingKey(nil)
|
||
if err != nil {
|
||
t.Fatalf("GenerateXWingKey(nil): %v", err)
|
||
}
|
||
if sk == nil || sk.MLKEMPriv == nil {
|
||
t.Fatal("expected populated keypair")
|
||
}
|
||
}
|
||
|
||
func TestXWingDecapsulateLowOrderEph(t *testing.T) {
|
||
id, err := GenerateIdentity()
|
||
if err != nil {
|
||
t.Fatalf("identity: %v", err)
|
||
}
|
||
ct := make([]byte, XWingCiphertextSize)
|
||
for i := range XWingCiphertextSize - 32 {
|
||
ct[i] = 0xCC
|
||
}
|
||
// Trailing 32 bytes are zero → low-order point → ssX errors.
|
||
if _, err := XWingDecapsulate(id.XWing, ct); err == nil {
|
||
t.Fatal("expected XWingDecapsulate to fail on low-order ephemeral")
|
||
}
|
||
}
|
||
|
||
func TestWriteFramePartialFail(t *testing.T) {
|
||
pw := &partialWriter{succeed: 4, err: errors.New("body boom")}
|
||
if err := writeFrame(pw, []byte("payload")); err == nil {
|
||
t.Fatal("expected body write failure")
|
||
}
|
||
}
|
||
|
||
type partialWriter struct {
|
||
succeed int
|
||
written int
|
||
err error
|
||
}
|
||
|
||
func (w *partialWriter) Write(p []byte) (int, error) {
|
||
if w.written < w.succeed {
|
||
room := w.succeed - w.written
|
||
n := min(len(p), room)
|
||
w.written += n
|
||
if n < len(p) {
|
||
return n, w.err
|
||
}
|
||
return n, nil
|
||
}
|
||
return 0, w.err
|
||
}
|
||
|
||
func TestRunInitiatorBadResponseAEADParse(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
ct, shared, err := XWingEncapsulate(nil, clientID.XWing.Public())
|
||
if err != nil {
|
||
t.Fatalf("encap: %v", err)
|
||
}
|
||
idKey := derive(shared[:], []byte("lux.zwing.v1/resp-id"), chacha20KeySize)
|
||
encrypted := aeadSeal(idKey, []byte("zwing-resp-id"), ct, []byte{0xFF, 0xFF})
|
||
|
||
resp := &HandshakeResponse{XWingCiphertext: ct, EncryptedID: encrypted}
|
||
respWire := resp.MarshalBinary()
|
||
frame := make([]byte, 4+len(respWire))
|
||
frame[0] = byte(len(respWire) >> 24)
|
||
frame[1] = byte(len(respWire) >> 16)
|
||
frame[2] = byte(len(respWire) >> 8)
|
||
frame[3] = byte(len(respWire))
|
||
copy(frame[4:], respWire)
|
||
|
||
rw := &controlledRW{r: bytes.NewReader(frame), w: io.Discard}
|
||
if _, err := runInitiator(rw, &Config{LocalIdentity: clientID}); err == nil {
|
||
t.Fatal("expected splitRespIDPayload error")
|
||
}
|
||
}
|
||
|
||
func TestRunInitiatorBadResponseIDParse(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
ct, shared, err := XWingEncapsulate(nil, clientID.XWing.Public())
|
||
if err != nil {
|
||
t.Fatalf("encap: %v", err)
|
||
}
|
||
idKey := derive(shared[:], []byte("lux.zwing.v1/resp-id"), chacha20KeySize)
|
||
plain := []byte{0, 4, 'a', 'b', 'c', 'd', 0, 0}
|
||
encrypted := aeadSeal(idKey, []byte("zwing-resp-id"), ct, plain)
|
||
|
||
resp := &HandshakeResponse{XWingCiphertext: ct, EncryptedID: encrypted}
|
||
respWire := resp.MarshalBinary()
|
||
frame := make([]byte, 4+len(respWire))
|
||
frame[0] = byte(len(respWire) >> 24)
|
||
frame[1] = byte(len(respWire) >> 16)
|
||
frame[2] = byte(len(respWire) >> 8)
|
||
frame[3] = byte(len(respWire))
|
||
copy(frame[4:], respWire)
|
||
|
||
rw := &controlledRW{r: bytes.NewReader(frame), w: io.Discard}
|
||
if _, err := runInitiator(rw, &Config{LocalIdentity: clientID}); err == nil {
|
||
t.Fatal("expected ParseIdentityPublic error")
|
||
}
|
||
}
|
||
|
||
func TestRunInitiatorBadResponseSigVerify(t *testing.T) {
|
||
clientID, _ := newPair(t)
|
||
realServerID, _ := newPair(t)
|
||
otherID, _ := newPair(t)
|
||
|
||
ct, shared, err := XWingEncapsulate(nil, clientID.XWing.Public())
|
||
if err != nil {
|
||
t.Fatalf("encap: %v", err)
|
||
}
|
||
idKey := derive(shared[:], []byte("lux.zwing.v1/resp-id"), chacha20KeySize)
|
||
idPub := realServerID.Public().MarshalBinary()
|
||
transcript := transcriptHash(clientID.Public().MarshalBinary(), ct)
|
||
wrongSig := otherID.Sign([]byte(hsLabelResponse), transcript)
|
||
plain := buildRespIDPayload(idPub, wrongSig)
|
||
encrypted := aeadSeal(idKey, []byte("zwing-resp-id"), ct, plain)
|
||
|
||
resp := &HandshakeResponse{XWingCiphertext: ct, EncryptedID: encrypted}
|
||
respWire := resp.MarshalBinary()
|
||
frame := make([]byte, 4+len(respWire))
|
||
frame[0] = byte(len(respWire) >> 24)
|
||
frame[1] = byte(len(respWire) >> 16)
|
||
frame[2] = byte(len(respWire) >> 8)
|
||
frame[3] = byte(len(respWire))
|
||
copy(frame[4:], respWire)
|
||
|
||
rw := &controlledRW{r: bytes.NewReader(frame), w: io.Discard}
|
||
if _, err := runInitiator(rw, &Config{LocalIdentity: clientID}); !errors.Is(err, ErrSignatureInvalid) {
|
||
t.Fatalf("expected ErrSignatureInvalid, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestRunResponderXWingEncapsulateLowOrder(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
|
||
mutated := *clientID
|
||
mutXW := *clientID.XWing
|
||
mutated.XWing = &mutXW
|
||
for i := range mutXW.X25519Pub {
|
||
mutXW.X25519Pub[i] = 0
|
||
}
|
||
for i := range mutXW.X25519Priv {
|
||
mutXW.X25519Priv[i] = 0
|
||
}
|
||
|
||
idPub := mutated.Public().MarshalBinary()
|
||
sig := mutated.Sign([]byte(hsLabelInit), idPub)
|
||
hi := &HandshakeInit{IdentityPub: idPub, Signature: sig}
|
||
wire := hi.MarshalBinary()
|
||
frame := make([]byte, 4+len(wire))
|
||
frame[0] = byte(len(wire) >> 24)
|
||
frame[1] = byte(len(wire) >> 16)
|
||
frame[2] = byte(len(wire) >> 8)
|
||
frame[3] = byte(len(wire))
|
||
copy(frame[4:], wire)
|
||
|
||
rw := &controlledRW{r: bytes.NewReader(frame), w: io.Discard}
|
||
if _, err := runResponder(rw, &Config{LocalIdentity: serverID}); err == nil {
|
||
t.Fatal("expected XWingEncapsulate failure on low-order pub")
|
||
}
|
||
}
|
||
|
||
func TestSplitRespIDPayloadShortSigLenU16(t *testing.T) {
|
||
if _, _, err := splitRespIDPayload([]byte{0, 0}); !errors.Is(err, ErrShortRead) {
|
||
t.Fatalf("expected ErrShortRead for missing sigLen u16, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestGenerateIdentityFromMLDSAReadFailure(t *testing.T) {
|
||
// 32-byte reader exhausts after ed25519 keygen consumes its seed.
|
||
rdr := newFailingReader(bytes.NewReader(make([]byte, 1024)), 32)
|
||
if _, err := GenerateIdentityFrom(rdr); err == nil {
|
||
t.Fatal("expected ml-dsa keygen failure after 32-byte exhaustion")
|
||
}
|
||
}
|
||
|
||
// ─── upstream-defensive seam tests ──────────────────────────────────
|
||
|
||
// TestParseTestableDSAPubErrorSeam swaps the parseTestableDSAPub seam to
|
||
// force an error and exercise the defensive branch in
|
||
// ParseIdentityPublic. The seam exists exactly because the upstream
|
||
// constructor only fails on size mismatches that ParseIdentityPublic
|
||
// already rejects.
|
||
func TestParseTestableDSAPubErrorSeam(t *testing.T) {
|
||
orig := parseTestableDSAPub
|
||
defer func() { parseTestableDSAPub = orig }()
|
||
|
||
parseTestableDSAPub = func(b []byte) (*mldsa.PublicKey, error) {
|
||
return nil, errors.New("seam: forced ml-dsa parse error")
|
||
}
|
||
|
||
id, _ := GenerateIdentity()
|
||
bytes := id.Public().MarshalBinary()
|
||
if _, err := ParseIdentityPublic(bytes); err == nil {
|
||
t.Fatal("expected forced error from seam")
|
||
}
|
||
}
|
||
|
||
// TestDecapsulateMLKEMErrorSeam swaps the decapsulateMLKEM seam to drive
|
||
// the defensive ML-KEM Decapsulate error branch.
|
||
func TestDecapsulateMLKEMErrorSeam(t *testing.T) {
|
||
orig := decapsulateMLKEM
|
||
defer func() { decapsulateMLKEM = orig }()
|
||
|
||
decapsulateMLKEM = func(*XWingPrivateKey, []byte) ([]byte, error) {
|
||
return nil, errors.New("seam: forced ml-kem decap error")
|
||
}
|
||
|
||
id, _ := GenerateIdentity()
|
||
ct := make([]byte, XWingCiphertextSize)
|
||
if _, err := XWingDecapsulate(id.XWing, ct); err == nil {
|
||
t.Fatal("expected forced error from seam")
|
||
}
|
||
}
|
||
|
||
// ─── concurrency smoke ──────────────────────────────────────────────
|
||
|
||
func TestConcurrentReadsAreSerialized(t *testing.T) {
|
||
clientID, serverID := newPair(t)
|
||
cConn, sConn, cErr, sErr := runHandshakeOverPipe(t,
|
||
&Config{LocalIdentity: clientID},
|
||
&Config{LocalIdentity: serverID},
|
||
)
|
||
if cErr != nil || sErr != nil {
|
||
t.Fatalf("handshake: %v / %v", cErr, sErr)
|
||
}
|
||
defer cConn.Close()
|
||
defer sConn.Close()
|
||
|
||
// 8 concurrent writes from server, 8 concurrent reads on client.
|
||
const N = 8
|
||
var wg sync.WaitGroup
|
||
for range N {
|
||
wg.Go(func() {
|
||
_, _ = sConn.Write([]byte("z"))
|
||
})
|
||
}
|
||
got := 0
|
||
for got < N {
|
||
buf := make([]byte, 1)
|
||
n, err := cConn.Read(buf)
|
||
if err != nil {
|
||
t.Fatalf("read: %v", err)
|
||
}
|
||
got += n
|
||
}
|
||
wg.Wait()
|
||
}
|