143 lines
6.1 KiB
Go
143 lines
6.1 KiB
Go
package onboarding
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/rand"
|
||
|
|
"crypto/rsa"
|
||
|
|
"crypto/sha256"
|
||
|
|
"crypto/x509"
|
||
|
|
"encoding/base64"
|
||
|
|
"encoding/json"
|
||
|
|
"encoding/pem"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestIntegrationCredentialRotationAndReboot(t *testing.T) {
|
||
|
|
f := newFixture(t)
|
||
|
|
f.pending()
|
||
|
|
rotation := RotationInput{Reason: "test rotation", ExpectedVersion: 1}
|
||
|
|
req := uuid.NewString()
|
||
|
|
rotated, err := f.s.Rotate(f.ctx, f.p, f.tenantPublic, f.d.PublicID, req, req, requestJSON(rotation), rotation)
|
||
|
|
must(t, err)
|
||
|
|
_, err = f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 2)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("pending key became active")
|
||
|
|
}
|
||
|
|
receiver, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
|
|
must(t, err)
|
||
|
|
pub, err := x509.MarshalPKIXPublicKey(&receiver.PublicKey)
|
||
|
|
must(t, err)
|
||
|
|
job := rotated["enrollment_job_id"].(string)
|
||
|
|
input := DeliveryInput{JobID: job, ReceiverKey: string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pub}))}
|
||
|
|
first, err := f.s.DeliverCredential(f.ctx, f.p, f.tenantPublic, f.d.PublicID, uuid.NewString(), input)
|
||
|
|
must(t, err)
|
||
|
|
_, err = f.s.pool.Exec(f.ctx, `UPDATE device_enrollment_job SET delivery_expires_at=now()-interval '1 second' WHERE id=$1::uuid`, job)
|
||
|
|
must(t, err)
|
||
|
|
renewed, err := f.s.DeliverCredential(f.ctx, f.p, f.tenantPublic, f.d.PublicID, uuid.NewString(), input)
|
||
|
|
must(t, err)
|
||
|
|
if first["ciphertext_b64"] == renewed["ciphertext_b64"] {
|
||
|
|
t.Fatal("expired envelope reused")
|
||
|
|
}
|
||
|
|
release := ReleaseInput{JobID: job, ExpectedVersion: 2, Evidence: "fixture readback"}
|
||
|
|
_, err = f.s.ReleaseInventory(f.ctx, f.p, f.tenantPublic, f.d.PublicID, uuid.NewString(), release)
|
||
|
|
must(t, err)
|
||
|
|
_, err = f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 1)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("old key still active")
|
||
|
|
}
|
||
|
|
f.d, err = f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 2)
|
||
|
|
must(t, err)
|
||
|
|
boot := uuid.NewString()
|
||
|
|
challenge, err := f.s.TimeChallenge(f.ctx, TimeChallengeInput{DeviceID: f.d.FactoryID, KeyVersion: 2, BootID: boot, ClientNonce: randomToken(16)})
|
||
|
|
must(t, err)
|
||
|
|
raw, err := base64.RawURLEncoding.DecodeString(challenge["payload_b64"].(string))
|
||
|
|
must(t, err)
|
||
|
|
var packet map[string]any
|
||
|
|
must(t, json.Unmarshal(raw, &packet))
|
||
|
|
reg := RegisterInput{ChallengeID: packet["challenge_id"].(string), BootID: boot, Model: f.d.Model, HardwareRevision: f.d.HardwareRevision, FirmwareVersion: "1.2", SDKVersion: "1.0"}
|
||
|
|
_, err = f.s.Register(f.ctx, f.d, uuid.NewString(), requestJSON(reg), reg)
|
||
|
|
must(t, err)
|
||
|
|
f.d, err = f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 2)
|
||
|
|
must(t, err)
|
||
|
|
_, err = f.s.Confirm(f.ctx, f.d, uuid.NewString(), requestJSON(f.confirm), f.confirm)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("pre-reboot confirmation accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
func TestIntegrationRegistrationSurvivesRebindAndRejectsOldResult(t *testing.T) {
|
||
|
|
f := newFixture(t)
|
||
|
|
f.bound()
|
||
|
|
j := f.registerSDK()
|
||
|
|
var status string
|
||
|
|
_, err := f.s.pool.Exec(f.ctx, `UPDATE device_binding SET unbound_at=now() WHERE device_id=$1::uuid AND unbound_at IS NULL`, f.d.ID)
|
||
|
|
must(t, err)
|
||
|
|
current, err := f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 1)
|
||
|
|
must(t, err)
|
||
|
|
late := ResultInput{ActivationID: j.ID, AttemptID: j.AttemptID, Stage: "SDK_REGISTER", Outcome: "SUCCESS_OBSERVED"}
|
||
|
|
_, err = f.s.ActivationResult(f.ctx, current, uuid.NewString(), requestJSON(late), late)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("old binding result was accepted")
|
||
|
|
}
|
||
|
|
f.d, err = f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 1)
|
||
|
|
must(t, err)
|
||
|
|
setupIn := SetupInput{BootID: f.d.BootID, LocalSetupID: uuid.NewString(), Transport: "LOCAL_WEB"}
|
||
|
|
setup, err := f.s.Setup(f.ctx, f.d, uuid.NewString(), requestJSON(setupIn), setupIn)
|
||
|
|
must(t, err)
|
||
|
|
f.setup = setup["setup_id"].(string)
|
||
|
|
verifier := randomToken(32)
|
||
|
|
sum := sha256.Sum256([]byte(verifier))
|
||
|
|
handoffIn := HandoffInput{SetupID: f.setup, LocalID: setupIn.LocalSetupID, Challenge: encode64(sum[:])}
|
||
|
|
handoff, err := f.s.Handoff(f.ctx, f.d, uuid.NewString(), requestJSON(handoffIn), handoffIn)
|
||
|
|
must(t, err)
|
||
|
|
browser, err := f.s.Exchange(f.ctx, ExchangeInput{Token: handoff["handoff_token"].(string), Verifier: verifier})
|
||
|
|
must(t, err)
|
||
|
|
f.web, err = f.s.webSession(f.ctx, f.s.pool, digest([]byte(browser.Token)))
|
||
|
|
must(t, err)
|
||
|
|
browser, err = f.s.AuthLink(f.ctx, f.web, f.user)
|
||
|
|
must(t, err)
|
||
|
|
f.web, err = f.s.webSession(f.ctx, f.s.pool, digest([]byte(browser.Token)))
|
||
|
|
must(t, err)
|
||
|
|
f.bound()
|
||
|
|
must(t, f.s.ProcessActivation(f.ctx, f.event()))
|
||
|
|
if f.registrar.calls.Load() != 0 {
|
||
|
|
t.Fatal("rebind repeated supplier registration")
|
||
|
|
}
|
||
|
|
must(t, f.s.pool.QueryRow(f.ctx, `SELECT status FROM device_activation_job WHERE device_id=$1::uuid`, f.d.ID).Scan(&status))
|
||
|
|
if status != "REGISTERED_OBSERVED" {
|
||
|
|
t.Fatal("rebind did not resume activation")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
func TestIntegrationCloseRequiresDurableACKAndGrace(t *testing.T) {
|
||
|
|
f := newFixture(t)
|
||
|
|
f.bound()
|
||
|
|
poll, err := f.s.Poll(f.ctx, f.d, PollInput{SetupID: f.setup, BootID: f.d.BootID})
|
||
|
|
must(t, err)
|
||
|
|
st := poll["state"].(State)
|
||
|
|
ack := StateAckInput{Revision: st.Revision, Generation: st.Generation, Hash: st.Hash, LocalGeneration: 1}
|
||
|
|
_, err = f.s.StateAck(f.ctx, f.d, uuid.NewString(), requestJSON(ack), ack)
|
||
|
|
must(t, err)
|
||
|
|
var saved time.Time
|
||
|
|
must(t, f.s.pool.QueryRow(f.ctx, `SELECT accepted_at FROM device_state_ack WHERE device_id=$1::uuid`, f.d.ID).Scan(&saved))
|
||
|
|
_, err = f.s.StateAck(f.ctx, f.d, uuid.NewString(), requestJSON(ack), ack)
|
||
|
|
must(t, err)
|
||
|
|
var again time.Time
|
||
|
|
must(t, f.s.pool.QueryRow(f.ctx, `SELECT accepted_at FROM device_state_ack WHERE device_id=$1::uuid`, f.d.ID).Scan(&again))
|
||
|
|
if !again.Equal(saved) {
|
||
|
|
t.Fatal("ACK retry extended close grace")
|
||
|
|
}
|
||
|
|
closeIn := CloseInput{SetupID: f.setup, Reason: "SUCCESS", Revision: st.Revision}
|
||
|
|
_, err = f.s.Close(f.ctx, f.d, uuid.NewString(), requestJSON(closeIn), closeIn)
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("closed before 30 second grace")
|
||
|
|
}
|
||
|
|
var binding string
|
||
|
|
must(t, f.s.pool.QueryRow(f.ctx, `SELECT public_id::text FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL`, f.d.ID).Scan(&binding))
|
||
|
|
webAck := WebAckInput{BindingID: binding, Generation: st.Generation, Revision: st.Revision}
|
||
|
|
_, err = f.s.WebAck(f.ctx, f.web, uuid.NewString(), requestJSON(webAck), webAck)
|
||
|
|
must(t, err)
|
||
|
|
f.s.now = func() time.Time { return time.Now().Add(4 * time.Second) }
|
||
|
|
_, err = f.s.Close(f.ctx, f.d, uuid.NewString(), requestJSON(closeIn), closeIn)
|
||
|
|
must(t, err)
|
||
|
|
}
|