drivers/sesame/registration_test.go
raw ยท 6003 bytes
package sesame
import (
"bytes"
"context"
"crypto/ecdh"
"crypto/rand"
"encoding/hex"
"errors"
"sync/atomic"
"testing"
"time"
)
func waitRegistration(t *testing.T, p *RegistrationConnection, phase string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if p.State().Phase == phase {
return
}
time.Sleep(time.Millisecond)
}
t.Fatalf("registration phase = %+v, want %s", p.State(), phase)
}
func linkClosed(f *fakeLink) bool { f.mu.Lock(); defer f.mu.Unlock(); return f.closed }
func TestPreparedRegistrationWaitsAndHandsOffOriginalSession(t *testing.T) {
f := newLink()
device, err := ecdh.P256().GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
var peer *sessionCipher
var registrations, logins, dials atomic.Int32
var expectedSecret string
f.write = func(kind byte, payload []byte) error {
if kind == 1 {
if payload[0] == 2 {
logins.Add(1)
return errors.New("registration must not login again")
}
if len(payload) != 69 || payload[0] != 1 {
return errors.New("bad registration")
}
registrations.Add(1)
pub, err := ecdh.P256().NewPublicKey(append([]byte{4}, payload[1:65]...))
if err != nil {
return err
}
shared, err := device.ECDH(pub)
if err != nil {
return err
}
expectedSecret = hex.EncodeToString(shared[:16])
peer, _, err = newCipher(shared[:16], unhex("11223344"))
if err != nil {
return err
}
reply := make([]byte, 77)
copy(reply, []byte{0, 0, 0, 128, 45, 0, 18})
copy(reply[7:], []byte{90, 0, 0, 0, 0, 0})
copy(reply[13:], device.PublicKey().Bytes()[1:])
f.emit(1, append([]byte{7, 1, 0}, reply...))
return nil
}
plain, err := peer.decrypt(payload)
if err != nil {
return err
}
if !bytes.Equal(plain, []byte{83, 0, 14}) {
return errors.New("wrong live-session operation")
}
ack, err := peer.encrypt([]byte{7, 83, 0})
if err != nil {
return err
}
f.emit(2, ack)
status, err := peer.encrypt([]byte{8, 81, 0, 0, 0, 128, 46, 0, 16})
if err != nil {
return err
}
f.emit(2, status)
return nil
}
p := PrepareRegistration(context.Background(), testTarget, func(context.Context, Target) (Link, Target, error) { dials.Add(1); return f, testTarget, nil })
defer p.Close()
waitRegistration(t, p, "ready")
if registrations.Load() != 0 || linkClosed(f) {
t.Fatal("acted before Register")
}
saved := ""
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
c, err := p.Register(ctx, func(key Credential) error { saved = key.Secret; return nil })
if err != nil {
t.Fatal(err)
}
defer c.Close()
if saved == "" || saved != expectedSecret {
t.Fatal("credential not persisted")
}
p.Close()
if linkClosed(f) {
t.Fatal("handoff closed the live connection")
}
if state := c.State(); !state.Online || state.Position == nil || *state.Position != 45 || state.LockPosition == nil || *state.LockPosition != 90 {
t.Fatalf("registration state lost: %+v", state)
}
life, stop := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { c.Run(life); close(done) }()
defer func() { stop(); <-done }()
if err = c.Action(ctx, "unlock"); err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(time.Second)
for c.State().Position != nil && *c.State().Position != 46 && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if c.State().Position == nil || *c.State().Position != 46 {
t.Fatal("live notifications not handed off")
}
if dials.Load() != 1 || registrations.Load() != 1 || logins.Load() != 0 {
t.Fatal("unexpected reconnect, replay or login")
}
}
func TestPreparedRegistrationCannotRegisterBeforeInitialAndClosesOnCancel(t *testing.T) {
f := newLink()
<-f.packets
var writes atomic.Int32
f.write = func(byte, []byte) error { writes.Add(1); return nil }
p := PrepareRegistration(context.Background(), testTarget, func(context.Context, Target) (Link, Target, error) { return f, testTarget, nil })
waitRegistration(t, p, "waiting-token")
if _, err := p.Register(context.Background(), func(Credential) error { return nil }); err == nil {
t.Fatal("registered before initial token")
}
p.Close()
if !linkClosed(f) || writes.Load() != 0 {
t.Fatal("cancel did not clean up idle link")
}
}
func TestPreparedRegistrationDisconnectAndTimeoutNeverReplay(t *testing.T) {
for _, lost := range []bool{true, false} {
t.Run(map[bool]string{true: "disconnect", false: "exchange-timeout"}[lost], func(t *testing.T) {
f := newLink()
var writes atomic.Int32
f.write = func(byte, []byte) error { writes.Add(1); return nil }
p := PrepareRegistration(context.Background(), testTarget, func(context.Context, Target) (Link, Target, error) { return f, testTarget, nil })
defer p.Close()
waitRegistration(t, p, "ready")
if lost {
f.done <- errors.New("lost link")
waitRegistration(t, p, "disconnected")
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
_, err := p.Register(ctx, func(Credential) error { t.Fatal("persisted without response"); return nil })
if err == nil {
t.Fatal("accepted incomplete registration")
}
<-p.done
want := int32(1)
if lost {
want = 0
}
if writes.Load() != want || !linkClosed(f) {
t.Fatal("replayed command or leaked link")
}
if _, err = p.Register(context.Background(), func(Credential) error { return nil }); err == nil {
t.Fatal("reused failed registration")
}
if writes.Load() != want {
t.Fatal("replayed failed registration")
}
})
}
}
func TestPreparedRegistrationCancelsInFlightSetup(t *testing.T) {
started := make(chan struct{})
p := PrepareRegistration(context.Background(), testTarget, func(ctx context.Context, _ Target) (Link, Target, error) {
close(started)
<-ctx.Done()
return nil, Target{}, ctx.Err()
})
<-started
p.Close()
if p.State().Connected {
t.Fatal("connected after setup cancellation")
}
}