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) })
}