pax_global_header00006660000000000000000000000064147067010140014512gustar00rootroot0000000000000052 comment=a383c40f94cc4bbc3d526f3b684013f5cb63a8c9 tkeyclient-1.1.0/000077500000000000000000000000001470670101400136645ustar00rootroot00000000000000tkeyclient-1.1.0/.editorconfig000066400000000000000000000000341470670101400163360ustar00rootroot00000000000000[*.md] max_line_length = 70 tkeyclient-1.1.0/.github/000077500000000000000000000000001470670101400152245ustar00rootroot00000000000000tkeyclient-1.1.0/.github/workflows/000077500000000000000000000000001470670101400172615ustar00rootroot00000000000000tkeyclient-1.1.0/.github/workflows/ci.yaml000066400000000000000000000015301470670101400205370ustar00rootroot00000000000000 name: ci on: push: branches: - 'main' pull_request: {} # allow manual runs: workflow_dispatch: {} jobs: ci: runs-on: ubuntu-latest container: image: ghcr.io/tillitis/tkey-builder:4 steps: - name: checkout uses: actions/checkout@v4 with: # fetch-depth: 0 persist-credentials: false - name: fix # https://github.com/actions/runner-images/issues/6775 run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: make run: make - name: check for SPDX tags run: ./tools/spdx-ensure reuse-compliance-check: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: REUSE Compliance Check uses: fsfe/reuse-action@v4 with: args: lint tkeyclient-1.1.0/.github/workflows/golangci-lint.yml000066400000000000000000000037011470670101400225340ustar00rootroot00000000000000name: golangci-lint on: push: branches: - main pull_request: permissions: contents: read # Optional: allow read access to pull request. Use with `only-new-issues` option. # pull-requests: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.21' cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: # Require: The version of golangci-lint to use. # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. version: v1.61.0 # Optional: working directory, useful for monorepos # working-directory: somedir # Optional: golangci-lint command line arguments. # # Note: By default, the `.golangci.yml` file should be at the root of the repository. # The location of the configuration file can be changed by using `--config=` # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true # Optional: if set to true, then all caching functionality will be completely disabled, # takes precedence over all other caching options. # skip-cache: true # Optional: if set to true, then the action won't cache or restore ~/go/pkg. # skip-pkg-cache: true # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. # skip-build-cache: true # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. # install-mode: "goinstall" tkeyclient-1.1.0/.gitignore000066400000000000000000000004331470670101400156540ustar00rootroot00000000000000*.a *.o *.bin *.elf /tkey-ssh-agent /tkey-ssh-agent.exe /cmd/tkey-ssh-agent/rsrc_windows_amd64.syso /tkey-ssh-agent-tray.exe /cmd/tkey-ssh-agent-tray/rsrc_windows_amd64.syso /tkey-runapp /tkey-sign /runsign.sh /runtimer /runrandom /gotools/golangci-lint /gotools/go-winres test/venv tkeyclient-1.1.0/.golangci.yml000066400000000000000000000010711470670101400162470ustar00rootroot00000000000000linters: presets: # found in: golangci-lint help linters - bugs - comment - complexity - error - format - import - metalinter - module - performance - sql # - style # turned off, can be too much - test - unused disable: - cyclop - depguard - funlen - gocognit - nestif - exhaustruct # TODO? annoying for now - err113 # TODO enable later - godot - perfsprint issues: max-issues-per-linter: 0 max-same-issues: 0 linters-settings: govet: enable: - shadow tkeyclient-1.1.0/LICENSE000066400000000000000000000024261470670101400146750ustar00rootroot00000000000000 BSD 2-Clause License Copyright 2022 Tillitis AB Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. tkeyclient-1.1.0/LICENSES/000077500000000000000000000000001470670101400150715ustar00rootroot00000000000000tkeyclient-1.1.0/LICENSES/BSD-2-Clause.txt000066400000000000000000000023761470670101400176230ustar00rootroot00000000000000Copyright 2022 Tillitis AB Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. tkeyclient-1.1.0/Makefile000066400000000000000000000000711470670101400153220ustar00rootroot00000000000000all: go build .PHONY: spdx spdx: ./tools/spdx-ensure tkeyclient-1.1.0/README.md000066400000000000000000000033421470670101400151450ustar00rootroot00000000000000[![ci](https://github.com/tillitis/tkeyclient/actions/workflows/ci.yaml/badge.svg?branch=main&event=push)](https://github.com/tillitis/tkeyclient/actions/workflows/ci.yaml) [![Go Reference](https://pkg.go.dev/badge/github.com/tillitis/tkeyclient.svg)](https://pkg.go.dev/github.com/tillitis/tkeyclient) # Tillitis TKey Client package A Go package for controlling a [Tillitis](https://tillitis.se/) TKey, upload device apps, and communicate with it. See the [Go doc](https://pkg.go.dev/github.com/tillitis/tkeyclient) for `tkeyclient` for details on how to call the functions. See [tkey-ssh-agent](https://github.com/tillitis/tkey-ssh-agent) or [tkeysign](https://github.com/tillitis/tkeysign) for example applications using this Go package. Release notes in [RELEASE.md](RELEASE.md). ## Licenses and SPDX tags Unless otherwise noted, the project sources are copyright Tillitis AB, licensed under the terms and conditions of the "BSD-2-Clause" license. See [LICENSE](LICENSE) for the full license text. Until Oct 7, 2024, the license was GPL-2.0 Only. External source code we have imported are isolated in their own directories. They may be released under other licenses. This is noted with a similar `LICENSE` file in every directory containing imported sources. The project uses single-line references to Unique License Identifiers as defined by the Linux Foundation's [SPDX project](https://spdx.org/) on its own source files, but not necessarily imported files. The line in each individual source file identifies the license applicable to that file. The current set of valid, predefined SPDX identifiers can be found on the SPDX License List at: https://spdx.org/licenses/ We attempt to follow the [REUSE specification](https://reuse.software/). tkeyclient-1.1.0/RELEASE.md000066400000000000000000000020001470670101400152560ustar00rootroot00000000000000# Release notes ## v1.1.0 - Change license to BSD-2-Clause - Follow REUSE specification, see https://reuse.software/ - Return payload even if NOK is set in header - Update max app size to 128 kiB - Reset read timeout to disabled in GetNameVersion - Add function SetReadTimeoutNoErr, to use nicely with defer - Update golanglint-ci - Introduce use of safecast Complete [changelog](https://github.com/tillitis/tkeyclient/compare/v1.0.0...v1.1.0). ## v1.0.0 Going to version 1.0.0 to indicate that tkeyclient is stable and production ready, according to Semantic Versioning. - Bumping go dependencies. - Removing stray dependency go-winres. - Go lint using golangci-lint-action in CI. - Updating README and copyright notice ## v0.0.8 Update dependencies. Updating the serial package to keep tkeyclient buildable on darwin with go 1.21. - go.bug.st/serial v1.6.1 - golang.org/x/crypto v0.13.0 - golang.org/x/sys v0.12.0 ## v0.0.7 Just ripped from https://github.com/tillitis/tillitis-key1-apps No semantic changes. tkeyclient-1.1.0/REUSE.toml000066400000000000000000000010371470670101400154450ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2024 Tillitis AB # SPDX-License-Identifier: BSD-2-Clause version = 1 [[annotations]] path = ".github/workflows/*" SPDX-FileCopyrightText = "2022 Tillitis AB " SPDX-License-Identifier = "BSD-2-Clause" [[annotations]] path = [ ".editorconfig", ".gitignore", ".golangci.yml", "LICENSE", "Makefile", "RELEASE.md", "README.md", "go.mod", "go.sum" ] SPDX-FileCopyrightText = "2022 Tillitis AB " SPDX-License-Identifier = "BSD-2-Clause" tkeyclient-1.1.0/go.mod000066400000000000000000000005011470670101400147660ustar00rootroot00000000000000module github.com/tillitis/tkeyclient go 1.21 toolchain go1.21.1 require ( github.com/ccoveille/go-safecast v1.1.0 go.bug.st/serial v1.6.2 golang.org/x/crypto v0.22.0 ) require ( github.com/creack/goselect v0.1.2 // indirect github.com/stretchr/testify v1.8.0 // indirect golang.org/x/sys v0.19.0 // indirect ) tkeyclient-1.1.0/go.sum000066400000000000000000000040111470670101400150130ustar00rootroot00000000000000github.com/ccoveille/go-safecast v1.1.0 h1:iHKNWaZm+OznO7Eh6EljXPjGfGQsSfa6/sxPlIEKO+g= github.com/ccoveille/go-safecast v1.1.0/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= tkeyclient-1.1.0/ports.go000066400000000000000000000033061470670101400153640ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2022 Tillitis AB // SPDX-License-Identifier: BSD-2-Clause package tkeyclient import ( "fmt" "os" "go.bug.st/serial/enumerator" ) const ( tillitisUSBVID = "1207" tillitisUSBPID = "8887" // Custom errors ErrNoDevice = constError("no TKey connected") ErrManyDevices = constError("more than one TKey connected") ) type SerialPort struct { DevPath string SerialNumber string } // DetectSerialPort tries to detect an inserted TKey and returns the // device path if successful. func DetectSerialPort(verbose bool) (string, error) { ports, err := GetSerialPorts() if err != nil { return "", err } if len(ports) == 0 { if verbose { fmt.Fprintf(os.Stderr, "No TKey serial ports detected.\n") } return "", ErrNoDevice } if len(ports) > 1 { if verbose { fmt.Fprintf(os.Stderr, "Detected %d TKey serial ports:\n", len(ports)) for _, p := range ports { fmt.Fprintf(os.Stderr, "%s with serial number %s\n", p.DevPath, p.SerialNumber) } } return "", ErrManyDevices } if verbose { fmt.Fprintf(os.Stderr, "Auto-detected serial port %s\n", ports[0].DevPath) } return ports[0].DevPath, nil } // GetSerialPorts enumerates any existing TKey serial ports identified // on the system. func GetSerialPorts() ([]SerialPort, error) { var ports []SerialPort portDetails, err := enumerator.GetDetailedPortsList() if err != nil { return nil, fmt.Errorf("GetDetailedPortsList: %w", err) } if len(portDetails) == 0 { return ports, nil } for _, port := range portDetails { if port.IsUSB && port.VID == tillitisUSBVID && port.PID == tillitisUSBPID { ports = append(ports, SerialPort{port.Name, port.SerialNumber}) } } return ports, nil } tkeyclient-1.1.0/proto.go000066400000000000000000000165611470670101400153670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2022 Tillitis AB // SPDX-License-Identifier: BSD-2-Clause package tkeyclient import ( "encoding/hex" "fmt" "io" ) type Endpoint byte const ( // destAFPGA endpoint = 1 DestFW Endpoint = 2 DestApp Endpoint = 3 ) // Length of command data that follows the first 1 byte frame header type CmdLen byte const ( CmdLen1 CmdLen = 0 CmdLen4 CmdLen = 1 CmdLen32 CmdLen = 2 CmdLen128 CmdLen = 3 ) // Bytelen returns the number of bytes corresponding to the specific // CmdLen value. func (l CmdLen) Bytelen() int { switch l { case CmdLen1: return 1 case CmdLen4: return 4 case CmdLen32: return 32 case CmdLen128: return 128 } return 0 } type Cmd interface { Code() byte String() string CmdLen() CmdLen Endpoint() Endpoint } var ( cmdGetNameVersion = fwCmd{0x01, "cmdGetNameVersion", CmdLen1} rspGetNameVersion = fwCmd{0x02, "rspGetNameVersion", CmdLen32} cmdLoadApp = fwCmd{0x03, "cmdLoadApp", CmdLen128} rspLoadApp = fwCmd{0x04, "rspLoadApp", CmdLen4} cmdLoadAppData = fwCmd{0x05, "cmdLoadAppData", CmdLen128} rspLoadAppData = fwCmd{0x06, "rspLoadAppData", CmdLen4} rspLoadAppDataReady = fwCmd{0x07, "rspLoadAppDataReady", CmdLen128} cmdGetUDI = fwCmd{0x08, "cmdGetUDI", CmdLen1} rspGetUDI = fwCmd{0x09, "rspGetUDI", CmdLen32} ) type fwCmd struct { code byte name string cmdLen CmdLen } func (c fwCmd) Code() byte { return c.code } func (c fwCmd) CmdLen() CmdLen { return c.cmdLen } func (c fwCmd) Endpoint() Endpoint { return DestFW } func (c fwCmd) String() string { return c.name } type FramingHdr struct { ID byte Endpoint Endpoint CmdLen CmdLen ResponseNotOK bool } func parseframe(b byte) (FramingHdr, error) { var f FramingHdr if (b & 0b1000_0000) != 0 { return f, fmt.Errorf("reserved bit #7 is not zero") } // If bit #2 is set if (b & 0b0000_0100) != 0 { f.ResponseNotOK = true } f.ID = byte((b & 0b0110_0000) >> 5) f.Endpoint = Endpoint((b & 0b0001_1000) >> 3) f.CmdLen = CmdLen(b & 0b0000_0011) return f, nil } // NewFrameBuf allocates a buffer with the appropriate size for the // command in cmd, including the framing protocol header byte. The cmd // parameter is used to get the endpoint and command length, which // together with id parameter are encoded as the header byte. The // header byte is placed in the first byte in the returned buffer. The // command code from cmd is placed in the buffer's second byte. // // Header byte (used for both command and response frame): // // Bit [7] (1 bit). Reserved - possible protocol version. // // Bits [6..5] (2 bits). Frame ID tag. // // Bits [4..3] (2 bits). Endpoint number: // // 00 == reserved // 01 == HW in application_fpga // 10 == FW in application_fpga // 11 == SW (application) in application_fpga // // Bit [2] (1 bit). Usage: // // Command: Unused. MUST be zero. // Response: 0 == OK, 1 == Not OK (NOK) // // Bits [1..0] (2 bits). Command/Response data length: // // 00 == 1 byte // 01 == 4 bytes // 10 == 32 bytes // 11 == 128 bytes // // Note that the number of bytes indicated by the command data length // field does **not** include the header byte. This means that a // complete command frame, with a header indicating a command length // of 128 bytes, is 128+1 bytes in length. func NewFrameBuf(cmd Cmd, id int) ([]byte, error) { if id > 3 { return nil, fmt.Errorf("frame ID must be 0..3") } if cmd.Endpoint() > 3 { return nil, fmt.Errorf("endpoint must be 0..3") } if cmd.CmdLen() > 3 { return nil, fmt.Errorf("cmdlen must be 0..3") } // Make a buffer with frame header + cmdLen payload tx := make([]byte, 1+cmd.CmdLen().Bytelen()) tx[0] = (byte(id) << 5) | (byte(cmd.Endpoint()) << 3) | byte(cmd.CmdLen()) // Set command code tx[1] = cmd.Code() return tx, nil } // Dump() hexdumps data in d with an explaining string s first. It // expects d to contain the whole frame as sent on the wire, with the // framing protocol header in the first byte. func Dump(s string, d []byte) { if d == nil || len(d) == 0 { le.Printf("%s: no data\n", s) return } hdr, err := parseframe(d[0]) if err != nil { le.Printf("%s (parseframe error: %s):\n", s, err) } else { le.Printf("%s (frame len: 1+%d bytes):\n", s, hdr.CmdLen.Bytelen()) } le.Printf("%s", hex.Dump(d)) } func (tk TillitisKey) Write(d []byte) error { _, err := tk.conn.Write(d) if err != nil { return fmt.Errorf("Write: %w", err) } return nil } type constError string func (err constError) Error() string { return string(err) } const ErrResponseStatusNotOK = constError("response status not OK") // ReadFrame reads a response in the framing protocol. It expects the // expected response endpoint and length in the expectedResp, and the // expected frame ID in expectedID. // // Returns the whole frame read, the parsed header, and any error. // Returns ErrResponseStatusNotOK if the frame header indicated error. // The payload may have more information about the error and is // returned even when returning ErrResponseStatusNotOK. func (tk TillitisKey) ReadFrame(expectedResp Cmd, expectedID int) ([]byte, FramingHdr, error) { if expectedID > 3 { return nil, FramingHdr{}, fmt.Errorf("frame ID to expect must be 0..3") } if expectedResp.Endpoint() > 3 { return nil, FramingHdr{}, fmt.Errorf("endpoint to expect must be 0..3") } if expectedResp.CmdLen() > 3 { return nil, FramingHdr{}, fmt.Errorf("cmdlen to expect must be 0..3") } // Try to read the single header byte rxHdr := make([]byte, 1) // Read() obeys timeout set using SetReadTimeout() n, err := tk.conn.Read(rxHdr) if err != nil { return nil, FramingHdr{}, fmt.Errorf("Read: %w", err) } if n == 0 { return nil, FramingHdr{}, fmt.Errorf("Read timeout") } hdr, err := parseframe(rxHdr[0]) if err != nil { return nil, hdr, fmt.Errorf("Couldn't parse framing header: %w", err) } if hdr.ResponseNotOK { err = ErrResponseStatusNotOK // Read out the payload and return it anyway, to let // the application decide what to do. There might be // information about the error in the payload. // // Note that ReadFull() overrides any timeout set // using SetReadTimeout() rx := make([]byte, 1+hdr.CmdLen.Bytelen()) if _, readErr := io.ReadFull(tk.conn, rx[1:]); readErr != nil { // NOTE: go 1.20 has errors.Join() err = fmt.Errorf("%w; ReadFull: %w", ErrResponseStatusNotOK, readErr) return nil, hdr, err } rx[0] = rxHdr[0] return rx, hdr, err } if hdr.CmdLen != expectedResp.CmdLen() { return nil, hdr, fmt.Errorf("Expected cmdlen %v (%d bytes), got %v (%d bytes)", expectedResp.CmdLen(), expectedResp.CmdLen().Bytelen(), hdr.CmdLen, hdr.CmdLen.Bytelen()) } if hdr.Endpoint != expectedResp.Endpoint() { return nil, hdr, fmt.Errorf("Message not meant for us: dest %v", hdr.Endpoint) } if hdr.ID != byte(expectedID) { return nil, hdr, fmt.Errorf("Expected ID %d, got %d", expectedID, hdr.ID) } // Prepare a buffer with the header byte first, for returning rx := make([]byte, 1+expectedResp.CmdLen().Bytelen()) rx[0] = rxHdr[0] // Try to read the whole rest of the frame; ReadFull() overrides // any timeout set using SetReadTimeout() if _, err = io.ReadFull(tk.conn, rx[1:]); err != nil { return nil, hdr, fmt.Errorf("ReadFull: %w", err) } if rx[1] != expectedResp.Code() { return rx, hdr, fmt.Errorf("Expected cmd code 0x%x (%s), got 0x%x", expectedResp.Code(), expectedResp, rx[1]) } return rx, hdr, nil } tkeyclient-1.1.0/tkeyclient.go000066400000000000000000000244751470670101400164020ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2022 Tillitis AB // SPDX-License-Identifier: BSD-2-Clause // Package tkeyclient provides a connection to a Tillitis TKey // security stick. To create a new connection: // // tk := tkeyclient.New() // err := tk.Connect(port) // // Then you can start using it by asking it to identify itself: // // nameVer, err := tk.GetNameVersion() // // Or loading and starting an app on the stick: // // err = tk.LoadAppFromFile(*fileName) // // After this, you will have to switch to a new protocol specific to // the app, see for instance the Go package // https://github.com/tillitis/tkeysign for one such app specific // protocol to speak to the signer app: // // https://github.com/tillitis/tkey-device-signer // // When writing your app specific protocol you might still want to use // the framing protocol provided here. See NewFrameBuf() and // ReadFrame(). package tkeyclient import ( "encoding/binary" "fmt" "io" "log" "os" "time" "github.com/ccoveille/go-safecast" "go.bug.st/serial" "golang.org/x/crypto/blake2s" ) var le = log.New(os.Stderr, "", 0) func SilenceLogging() { le.SetOutput(io.Discard) } const ( // Speed in bps for talking to the TKey SerialSpeed = 62500 // Codes used in app proto responses StatusOK = 0x00 StatusBad = 0x01 // Size of RAM in the TKey. See TK1_APP_MAX_SIZE in tk1_mem.h AppMaxSize = 0x20000 ) // TillitisKey is a serial connection to a TKey and the commands that // the firmware supports. type TillitisKey struct { speed int conn serial.Port } // New allocates a new TillitisKey. Use the Connect() method to // actually open a connection. func New() *TillitisKey { tk := &TillitisKey{} return tk } func WithSpeed(speed int) func(*TillitisKey) { return func(tk *TillitisKey) { tk.speed = speed } } // Connect connects to a TKey serial port using the provided port // device and options. func (tk *TillitisKey) Connect(port string, options ...func(*TillitisKey)) error { var err error tk.speed = SerialSpeed for _, opt := range options { opt(tk) } tk.conn, err = serial.Open(port, &serial.Mode{BaudRate: tk.speed}) if err != nil { // Ensure this value is nil, because Open returns an interface tk.conn = nil return fmt.Errorf("Open %s: %w", port, err) } return nil } // Close the connection to the TKey func (tk TillitisKey) Close() error { if tk.conn == nil { return nil } if err := tk.conn.Close(); err != nil { return fmt.Errorf("conn.Close: %w", err) } return nil } // SetReadTimeout sets the timeout of the underlying serial connection to the // TKey. Pass 0 seconds to not have any timeout. Note that the timeout // implemented in the serial lib only works for simple Read(). E.g. // io.ReadFull() will Read() until the buffer is full. // // Deprecated: use SetReadTimeoutNoErr, which can more easily be used with // defer. func (tk TillitisKey) SetReadTimeout(seconds int) error { var t time.Duration = -1 if seconds > 0 { t = time.Duration(seconds) * time.Second } if err := tk.conn.SetReadTimeout(t); err != nil { return fmt.Errorf("SetReadTimeout: %w", err) } return nil } // SetReadTimeoutNoErr sets the timeout, in seconds, of the underlying // serial connection to the TKey. Pass 0 seconds to not have any // timeout. // // Note that the timeout only works for simple Read(). E.g. // io.ReadFull() will still read until the buffer is full. func (tk TillitisKey) SetReadTimeoutNoErr(seconds int) { var t time.Duration = -1 // disables timeout if seconds > 0 { t = time.Duration(seconds) * time.Second } if err := tk.conn.SetReadTimeout(t); err != nil { // err != nil exclusively on invalid values of t, // which is handled before the call. Panic only // possible for API change in go.bug.st/serial panic(fmt.Sprintf("SetReadTimeout: %v", err)) } return } type NameVersion struct { Name0 string Name1 string Version uint32 } func (n *NameVersion) Unpack(raw []byte) { n.Name0 = fmt.Sprintf("%c%c%c%c", raw[0], raw[1], raw[2], raw[3]) n.Name1 = fmt.Sprintf("%c%c%c%c", raw[4], raw[5], raw[6], raw[7]) n.Version = binary.LittleEndian.Uint32(raw[8:12]) } // GetNameVersion gets the name and version from the TKey firmware func (tk TillitisKey) GetNameVersion() (*NameVersion, error) { id := 2 tx, err := NewFrameBuf(cmdGetNameVersion, id) if err != nil { return nil, err } Dump("GetNameVersion tx", tx) if err = tk.Write(tx); err != nil { return nil, err } tk.SetReadTimeoutNoErr(2) defer tk.SetReadTimeoutNoErr(0) rx, _, err := tk.ReadFrame(rspGetNameVersion, id) if err != nil { return nil, fmt.Errorf("ReadFrame: %w", err) } nameVer := &NameVersion{} nameVer.Unpack(rx[2:]) return nameVer, nil } // Modelled after how tpt.py (in tillitis-key1 repo) generates the UDI type UDI struct { Unnamed uint8 // 4 bits, hardcoded to 0 by tpt.py VendorID uint16 ProductID uint8 // 6 bits ProductRevision uint8 // 6 bits Serial uint32 raw []byte } func (u *UDI) RawBytes() []byte { return u.raw } func (u *UDI) String() string { return fmt.Sprintf("%01x%04x:%x:%x:%08x", u.Unnamed, u.VendorID, u.ProductID, u.ProductRevision, u.Serial) } // Unpack unpacks the UDI parts from the raw 8 bytes (2 * 32-bit // words) sent on the wire. // // Returns any error func (u *UDI) Unpack(raw []byte) error { var err error vpr := binary.LittleEndian.Uint32(raw[0:4]) u.Unnamed, err = safecast.ToUint8((vpr >> 28) & 0xf) if err != nil { return fmt.Errorf("%w", err) } u.VendorID, err = safecast.ToUint16((vpr >> 12) & 0xffff) if err != nil { return fmt.Errorf("%w", err) } u.ProductID, err = safecast.ToUint8((vpr >> 6) & 0x3f) if err != nil { return fmt.Errorf("%w", err) } u.ProductRevision, err = safecast.ToUint8(vpr & 0x3f) if err != nil { return fmt.Errorf("%w", err) } u.Serial = binary.LittleEndian.Uint32(raw[4:8]) u.raw = make([]byte, len(raw)) copy(u.raw, raw) return nil } // GetUDI gets the UDI (Unique Device ID) from the TKey firmware func (tk TillitisKey) GetUDI() (*UDI, error) { id := 2 tx, err := NewFrameBuf(cmdGetUDI, id) if err != nil { return nil, err } Dump("GetUDI tx", tx) if err = tk.Write(tx); err != nil { return nil, err } rx, _, err := tk.ReadFrame(rspGetUDI, id) if err != nil { return nil, fmt.Errorf("ReadFrame: %w", err) } if rx[2] != StatusOK { return nil, fmt.Errorf("GetUDI NOK") } udi := &UDI{} err = udi.Unpack(rx[3 : 3+8]) if err != nil { return nil, fmt.Errorf("couldn't unpack UDI: %w", err) } return udi, nil } // LoadAppFromFile loads and runs a raw binary file from fileName into // the TKey. func (tk TillitisKey) LoadAppFromFile(fileName string, secretPhrase []byte) error { content, err := os.ReadFile(fileName) if err != nil { return fmt.Errorf("ReadFile: %w", err) } return tk.LoadApp(content, secretPhrase) } // LoadApp loads the USS (User Supplied Secret), and contents of bin // into the TKey, running the app after verifying that the digest // calculated on the host is the same as the digest from the TKey. // // The USS is a 32 bytes digest hashed from secretPhrase (which is // provided by the user). If secretPhrase is an empty slice, 32 bytes // of zeroes will be loaded as USS. // // Loading USS is always done together with loading and running an // app, because the host program can't otherwise be sure that the // expected USS is used. func (tk TillitisKey) LoadApp(bin []byte, secretPhrase []byte) error { binLen := len(bin) if binLen > AppMaxSize { return fmt.Errorf("File too big") } le.Printf("app size: %v, 0x%x, 0b%b\n", binLen, binLen, binLen) err := tk.loadApp(binLen, secretPhrase) if err != nil { return err } // Load the file var offset int var deviceDigest [32]byte for nsent := 0; offset < binLen; offset += nsent { if binLen-offset <= cmdLoadAppData.CmdLen().Bytelen()-1 { deviceDigest, nsent, err = tk.loadAppData(bin[offset:], true) } else { _, nsent, err = tk.loadAppData(bin[offset:], false) } if err != nil { return fmt.Errorf("loadAppData: %w", err) } } if offset > binLen { return fmt.Errorf("transmitted more than expected") } digest := blake2s.Sum256(bin) le.Printf("Digest from host:\n") printDigest(digest) le.Printf("Digest from device:\n") printDigest(deviceDigest) if deviceDigest != digest { return fmt.Errorf("Different digests") } le.Printf("Same digests!\n") // The app has now started automatically. return nil } // loadApp sets the size and USS of the app to be loaded into the TKey. func (tk TillitisKey) loadApp(size int, secretPhrase []byte) error { id := 2 tx, err := NewFrameBuf(cmdLoadApp, id) if err != nil { return err } // Set size tx[2] = byte(size) tx[3] = byte(size >> 8) tx[4] = byte(size >> 16) tx[5] = byte(size >> 24) if len(secretPhrase) == 0 { tx[6] = 0 } else { tx[6] = 1 // Hash user's phrase as USS uss := blake2s.Sum256(secretPhrase) copy(tx[6:], uss[:]) } Dump("LoadApp tx", tx) if err = tk.Write(tx); err != nil { return err } rx, _, err := tk.ReadFrame(rspLoadApp, id) if err != nil { return fmt.Errorf("ReadFrame: %w", err) } if rx[2] != StatusOK { return fmt.Errorf("LoadApp NOK") } return nil } // loadAppData loads a chunk of the raw app binary into the TKey. func (tk TillitisKey) loadAppData(content []byte, last bool) ([32]byte, int, error) { id := 2 tx, err := NewFrameBuf(cmdLoadAppData, id) if err != nil { return [32]byte{}, 0, err } payload := make([]byte, cmdLoadAppData.CmdLen().Bytelen()-1) copied := copy(payload, content) // Add padding if not filling the payload buffer. if copied < len(payload) { padding := make([]byte, len(payload)-copied) copy(payload[copied:], padding) } copy(tx[2:], payload) Dump("LoadAppData tx", tx) if err = tk.Write(tx); err != nil { return [32]byte{}, 0, err } var rx []byte var expectedResp Cmd if last { expectedResp = rspLoadAppDataReady } else { expectedResp = rspLoadAppData } // Wait for reply rx, _, err = tk.ReadFrame(expectedResp, id) if err != nil { return [32]byte{}, 0, fmt.Errorf("ReadFrame: %w", err) } if rx[2] != StatusOK { return [32]byte{}, 0, fmt.Errorf("LoadAppData NOK") } if last { var digest [32]byte copy(digest[:], rx[3:]) return digest, copied, nil } return [32]byte{}, copied, nil } func printDigest(md [32]byte) { digest := "" for j := 0; j < 4; j++ { for i := 0; i < 8; i++ { digest += fmt.Sprintf("%02x", md[i+8*j]) } digest += " " } le.Print(digest + "\n") } tkeyclient-1.1.0/tools/000077500000000000000000000000001470670101400150245ustar00rootroot00000000000000tkeyclient-1.1.0/tools/spdx-ensure000077500000000000000000000034261470670101400172340ustar00rootroot00000000000000#!/bin/bash # SPDX-FileCopyrightText: 2022 Tillitis AB # SPDX-License-Identifier: BSD-2-Clause set -eu # Check for the SPDX tag in all files in the repo. Exit with a non-zero code if # some is missing. The missingok arrays below contain files and directories # with files where the the tag is not required. cd "${0%/*}" cd .. tag="SPDX-License-Identifier:" missingok_dirs=( .github/workflows/ ) missingok_files=( .editorconfig .gitignore .golangci.yml LICENSE LICENSES/BSD-2-Clause.txt Makefile README.md RELEASE.md go.mod go.sum ) is_missingok() { item="$1" # ok for empty files [[ -f "$item" ]] && [[ ! -s "$item" ]] && return 0 for fileok in "${missingok_files[@]}"; do [[ "$item" = "$fileok" ]] && return 0 done for dirok in "${missingok_dirs[@]}"; do [[ "$item" =~ ^$dirok ]] && return 0 done return 1 } printf "* Checking for SPDX tags in %s\n" "$PWD" mapfile -t repofiles < <(git ls-files || true) if [[ -z "${repofiles[*]}" ]]; then printf "* No files in the repo?!\n" exit 1 fi failed=0 printed=0 for fileok in "${missingok_files[@]}"; do [[ -f "$fileok" ]] && continue if (( !printed )); then printf "* Some files in missingok_files are themselves missing:\n" printed=1 failed=1 fi printf "%s\n" "$fileok" done printed=0 for dirok in "${missingok_dirs[@]}"; do [[ -d "$dirok" ]] && continue if (( !printed )); then printf "* Some dirs in missingok_dirs are themselves missing:\n" printed=1 failed=1 fi printf "%s\n" "$dirok" done printed=0 for file in "${repofiles[@]}"; do is_missingok "$file" && continue if ! grep -q "$tag" "$file"; then if (( !printed )); then printf "* Files missing the SPDX tag:\n" printed=1 failed=1 fi printf "%s\n" "$file" fi done exit "$failed"