mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-10-31 18:21:51 +08:00
231 lines
6.0 KiB
Go
231 lines
6.0 KiB
Go
package wafsec
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
func sealStream(t *testing.T, key, plain []byte) []byte {
|
|
t.Helper()
|
|
var out bytes.Buffer
|
|
w, err := NewStreamWriter(&out, key, "task-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = w.Write(plain); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = w.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out.Bytes()
|
|
}
|
|
func TestStreamRoundTripAndRandomization(t *testing.T) {
|
|
key := bytes.Repeat([]byte{3}, 32)
|
|
for _, size := range []int{0, 1, 65536, 65537, 200000} {
|
|
plain := bytes.Repeat([]byte("x"), size)
|
|
sealed := sealStream(t, key, plain)
|
|
if bytes.Equal(sealed, sealStream(t, key, plain)) {
|
|
t.Fatal("streams repeated")
|
|
}
|
|
r, err := NewStreamReader(bytes.NewReader(sealed), key, "task-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := io.ReadAll(r)
|
|
if err != nil || !bytes.Equal(got, plain) {
|
|
t.Fatalf("size %d: %v", size, err)
|
|
}
|
|
}
|
|
}
|
|
func TestStreamAuthenticationFailures(t *testing.T) {
|
|
key := bytes.Repeat([]byte{3}, 32)
|
|
sealed := sealStream(t, key, bytes.Repeat([]byte("x"), 65536*2))
|
|
cases := map[string][]byte{"no-final": sealed[:len(sealed)-20], "short-final": sealed[:len(sealed)-1], "trailing": append(append([]byte(nil), sealed...), 1), "short-header": sealed[:79]}
|
|
altered := append([]byte(nil), sealed...)
|
|
altered[100] ^= 1
|
|
cases["modified"] = altered
|
|
swapped := append([]byte(nil), sealed...)
|
|
record := 65536 + 20
|
|
copy(swapped[80:80+record], sealed[80+record:80+2*record])
|
|
copy(swapped[80+record:80+2*record], sealed[80:80+record])
|
|
cases["reordered"] = swapped
|
|
changedHeader := append([]byte(nil), sealed...)
|
|
changedHeader[10] ^= 1
|
|
cases["header"] = changedHeader
|
|
for name, data := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
r, err := NewStreamReader(bytes.NewReader(data), key, "task-1")
|
|
if err == nil {
|
|
_, err = io.ReadAll(r)
|
|
}
|
|
if !errors.Is(err, ErrStreamAuthentication) {
|
|
t.Fatalf("accepted: %v", err)
|
|
}
|
|
})
|
|
}
|
|
for _, tc := range []struct {
|
|
key []byte
|
|
id string
|
|
}{{bytes.Repeat([]byte{4}, 32), "task-1"}, {key, "task-2"}} {
|
|
r, err := NewStreamReader(bytes.NewReader(sealed), tc.key, tc.id)
|
|
if err == nil {
|
|
_, err = io.ReadAll(r)
|
|
}
|
|
if !errors.Is(err, ErrStreamAuthentication) {
|
|
t.Fatalf("accepted wrong identity/key: %v", err)
|
|
}
|
|
}
|
|
}
|
|
func TestStreamRecordLayoutAndManualOpen(t *testing.T) {
|
|
key := bytes.Repeat([]byte{7}, 32)
|
|
plain := bytes.Repeat([]byte("a"), streamChunkSize+10)
|
|
sealed := sealStream(t, key, plain)
|
|
if len(sealed) < 80 {
|
|
t.Fatal("short stream")
|
|
}
|
|
header := sealed[:80]
|
|
if !bytes.Equal(header[:8], streamMagic) {
|
|
t.Fatal("magic")
|
|
}
|
|
if binary.BigEndian.Uint32(header[44:48]) != streamChunkSize {
|
|
t.Fatal("chunk size")
|
|
}
|
|
digest := sha256.Sum256([]byte("task-1"))
|
|
if !bytes.Equal(header[48:], digest[:]) {
|
|
t.Fatal("context digest")
|
|
}
|
|
aead, err := streamAEAD(key, header[8:40])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
off := 80
|
|
seq := uint64(0)
|
|
var recovered []byte
|
|
for {
|
|
if off+4 > len(sealed) {
|
|
t.Fatal("truncated record framing")
|
|
}
|
|
n := binary.BigEndian.Uint32(sealed[off : off+4])
|
|
off += 4
|
|
if n > streamChunkSize || off+int(n)+aead.Overhead() > len(sealed) {
|
|
t.Fatal("record length out of bounds")
|
|
}
|
|
record := sealed[off : off+int(n)+aead.Overhead()]
|
|
off += len(record)
|
|
opened, err := aead.Open(nil, streamNonce(header, seq), record, streamAAD(header, seq, n))
|
|
if err != nil {
|
|
t.Fatalf("record %d rejected by manual open: %v", seq, err)
|
|
}
|
|
recovered = append(recovered, opened...)
|
|
seq++
|
|
if n == 0 {
|
|
break
|
|
}
|
|
}
|
|
if off != len(sealed) || !bytes.Equal(recovered, plain) {
|
|
t.Fatal("layout round trip")
|
|
}
|
|
if bytes.Equal(streamNonce(header, 0), streamNonce(header, 1)) {
|
|
t.Fatal("nonce reuse")
|
|
}
|
|
}
|
|
|
|
func TestStreamRejectsCorruptLengthField(t *testing.T) {
|
|
key := bytes.Repeat([]byte{3}, 32)
|
|
sealed := sealStream(t, key, bytes.Repeat([]byte("x"), streamChunkSize))
|
|
for _, pos := range []int{80, 81, 82, 83} {
|
|
data := append([]byte(nil), sealed...)
|
|
data[pos] ^= 0xff
|
|
r, err := NewStreamReader(bytes.NewReader(data), key, "task-1")
|
|
if err == nil {
|
|
_, err = io.ReadAll(r)
|
|
}
|
|
if !errors.Is(err, ErrStreamAuthentication) {
|
|
t.Fatalf("pos %d accepted: %v", pos, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStreamErrorIsSticky(t *testing.T) {
|
|
boom := errors.New("boom")
|
|
dst := &failAfterWriter{left: 80, err: boom}
|
|
w, err := NewStreamWriter(dst, bytes.Repeat([]byte{5}, 32), "task-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dst.left = 0
|
|
if _, err = w.Write(make([]byte, streamChunkSize)); !errors.Is(err, boom) {
|
|
t.Fatalf("first error: %v", err)
|
|
}
|
|
dst.left = -1
|
|
if _, err = w.Write([]byte("y")); !errors.Is(err, boom) {
|
|
t.Fatalf("sticky error: %v", err)
|
|
}
|
|
if err = w.Close(); !errors.Is(err, boom) {
|
|
t.Fatalf("close error: %v", err)
|
|
}
|
|
if _, err = w.Write([]byte("z")); err == nil {
|
|
t.Fatal("write after close accepted")
|
|
}
|
|
}
|
|
|
|
type failAfterWriter struct {
|
|
left int
|
|
err error
|
|
}
|
|
|
|
func (w *failAfterWriter) Write(p []byte) (int, error) {
|
|
if w.left < 0 {
|
|
return 0, errors.New("must not be called")
|
|
}
|
|
if w.left == 0 {
|
|
if w.err == nil {
|
|
w.err = errors.New("boom")
|
|
}
|
|
return 0, w.err
|
|
}
|
|
if len(p) > w.left {
|
|
n := w.left
|
|
w.left = 0
|
|
return n, nil
|
|
}
|
|
w.left -= len(p)
|
|
return len(p), nil
|
|
}
|
|
|
|
func TestStreamRejectsMalformedInput(t *testing.T) {
|
|
setupDataKey(t)
|
|
key := bytes.Repeat([]byte{3}, 32)
|
|
if _, err := NewStreamReader(bytes.NewReader(nil), key, "task-1"); !errors.Is(err, ErrStreamAuthentication) {
|
|
t.Fatalf("empty input: %v", err)
|
|
}
|
|
wrapped, err := DataEncrypt(base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{1}, 16)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = UnwrapStreamKey(wrapped); !errors.Is(err, ErrStreamAuthentication) {
|
|
t.Fatalf("short key accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStreamWrappedKey(t *testing.T) {
|
|
setupDataKey(t)
|
|
key, wrapped, err := NewStreamKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := UnwrapStreamKey(wrapped)
|
|
if err != nil || !bytes.Equal(key, got) {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = UnwrapStreamKey("plain"); err == nil {
|
|
t.Fatal("accepted unwrapped key")
|
|
}
|
|
}
|