penguin/homekey-go

homekey-go

internal/provision/provision_test.go

raw ยท 5211 bytes

package provision

import (
	"bytes"
	"crypto/elliptic"
	"encoding/hex"
	"encoding/json"
	"os"
	"path/filepath"
	"sync"
	"testing"
)

func mustHex(s string) []byte {
	b, e := hex.DecodeString(s)
	if e != nil {
		panic(e)
	}
	return b
}
func request(op, tag byte, inner []byte) []byte {
	return append(TLV(1, []byte{op}), TLV(tag, inner)...)
}
func readerAdd() []byte {
	key := make([]byte, 32)
	key[31] = 1
	b := append(TLV(1, []byte{2}), TLV(2, key)...)
	b = append(b, TLV(3, []byte("reader01"))...)
	return request(2, 6, b)
}
func paired(t *testing.T) (*Store, string, string) {
	t.Helper()
	dir := t.TempDir()
	s, e := Open(dir)
	if e != nil {
		t.Fatal(e)
	}
	pub := bytes.Repeat([]byte{0x42}, 32)
	p, _ := json.Marshal(struct {
		Name       string
		PublicKey  []byte
		Permission byte
	}{"controller", pub, 1})
	if e = s.Set("controller.pairing", p); e != nil {
		t.Fatal(e)
	}
	return s, dir, keyID(pub)
}
func endpointAdd(iid string) []byte {
	x, y := elliptic.P256().ScalarBaseMult([]byte{2})
	pub := elliptic.Marshal(elliptic.P256(), x, y)
	b := append(TLV(1, []byte{2}), TLV(2, pub[1:])...)
	b = append(b, TLV(3, mustHex(iid))...)
	b = append(b, TLV(4, []byte{1})...)
	return request(2, 4, b)
}
func checkResponse(t *testing.T, s *Store, req []byte, want []byte) {
	t.Helper()
	got, e := s.Handle(req)
	if e != nil {
		t.Fatal(e)
	}
	if !bytes.Equal(got, want) {
		t.Fatalf("got %x, want %x", got, want)
	}
}
func TestProvisionAndRestart(t *testing.T) {
	s, dir, iid := paired(t)
	checkResponse(t, s, readerAdd(), mustHex("0703020100"))
	checkResponse(t, s, readerAdd(), mustHex("0703020102"))
	key := make([]byte, 32)
	key[31] = 1
	checkResponse(t, s, request(1, 6, nil), TLV(7, TLV(1, mustHex(keyID(key)))))
	want := TLV(5, append(TLV(2, mustHex(iid)), TLV(3, []byte{Success})...))
	checkResponse(t, s, endpointAdd(iid), want)
	want = TLV(5, append(TLV(2, mustHex(iid)), TLV(3, []byte{Duplicate})...))
	checkResponse(t, s, endpointAdd(iid), want)
	s2, e := Open(dir)
	if e != nil {
		t.Fatal(e)
	}
	if v := s2.Summary(); !v.ReaderProvisioned || v.Endpoints != 1 || v.Issuers != 1 {
		t.Fatalf("bad summary %+v", v)
	}
	info, e := os.Stat(filepath.Join(dir, "state.json"))
	if e != nil {
		t.Fatal(e)
	}
	if info.Mode().Perm() != 0600 {
		t.Fatalf("permissions %o", info.Mode().Perm())
	}
	if e = s2.Delete("controller.pairing"); e != nil {
		t.Fatal(e)
	}
	if v := s2.Summary(); v.ReaderProvisioned || v.Endpoints != 0 || v.Issuers != 0 {
		t.Fatalf("unpair did not clear credentials %+v", v)
	}
	s3, e := Open(dir)
	if e != nil {
		t.Fatal(e)
	}
	if s3.Summary().ReaderProvisioned {
		t.Fatal("revocation not persisted")
	}
}
func TestReaderRemove(t *testing.T) {
	s, _, _ := paired(t)
	checkResponse(t, s, readerAdd(), mustHex("0703020100"))
	key := make([]byte, 32)
	key[31] = 1
	checkResponse(t, s, request(3, 6, TLV(4, mustHex(keyID(key)))), mustHex("0703020100"))
	checkResponse(t, s, request(3, 6, TLV(4, mustHex(keyID(key)))), mustHex("0703020103"))
}
func TestUnknownIssuerAndInvalidKey(t *testing.T) {
	s, _, _ := paired(t)
	checkResponse(t, s, readerAdd(), mustHex("0703020100"))
	checkResponse(t, s, endpointAdd("0000000000000000"), mustHex("0503030103"))
	f, _ := DecodeTLV(readerAdd())
	inner, _ := DecodeTLV(f[6])
	inner[2] = make([]byte, 32)
	b := append(TLV(1, inner[1]), TLV(2, inner[2])...)
	b = append(b, TLV(3, inner[3])...)
	if _, e := s.Handle(request(2, 6, b)); e == nil {
		t.Fatal("zero scalar accepted")
	}
	if !s.Summary().ReaderProvisioned {
		t.Fatal("valid reader key lost")
	}
}
func TestRejectMalformedTLV(t *testing.T) {
	for _, b := range [][]byte{{1}, {1, 2, 0}, {1, 1, 1, 1, 1, 2}, {255, 0}, make([]byte, MaxMessage+1)} {
		if _, e := DecodeTLV(b); e == nil {
			t.Fatalf("accepted malformed input %x", b[:min(8, len(b))])
		}
	}
	for _, n := range []int{0, 1, 254, 255, 256, 510, 511} {
		b := bytes.Repeat([]byte{42}, n)
		f, e := DecodeTLV(TLV(6, b))
		if e != nil {
			t.Fatal(e)
		}
		v, ok := f[6]
		if !ok || !bytes.Equal(v, b) {
			t.Fatal("fragment roundtrip failed")
		}
	}
}
func TestDiskFailureRollsBack(t *testing.T) {
	s, _, _ := paired(t)
	s.path = filepath.Join(t.TempDir(), "missing", "state.json")
	if _, e := s.Handle(readerAdd()); e == nil {
		t.Fatal("disk failure ignored")
	}
	if s.Summary().ReaderProvisioned {
		t.Fatal("memory committed on failed disk write")
	}
}
func TestCorruptStateFailsClosed(t *testing.T) {
	dir := t.TempDir()
	p := filepath.Join(dir, "state.json")
	if e := os.WriteFile(p, []byte("{bad"), 0600); e != nil {
		t.Fatal(e)
	}
	if _, e := Open(dir); e == nil {
		t.Fatal("corrupt state silently reset")
	}
}
func TestConcurrentRequests(t *testing.T) {
	s, _, iid := paired(t)
	checkResponse(t, s, readerAdd(), mustHex("0703020100"))
	var wg sync.WaitGroup
	for i := 0; i < 12; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 5; j++ {
				if _, e := s.Handle(endpointAdd(iid)); e != nil {
					t.Error(e)
				}
				_ = s.Summary()
			}
		}()
	}
	wg.Wait()
	if s.Summary().Endpoints != 1 {
		t.Fatal("duplicate endpoints")
	}
}
func FuzzDecodeTLV(f *testing.F) {
	f.Add([]byte{1, 1, 2, 6, 0})
	f.Add(bytes.Repeat([]byte{1}, 256))
	f.Fuzz(func(t *testing.T, b []byte) { _, _ = DecodeTLV(b) })
}