diff --git a/api/firmware/device.go b/api/firmware/device.go index 2408507..8131c89 100644 --- a/api/firmware/device.go +++ b/api/firmware/device.go @@ -276,6 +276,10 @@ func (device *Device) Init() error { return nil } + if err := device.resetSession(); err != nil { + return err + } + attestation, err := device.performAttestation() if err != nil { return err diff --git a/api/firmware/device_test.go b/api/firmware/device_test.go index 945a225..8f828b2 100644 --- a/api/firmware/device_test.go +++ b/api/firmware/device_test.go @@ -209,6 +209,9 @@ func runSimulator(filename string) (func() error, *Device, *simulatorStdout, err ) return func() error { connErr := conn.Close() + if errors.Is(connErr, net.ErrClosed) { + connErr = nil + } killErr := cmd.Process.Kill() <-scannerDone _ = cmd.Wait() @@ -377,6 +380,67 @@ func testInitializedSimulators(t *testing.T, run func(*testing.T, *Device, *simu }) } +// Records real simulator responses without replacing the transport or firmware behavior. +type recordingCommunication struct { + Communication + + attestationResponse []byte +} + +func (communication *recordingCommunication) Query(request []byte) ([]byte, error) { + response, err := communication.Communication.Query(request) + if bytes.HasPrefix(request, []byte(hwwReqNew+opAttestation)) { + communication.attestationResponse = response + } + return response, err +} + +func TestSimulatorReconnect(t *testing.T) { + testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *simulatorStdout) { + t.Helper() + if device.version.AtLeast(semver.NewSemVer(9, 28, 0)) { + // Leave the signing workflow waiting for its next request when the host disconnects. + response, err := device.query(&messages.Request{ + Request: &messages.Request_BtcSignInit{ + BtcSignInit: &messages.BTCSignInitRequest{ + Coin: messages.BTCCoin_BTC, + ScriptConfigs: []*messages.BTCScriptConfigWithKeypath{{ + ScriptConfig: NewBTCScriptConfigSimple(messages.BTCScriptConfig_P2WPKH), + Keypath: []uint32{84 + 0x80000000, 0x80000000, 0x80000000}, + }}, + Version: 2, + NumInputs: 1, + NumOutputs: 1, + }, + }, + }) + require.NoError(t, err) + next, ok := response.Response.(*messages.Response_BtcSignNext) + require.True(t, ok) + require.Equal(t, messages.BTCSignNextResponse_INPUT, next.BtcSignNext.Type) + } + device.Close() + + // Reconnect to the same running simulator, preserving the firmware session state. + conn, err := net.DialTimeout("tcp", "localhost:15423", time.Second) + require.NoError(t, err) + require.NoError(t, conn.SetDeadline(time.Now().Add(15*time.Second))) + communication := &recordingCommunication{ + Communication: u2fhid.NewCommunication(conn, 0xc1), + } + reconnected := NewDevice(nil, nil, device.config, communication, device.log) + defer reconnected.Close() + require.NoError(t, reconnected.Init()) + // The simulator has no attestation certificate, so attestation returns FAILURE. + // An unfinished workflow would instead return its encrypted error, which Init ignores. + require.Equal(t, []byte{0x00, 0x01}, communication.attestationResponse) + reconnected.ChannelHashVerify(true) + fp, err := reconnected.RootFingerprint() + require.NoError(t, err) + require.Equal(t, "4c00739d", hex.EncodeToString(fp)) + }) +} + func TestSimulatorRootFingerprint(t *testing.T) { testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *simulatorStdout) { t.Helper() diff --git a/api/firmware/query.go b/api/firmware/query.go index bb21fb2..1ee8e19 100644 --- a/api/firmware/query.go +++ b/api/firmware/query.go @@ -25,6 +25,8 @@ const ( hwwReqRetry = "\x01" // Cancel any outstanding request. // hwwReqCancel = "\x02" + // Reset the previous session before starting a new one (since v9.28.0). + hwwReqReset = "\x03" // INFO api call (used to be OP_INFO api call), graduated to the toplevel framing so it works // the same way for all firmware versions. hwwInfo = "i" @@ -41,6 +43,27 @@ const ( hwwRspNack = "\x03" ) +func (device *Device) resetSession() error { + if !device.version.AtLeast(semver.NewSemVer(9, 28, 0)) { + return nil + } + // Send at the framing layer so an unfinished workflow cannot consume the request. + for { + response, err := device.communication.Query([]byte(hwwReqReset)) + if err != nil { + return err + } + switch string(response) { + case hwwRspAck: + return nil + case hwwRspBusy: + time.Sleep(time.Second) + default: + return errp.New("unexpected session reset response") + } + } +} + func (device *Device) rawQueryV7(msg []byte) ([]byte, error) { var status string var payload []byte