├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── attestation └── attestation.go ├── certs ├── attestation_certificate.pem └── ecdsa_privkey.pem ├── cmd ├── fidati-linux │ ├── README.md │ ├── certs │ │ └── statik.go │ ├── gadget-hid.c │ ├── gadget-hid.h │ ├── gadget_hid.go │ └── main.go └── gen-cert │ └── main.go ├── firmware ├── attestation.go ├── certs │ └── statik.go ├── keyring.go ├── leds │ └── leds.go ├── logs.go ├── logs_debug.go ├── main.go ├── sd.go └── usb.go ├── go.mod ├── go.sum ├── internal └── flog │ ├── l.go │ └── l_nolog.go ├── keyring ├── exports_test.go ├── keyring.go └── keyring_test.go ├── u2fhid ├── cmd_msg.go ├── cmd_msg_test.go ├── cmd_ping.go ├── cmd_ping_test.go ├── errors.go ├── errors_test.go ├── handler.go ├── handler_test.go ├── packets.go ├── packets_test.go ├── responses.go ├── types.go ├── types_test.go ├── u2ferror_string.go ├── u2fhidcommand_string.go └── utils_test.go ├── u2ftoken ├── cmd_authenticate.go ├── cmd_register.go ├── cmd_version.go ├── command_string.go ├── errorcode_string.go ├── handle_message.go └── types.go └── usb.go /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/gen-cert/gen-cert 2 | fidati-linux 3 | !cmd/fidati-linux/ 4 | .idea 5 | .vscode 6 | fidati 7 | *.imx 8 | *.bin 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Gianguido Sorà 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Based on http://github.com/usbarmory/tamago-example 2 | 3 | BUILD_USER = $(shell whoami) 4 | BUILD_HOST = $(shell hostname) 5 | BUILD_DATE = $(shell /bin/date -u "+%Y-%m-%d %H:%M:%S") 6 | BUILD = ${BUILD_USER}@${BUILD_HOST} on ${BUILD_DATE} 7 | REV = $(shell git rev-parse --short HEAD 2> /dev/null) 8 | 9 | APP := fidati 10 | TARGET ?= "usbarmory" 11 | GOENV := GO_EXTLINK_ENABLED=0 CGO_ENABLED=0 GOOS=tamago GOARM=7 GOARCH=arm 12 | TEXT_START := 0x80010000 # ramStart (defined in imx6/imx6ul/memory.go) + 0x10000 13 | LDFLAGS = -s -w -T $(TEXT_START) -E _rt0_arm_tamago -R 0x1000 -X 'main.Build=${BUILD}' -X 'main.Revision=${REV}' 14 | GOFLAGS = -tags ${TARGET} -ldflags "${LDFLAGS}" 15 | SHELL = /bin/bash 16 | 17 | .PHONY: clean install test fidati-linux 18 | 19 | #### primary targets #### 20 | 21 | all: $(APP) 22 | 23 | imx: $(APP).imx 24 | 25 | imx_signed: $(APP)-signed.imx 26 | 27 | elf: $(APP) 28 | 29 | #### utilities #### 30 | 31 | check_tamago: 32 | @if [ "${TAMAGO}" == "" ] || [ ! -f "${TAMAGO}" ]; then \ 33 | echo 'You need to set the TAMAGO variable to a compiled version of https://github.com/usbarmory/tamago-go'; \ 34 | exit 1; \ 35 | fi 36 | 37 | check_usbarmory_git: 38 | @if [ "${USBARMORY_GIT}" == "" ]; then \ 39 | echo 'You need to set the USBARMORY_GIT variable to the path of a clone of'; \ 40 | echo ' https://github.com/usbarmory/usbarmory'; \ 41 | exit 1; \ 42 | fi 43 | 44 | check_hab_keys: 45 | @if [ "${HAB_KEYS}" == "" ]; then \ 46 | echo 'You need to set the HAB_KEYS variable to the path of secure boot keys'; \ 47 | echo 'See https://github.com/usbarmory/usbarmory/wiki/Secure-boot-(Mk-II)'; \ 48 | exit 1; \ 49 | fi 50 | 51 | dcd: 52 | @if test "${TARGET}" = "usbarmory"; then \ 53 | cp -f $(GOMODCACHE)/$(TAMAGO_PKG)/board/f-secure/usbarmory/mark-two/imximage.cfg $(APP).dcd; \ 54 | elif test "${TARGET}" = "mx6ullevk"; then \ 55 | cp -f $(GOMODCACHE)/$(TAMAGO_PKG)/board/nxp/mx6ullevk/imximage.cfg $(APP).dcd; \ 56 | else \ 57 | echo "invalid target - options are: usbarmory, mx6ullevk"; \ 58 | exit 1; \ 59 | fi 60 | 61 | clean: 62 | rm -f $(APP) 63 | @rm -fr $(APP).bin $(APP).imx $(APP)-signed.imx $(APP).csf $(APP).dcd 64 | 65 | install: $(APP) 66 | @ssh usbarmory@10.0.0.1 sudo rm /boot/tamago 67 | @scp $(APP) usbarmory@10.0.0.1:/boot/tamago 68 | @ssh usbarmory@10.0.0.1 sudo reboot 69 | 70 | fidati-linux: 71 | $(TAMAGO) build -tags='fidati_logs' -gcflags "all=-N -l" -o ./fidati-linux ./cmd/fidati-linux 72 | #### dependencies #### 73 | $(APP): check_tamago 74 | $(GOENV) $(TAMAGO) build ${GOFLAGS} -o ${APP} ./firmware/ 75 | 76 | test: check_tamago 77 | $(TAMAGO) test $(shell go list ./... | sed -E '/(fidati|firmware|cmd|cert|certs)$$/d') 78 | 79 | $(APP).dcd: check_tamago 80 | $(APP).dcd: GOMODCACHE=$(shell ${TAMAGO} env GOMODCACHE) 81 | $(APP).dcd: TAMAGO_PKG=$(shell grep "github.com/usbarmory/tamago v" go.mod | awk '{print $$1"@"$$2}') 82 | $(APP).dcd: dcd 83 | 84 | $(APP).bin: $(APP) 85 | $(CROSS_COMPILE)objcopy -j .text -j .rodata -j .shstrtab -j .typelink \ 86 | -j .itablink -j .gopclntab -j .go.buildinfo -j .noptrdata -j .data \ 87 | -j .bss --set-section-flags .bss=alloc,load,contents \ 88 | -j .noptrbss --set-section-flags .noptrbss=alloc,load,contents\ 89 | $(APP) -O binary $(APP).bin 90 | 91 | $(APP).imx: check_usbarmory_git $(APP).bin $(APP).dcd 92 | mkimage -n $(APP).dcd -T imximage -e $(TEXT_START) -d $(APP).bin $(APP).imx 93 | # Copy entry point from ELF file 94 | dd if=$(APP) of=$(APP).imx bs=1 count=4 skip=24 seek=4 conv=notrunc 95 | 96 | #### secure boot #### 97 | 98 | $(APP)-signed.imx: check_usbarmory_git check_hab_keys $(APP).imx 99 | ${USBARMORY_GIT}/software/secure_boot/usbarmory_csftool \ 100 | --csf_key ${HAB_KEYS}/CSF_1_key.pem \ 101 | --csf_crt ${HAB_KEYS}/CSF_1_crt.pem \ 102 | --img_key ${HAB_KEYS}/IMG_1_key.pem \ 103 | --img_crt ${HAB_KEYS}/IMG_1_crt.pem \ 104 | --table ${HAB_KEYS}/SRK_1_2_3_4_table.bin \ 105 | --index 1 \ 106 | --image $(APP).imx \ 107 | --output $(APP).csf && \ 108 | cat $(APP).imx $(APP).csf > $(APP)-signed.imx 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `fidati`: DIY FIDO2 U2F token 2 | 3 | `fidati` is a FIDO2 U2F token implementation for the F-Secure USB Armory Mk.II, written in Go by leveraging the [Tamago](https://github.com/usbarmory/tamago) compiler. 4 | 5 | This repository holds a developer-friendly Tamago firmware, for a more user-friendly one check out [`GoKey`](https://github.com/usbarmory/gokey). 6 | 7 | ## Project status: **PoC** 8 | 9 | This project is still very much a Proof-of-Concept and should be handled as such: **there are exactly zero guarantees about the safety/security of fidati**. 10 | 11 | Code is still work-in-progress, expect bugs/bad practices and so on. 12 | 13 | What works: 14 | - HID interface 15 | - key generation 16 | - site registration 17 | - site authentication 18 | 19 | `fidati` uses the microSD card as its support for persistency. 20 | 21 | Currently no filesystem is supported, so `fidati` will use up the entire microSD space if needed. 22 | 23 | This means that `fidati` can only be ran from the Armory eMMC - a future revision will fix this. 24 | 25 | To prepare a microSD for `fidati`, zero out the first 512 bytes: 26 | 27 | ```bash 28 | dd if=/dev/zero of=/dev/mmcblk0 bs=512 count=1 29 | ``` 30 | 31 | No relying party private key is stored, the microSD is only used to store a monotonic counter. 32 | 33 | For more details about how `fidati` deterministic key derivation works, see [here](https://www.yubico.com/blog/yubicos-u2f-key-wrapping/). 34 | 35 | ## Building and running 36 | 37 | You can run `fidati` with or without a bootloader. 38 | 39 | By default the project `Makefile` produces a binary with logging disabled. 40 | 41 | To enable logging append `TARGET="'usbarmory debug fidati_logs'"` to the `make` parameters. 42 | 43 | `fidati` as a library disables logging by default. 44 | 45 | To enable it, build your program with the `fidati_logs` build tag. 46 | 47 | ### Booting via U-Boot 48 | 49 | ``` 50 | $ make 51 | ``` 52 | 53 | This command will produce a self-standing ELF executable, `fidati`, which can be booted via U-Boot in the usual way: 54 | 55 | ``` 56 | ext4load mmc 0:1 0x80800000 /fidati 57 | bootelf 0x80800000 58 | ``` 59 | 60 | ### Booting without a bootloader 61 | 62 | ``` 63 | $ make imx 64 | ``` 65 | 66 | This command will produce a i.MX native image, `fidati.imx`, which can be flashed to either the internal Armory eMMC or a microSD. 67 | 68 | Refer to [these instructions](https://github.com/usbarmory/usbarmory/wiki/Boot-Modes-(Mk-II)#flashing-imx-native-images) for further instructions. 69 | 70 | ## Usage as a library 71 | 72 | `fidati` can be used as a library, by importing the `github.com/gsora/fidati` package and invoking the `ConfigureUSB()` function. 73 | 74 | See `firmware/main.go` and `firmware/usb.go` for an example. 75 | 76 | ## Technical details 77 | 78 | `fidati` implements the bare minimum functionality to act as a FIDO2 U2F token, as detailed by the [FIDO Alliance](https://fidoalliance.org/specifications/download/). 79 | 80 | A default attestation certificate and private key are contained in this repository, in the `/certs` directory. 81 | 82 | A CLI tool – `gen-cert` – is available for those who want to generate their own certificate and private key. 83 | 84 | For each relying party, given their `appID` and a device-specific master key `fidati` derives in a deterministic fashion an ECDSA private key, which will then be used in the registration and authentication phase. 85 | 86 | The derivation algorithm is defined as follows: 87 | 88 | ``` 89 | nonce := (32 secure random bytes) 90 | relyingPartyPrivateKey := HMAC-SHA256(MasterKey, appID, nonce) 91 | keyHandle := HMAC-SHA256(MasterKey, appID, relyingPartyPrivateKey) + nonce 92 | ``` 93 | 94 | To derive the private key back given a `keyHandle` and `appID`, one must extract the `nonce` by reading the last 32 bytes of `keyHandle` and then execute the algorithm again. 95 | 96 | ## Debugging 97 | 98 | To test U2F token registration and login, the following tools can be used: 99 | - https://mdp.github.io/u2fdemo/ 100 | - https://demo.yubico.com/webauthn-technical/registration 101 | - https://github.com/Yubico/java-webauthn-server/ 102 | -------------------------------------------------------------------------------- /attestation/attestation.go: -------------------------------------------------------------------------------- 1 | package attestation 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // ErrNotPem represents a PEM decoding error. 12 | var ErrNotPEM = errors.New("not a PEM-encoded block") 13 | 14 | // ParseCertificate parses a X.509 certificate from the data contained in the 15 | // PEM data block. 16 | // Returns an error when c is not valid PEM data, or a valid X.509 certificate. 17 | func ParseCertificate(c []byte) ([]byte, *x509.Certificate, error) { 18 | certPem, _ := pem.Decode(c) 19 | if certPem == nil { 20 | return nil, nil, ErrNotPEM 21 | } 22 | 23 | cert, err := x509.ParseCertificate(certPem.Bytes) 24 | if err != nil { 25 | return nil, nil, fmt.Errorf("invalid X.509 certificate, %w", err) 26 | } 27 | 28 | return certPem.Bytes, cert, nil 29 | } 30 | 31 | // ParseKey parses a PEM-encoded ECDSA private key. 32 | // Returns an error if k is not a PEM-encoded block, or the embedded block doesn't 33 | // contain a valid ECDSA private key. 34 | func ParseKey(k []byte) (*ecdsa.PrivateKey, error) { 35 | pkPem, _ := pem.Decode(k) 36 | if pkPem == nil { 37 | return nil, ErrNotPEM 38 | } 39 | 40 | attestationPrivkey, err := x509.ParseECPrivateKey(pkPem.Bytes) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return attestationPrivkey, nil 46 | 47 | } 48 | -------------------------------------------------------------------------------- /certs/attestation_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBRzCB7qADAgECAghtIXm7RanNVjAKBggqhkjOPQQDAjAoMQswCQYDVQQGEwJJ 3 | VDEZMBcGA1UEAxMQRmlkYXRpIFUyRiBUb2tlbjAeFw0yMDEwMjExNTI5MzJaFw0z 4 | MDEwMjExNTI5MzJaMCgxCzAJBgNVBAYTAklUMRkwFwYDVQQDExBGaWRhdGkgVTJG 5 | IFRva2VuMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs04bp/oQuJDhnS8WHwL1 6 | iKDjp/ZM+BsPm+jkFXSSJz5cxX9j3yfUbyeehfxR/q9K5qYPqXsvVH8YwGsRXX7I 7 | /6MCMAAwCgYIKoZIzj0EAwIDSAAwRQIhAL4m70THkPNa42TaJ4wm8X9RICFBIddw 8 | eWTep502dn7RAiBJW0ZRDj1wOnIvIcvgnKDEtn0V47FzeAmMUqk1yQ65PA== 9 | -----END CERTIFICATE----- 10 | -------------------------------------------------------------------------------- /certs/ecdsa_privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIMbn7UuvkCD/vUjPof4/3Mvt5HrPYXRceEXX0gqlqzl9oAoGCCqGSM49 3 | AwEHoUQDQgAEs04bp/oQuJDhnS8WHwL1iKDjp/ZM+BsPm+jkFXSSJz5cxX9j3yfU 4 | byeehfxR/q9K5qYPqXsvVH8YwGsRXX7I/w== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /cmd/fidati-linux/README.md: -------------------------------------------------------------------------------- 1 | # fidati-linux 2 | 3 | This directory holds `fidati-linux`, a Go program which leverages Linux kernel to run a `fidati` U2F token in userspace. 4 | 5 | This is a **development tool**, since it has no security guarantees and doesn't store the usage counter in a persistent way. 6 | 7 | ## Dependencies 8 | 9 | `fidati-linux` requires the following components to run: 10 | 11 | - a Linux kernel configured with the `libcomposite`, `dummy_hcd`, `configfs` modules 12 | - root privileges 13 | - [`libusbgx`](https://github.com/libusbgx/libusbgx) 14 | 15 | `fidati-linux` simulates a full-blown USB HID device by leveraging the `dummy_hcd` kernel module. 16 | 17 | The `libusbgx` dependency is needed to properly configure and tear down the virtual USB device. 18 | 19 | ## Building and usage 20 | 21 | To build `fidati-linux`: 22 | 23 | ```bash 24 | make fidati-linux 25 | ``` 26 | 27 | Run `./fidati-linux -h` to see every configuration parameter. 28 | -------------------------------------------------------------------------------- /cmd/fidati-linux/certs/statik.go: -------------------------------------------------------------------------------- 1 | // Code generated by statik. DO NOT EDIT. 2 | 3 | package certs 4 | 5 | import ( 6 | "github.com/rakyll/statik/fs" 7 | ) 8 | 9 | 10 | func init() { 11 | data := "PK\x03\x04\x14\x00\x08\x00\x08\x00M\x80]Q\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00attestation_certificate.pemUT\x05\x00\x01\x92\xe7\x9a_d\x91\xbd\x92\xa2@\x18Es\x9eb\xf3\xa9-\xd1\xd5A\x83 \xbe\xfe\xc3\xc6i\xc6n\x01\xc1\x0c\x95il\x04\xa5tl\xe0\xe9\xb7\xd6\xda\xda`\xe7F\xb7Nr\x82\xf3\xf3\xcf\x10\xf5y\xf8\x03S\x15q\xc61D\xf4I\x1d\xc19R\x03F^\x0b\x044\xc5\xa0\xcb;OkO\xe5M\x98\x18X!\xad\xdb\xb22\x1fk) \x18\xb8\x08y\xb3Xf$\x91\xd2\xa76\x08\x9c\x84\xd0\x9d@\x07\x1f\xc61\x85NHU\x9f\xab,UW\xce\xe2^\x9dP\xbc\x9f\xdc\xcf{\x03\x05\xb3n/\x08\xb5\xc2\xd0.\x8c\xf8L\x0cA\xce\xac;8\xffC\x81u\x87\x07\x08\x90\x0e\x13\x04Y\x04\xd59\x16\xaa\xb2\xcc>\xad\x84v\xc8\xcf\xb7\xaa<\xfa\x95N\xa2\xc0w8S\x8f|\x92| VYj\xb3\xe5\xea\xb2\xe3\x83q1\xc8\x8c\xff\xfd\x04\xe4\x81H\x0d\xf4\xe6N\xf7\xd7\xd1E~\x05\xa4l6\xf3\xed\xd2\xbe\x8f\x9d\xd3\x8a\x98\xebh'^\xd0m]\xbf\x98\x8a\xa5\x9bM0\xcc\x0e]\xba0\xbf\xfa\xcfx\xdf\x17E\xf9\xd9\xa9Q\xbbX\xcd\xdal\xdd\xa6\xb7G\xb2\x9cg\xd6\xbf\xa94\xf5\xb83z\x15X\x00X\xac\xff\x19)XN6\x00VI^\xc2\xfb\xb4\xf6\xdchY\xad\xc3|:\x89\xf2`j\xeby\xbaP\x1c3\xc4\x8fG\xeb\x14\xdb\xa8\xb8\xce\xdc\xc9\xb1\xf1\x14\x9cP\xb0uw\x8a\x98\xb1\xfdh\xf8\x83\x1f\x1e\xbaY\x11zo\xdcd\xea\xb1\xa1\x80Z\xc4m5\xee\xe5\xebl\x0doo\xce3$\x0d\xc9\xf7\xb8\xbf\x03\x00\x00\xff\xffPK\x07\x08qduX\x83\x01\x00\x00\xf9\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00M\x80]Q\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x00ecdsa_privkey.pemUT\x05\x00\x01\x92\xe7\x9a_l\x8f1s\x820\x18@w~\x05\xbb\xd7\x8b=\xe1\x94\xc1!&_ \xd2x\x01\x8a\xfd\xe8\x08\x07Z\xb4\xc645Q\x7f}\xaf\xce}\xeb[\xde{\xfac\x05i\xb6 \x81\x85\xaa\xcc\xb6\xf4\x0d\xc2\x1c\x9a\x87\x08\xa4\xe8\x18-\x002\xd9\x9e\xe6\xf5\xc5\x1d\x18'\xae\x1e\x95\x1e\"2\x93\xee'\x16\xdf\xaa\xc1\xb2\xeb\x01q\xba3Gs?&\x9a\xea\x941\x93V2J\x02\xeaA\xe8\xba\xe0\xc5\x8e\x82\x9dF\xed\x99\xe8\xe2\xb2\xe6\xfbS\xb5x\x17\xfe\xf5\xf93\xe7\xe3\x99|\xc8\xc9\xca\xaa\xaf\xc9xx\xc1\xaaZ\xdf\xe3\xee\x8a\xc98\xbb\x0du\xd0\xde\xfa~?\\Kb\x92<6\x8d2h\xddV,\x1a\x9f\xda\x12q\x9e\x11\xbf\\\x06\x8fX\xd8\xf0\x7f\x1f~\x03\x00\x00\xff\xffPK\x07\x08D\x13\x8f\xfc\xc5\x00\x00\x00\xe3\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00M\x80]QqduX\x83\x01\x00\x00\xf9\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00attestation_certificate.pemUT\x05\x00\x01\x92\xe7\x9a_PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00M\x80]QD\x13\x8f\xfc\xc5\x00\x00\x00\xe3\x00\x00\x00\x11\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd5\x01\x00\x00ecdsa_privkey.pemUT\x05\x00\x01\x92\xe7\x9a_PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\x9a\x00\x00\x00\xe2\x02\x00\x00\x00\x00" 12 | fs.Register(data) 13 | } 14 | -------------------------------------------------------------------------------- /cmd/fidati-linux/gadget-hid.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Originally taken from libusbgx examples, modified for fidati-linux 3 | * needs. 4 | * 5 | * Copyright (C) 2014 Samsung Electronics 6 | * 7 | * Krzysztof Opasiak 8 | * 9 | * This program is free software; you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation; either version 2 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * This program is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | */ 19 | 20 | #include "gadget-hid.h" 21 | 22 | int configure_hidg( 23 | const char* serial, 24 | const char* manufacturer, 25 | const char* product, 26 | const char* configfs_path, 27 | const char* report_descriptor, 28 | size_t report_descriptor_len 29 | ) { 30 | usbg_state *s; 31 | usbg_gadget *g; 32 | usbg_config *c; 33 | usbg_function *f_hid; 34 | int ret = -EINVAL; 35 | int usbg_ret; 36 | 37 | struct usbg_gadget_attrs g_attrs = { 38 | .bcdUSB = 0x0200, 39 | .bDeviceClass = USB_CLASS_PER_INTERFACE, 40 | .bDeviceSubClass = 0x00, 41 | .bDeviceProtocol = 0x00, 42 | .bMaxPacketSize0 = 64, /* Max allowed ep0 packet size */ 43 | .idVendor = VENDOR, 44 | .idProduct = PRODUCT, 45 | .bcdDevice = 0x0001, /* Verson of device */ 46 | }; 47 | 48 | struct usbg_gadget_strs g_strs = { 49 | .serial = (char *)serial, /* Serial number */ 50 | .manufacturer = (char *)manufacturer, /* Manufacturer */ 51 | .product = (char *)product /* Product string */ 52 | }; 53 | 54 | struct usbg_config_strs c_strs = { 55 | .configuration = "1xHID" 56 | }; 57 | 58 | struct usbg_f_hid_attrs f_attrs = { 59 | .protocol = 0x21, 60 | .report_desc = { 61 | .desc = (char *)report_descriptor, 62 | .len = report_descriptor_len, 63 | }, 64 | .report_length = 64, 65 | .subclass = 0, 66 | }; 67 | 68 | usbg_ret = usbg_init(configfs_path, &s); 69 | if (usbg_ret != USBG_SUCCESS) { 70 | goto out1; 71 | } 72 | 73 | usbg_ret = usbg_create_gadget(s, "g1", &g_attrs, &g_strs, &g); 74 | if (usbg_ret != USBG_SUCCESS) { 75 | goto out2; 76 | } 77 | 78 | usbg_ret = usbg_create_function(g, USBG_F_HID, "usb0", &f_attrs, &f_hid); 79 | if (usbg_ret != USBG_SUCCESS) { 80 | goto out2; 81 | } 82 | 83 | usbg_ret = usbg_create_config(g, 1, "fidati-linux", NULL, &c_strs, &c); 84 | if (usbg_ret != USBG_SUCCESS) { 85 | goto out2; 86 | } 87 | 88 | usbg_ret = usbg_add_config_function(c, "u2fhid", f_hid); 89 | if (usbg_ret != USBG_SUCCESS) { 90 | goto out2; 91 | } 92 | 93 | usbg_ret = usbg_enable_gadget(g, DEFAULT_UDC); 94 | if (usbg_ret != USBG_SUCCESS) { 95 | goto out2; 96 | } 97 | 98 | ret = 0; 99 | out2: 100 | usbg_cleanup(s); 101 | 102 | out1: 103 | return ret; 104 | } 105 | 106 | int remove_gadget(usbg_gadget *g) 107 | { 108 | int usbg_ret; 109 | usbg_udc *u; 110 | 111 | /* Check if gadget is enabled */ 112 | u = usbg_get_gadget_udc(g); 113 | 114 | /* If gadget is enable we have to disable it first */ 115 | if (u) { 116 | usbg_ret = usbg_disable_gadget(g); 117 | if (usbg_ret != USBG_SUCCESS) { 118 | goto out; 119 | } 120 | } 121 | 122 | /* Remove gadget with USBG_RM_RECURSE flag to remove 123 | * also its configurations, functions and strings */ 124 | usbg_ret = usbg_rm_gadget(g, USBG_RM_RECURSE); 125 | 126 | out: 127 | return usbg_ret; 128 | } 129 | 130 | int cleanup_usbg(const char* configfs_path) 131 | { 132 | int usbg_ret; 133 | int ret = -EINVAL; 134 | usbg_state *s; 135 | usbg_gadget *g; 136 | struct usbg_gadget_attrs g_attrs; 137 | 138 | usbg_ret = usbg_init(configfs_path, &s); 139 | if (usbg_ret != USBG_SUCCESS) { 140 | goto out1; 141 | } 142 | 143 | g = usbg_get_first_gadget(s); 144 | while (g != NULL) { 145 | /* Get current gadget attrs to be compared */ 146 | usbg_ret = usbg_get_gadget_attrs(g, &g_attrs); 147 | if (usbg_ret != USBG_SUCCESS) { 148 | goto out2; 149 | } 150 | 151 | /* Compare attrs with given values and remove if suitable */ 152 | if (g_attrs.idVendor == VENDOR && g_attrs.idProduct == PRODUCT) { 153 | usbg_gadget *g_next = usbg_get_next_gadget(g); 154 | 155 | usbg_ret = remove_gadget(g); 156 | if (usbg_ret != USBG_SUCCESS) 157 | goto out2; 158 | 159 | g = g_next; 160 | } else { 161 | g = usbg_get_next_gadget(g); 162 | } 163 | } 164 | 165 | out2: 166 | usbg_cleanup(s); 167 | out1: 168 | return ret; 169 | } 170 | -------------------------------------------------------------------------------- /cmd/fidati-linux/gadget-hid.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define VENDOR 0x1d6b 8 | #define PRODUCT 0x0142 9 | 10 | int configure_hidg( 11 | const char* serial, 12 | const char* manufacturer, 13 | const char* product, 14 | const char* configfs_path, 15 | const char* report_descriptor, 16 | size_t report_descriptor_len 17 | ); 18 | 19 | int cleanup_usbg(const char* configfs_path); 20 | 21 | 22 | -------------------------------------------------------------------------------- /cmd/fidati-linux/gadget_hid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // #cgo LDFLAGS: -lusbgx 4 | // #include "gadget-hid.h" 5 | // #include 6 | import "C" 7 | 8 | import ( 9 | "fmt" 10 | "unsafe" 11 | 12 | "github.com/gsora/fidati/u2fhid" 13 | ) 14 | 15 | func configureHidg(configfsPath string) error { 16 | reportDescC := (*C.char)(unsafe.Pointer(&u2fhid.DefaultReport[0])) 17 | 18 | serial := C.CString("4242424242") 19 | manufacturer := C.CString("gsora") 20 | product := C.CString("fidati desktop") 21 | cfp := C.CString(configfsPath) 22 | 23 | defer func() { 24 | C.free(unsafe.Pointer(serial)) 25 | C.free(unsafe.Pointer(manufacturer)) 26 | C.free(unsafe.Pointer(product)) 27 | C.free(unsafe.Pointer(cfp)) 28 | }() 29 | 30 | res := C.configure_hidg( 31 | serial, 32 | manufacturer, 33 | product, 34 | cfp, 35 | reportDescC, 36 | C.ulong(len(u2fhid.DefaultReport)), 37 | ) 38 | 39 | if res != C.USBG_SUCCESS { 40 | rres := C.usbg_error(res) 41 | errName := C.GoString(C.usbg_error_name(rres)) 42 | stdErr := C.GoString(C.usbg_strerror(rres)) 43 | 44 | return fmt.Errorf("libusbgx failure, %s: %s", errName, stdErr) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func cleanupHidg(configfsPath string) error { 51 | cfp := C.CString(configfsPath) 52 | 53 | defer func() { 54 | C.free(unsafe.Pointer(cfp)) 55 | }() 56 | 57 | C.cleanup_usbg(cfp) 58 | return nil 59 | 60 | } 61 | -------------------------------------------------------------------------------- /cmd/fidati-linux/main.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/rakyll/statik -src=../certs -p=certs 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/gsora/fidati/keyring" 16 | "github.com/gsora/fidati/u2fhid" 17 | "github.com/gsora/fidati/u2ftoken" 18 | "github.com/rakyll/statik/fs" 19 | 20 | _ "github.com/gsora/fidati/cmd/fidati-linux/certs" 21 | ) 22 | 23 | var ( 24 | // X.509 attestation certificate, sent along in registration requests 25 | attestationCertificate []byte 26 | 27 | // ECDSA private key, used to sign registration requests 28 | attestationPrivkey []byte 29 | ) 30 | 31 | func cliArgs() (hidg, configfsPath string, mustClean bool) { 32 | flag.StringVar(&hidg, "hidg", "/dev/hidg0", "/dev/hidgX file descriptor path") 33 | flag.StringVar(&configfsPath, "configfs-path", "/sys/kernel/config", "configfs path") 34 | flag.BoolVar(&mustClean, "clean", false, "clean existing hidg descriptors and exit") 35 | flag.Parse() 36 | 37 | return 38 | } 39 | 40 | func main() { 41 | hidg, configfsPath, mustClean := cliArgs() 42 | 43 | if mustClean { 44 | if err := cleanupHidg(configfsPath); err != nil { 45 | panic(err) 46 | } 47 | 48 | return 49 | } 50 | 51 | if err := configureHidg(configfsPath); err != nil { 52 | panic(err) 53 | } 54 | 55 | sigs := make(chan os.Signal, 1) 56 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 57 | 58 | readCertPrivkey() 59 | 60 | hidRx, err := os.OpenFile(hidg, os.O_RDWR, 0666) 61 | notErr(err) 62 | 63 | log.Println("done, polling...") 64 | d := &dumbCounter{} 65 | k := genKeyring(attestationPrivkey, d) 66 | 67 | token, err := u2ftoken.New(k, attestationCertificate, attestationPrivkey) 68 | notErr(err) 69 | 70 | hid, err := u2fhid.NewHandler(token) 71 | notErr(err) 72 | 73 | // add 50ms delay in both rx and tx 74 | // we don't wanna burn laptop cpus :^) 75 | 76 | // rx 77 | go func() { 78 | for { 79 | time.Sleep(50 * time.Millisecond) 80 | buf := make([]byte, 64) 81 | _, err := hidRx.Read(buf) 82 | notErr(err) 83 | 84 | _, err = hid.Rx(buf, nil) 85 | if err != nil { 86 | log.Println("rx error:", err) 87 | continue 88 | } 89 | } 90 | }() 91 | 92 | go func() { 93 | for { 94 | time.Sleep(50 * time.Millisecond) 95 | data, err := hid.Tx(nil, nil) 96 | if err != nil { 97 | log.Println("tx error:", err) 98 | continue 99 | } 100 | 101 | if data == nil { 102 | continue 103 | } 104 | 105 | _, err = hidRx.Write(data) 106 | notErr(err) 107 | } 108 | }() 109 | 110 | log.Println("running...") 111 | 112 | <-sigs 113 | 114 | fmt.Println() 115 | log.Println("cleaning...") 116 | 117 | if err := cleanupHidg(configfsPath); err != nil { 118 | panic(err) 119 | } 120 | } 121 | 122 | func genKeyring(secret []byte, counter keyring.Counter) *keyring.Keyring { 123 | return keyring.New(secret, counter) 124 | } 125 | 126 | func readCertPrivkey() { 127 | statikFS, err := fs.New() 128 | notErr(err) 129 | 130 | aCert, err := statikFS.Open("/attestation_certificate.pem") 131 | notErr(err) 132 | 133 | aPk, err := statikFS.Open("/ecdsa_privkey.pem") 134 | notErr(err) 135 | 136 | aCertBytes, err := ioutil.ReadAll(aCert) 137 | notErr(err) 138 | 139 | aPkBytes, err := ioutil.ReadAll(aPk) 140 | notErr(err) 141 | 142 | attestationCertificate = aCertBytes 143 | attestationPrivkey = aPkBytes 144 | } 145 | 146 | // since we're in a critical configuration phase, panic on error. 147 | func notErr(e error) { 148 | if e != nil { 149 | panic(e) 150 | } 151 | } 152 | 153 | type dumbCounter struct { 154 | i uint32 155 | } 156 | 157 | func (d *dumbCounter) Increment(appID []byte, challenge []byte, keyHandle []byte) (uint32, error) { 158 | d.i++ 159 | return d.i, nil 160 | } 161 | 162 | func (d *dumbCounter) UserPresence() bool { 163 | return true 164 | } 165 | -------------------------------------------------------------------------------- /cmd/gen-cert/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "io/ioutil" 11 | "log" 12 | "math" 13 | "math/big" 14 | "time" 15 | ) 16 | 17 | func main() { 18 | serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) 19 | if err != nil { 20 | log.Fatal("cannot generate serial, ", err) 21 | } 22 | cert := &x509.Certificate{ 23 | NotBefore: time.Now(), 24 | NotAfter: time.Now().AddDate(10, 0, 0), // this certificate is valid for 10 years 25 | SerialNumber: serial, 26 | Issuer: pkix.Name{ 27 | Country: []string{"IT"}, 28 | SerialNumber: "", 29 | CommonName: "Fidati U2F Token", 30 | }, 31 | PublicKeyAlgorithm: x509.ECDSA, 32 | SignatureAlgorithm: x509.ECDSAWithSHA256, 33 | } 34 | 35 | cert.Subject = cert.Issuer 36 | 37 | privkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 38 | if err != nil { 39 | log.Fatal("cannot generate ecdsa key ", err) 40 | } 41 | 42 | certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &privkey.PublicKey, privkey) 43 | if err != nil { 44 | log.Fatal("cannot generate certificate, ", err) 45 | } 46 | 47 | privkeyDer, err := x509.MarshalECPrivateKey(privkey) 48 | if err != nil { 49 | log.Fatal("cannot marshal ecdsa privkey, ", err) 50 | } 51 | 52 | privkeyPem := pem.EncodeToMemory(&pem.Block{ 53 | Type: "EC PRIVATE KEY", 54 | Bytes: privkeyDer, 55 | }) 56 | 57 | err = ioutil.WriteFile("ecdsa_privkey.pem", privkeyPem, 0644) 58 | if err != nil { 59 | log.Fatal("cannot write private key: ", err) 60 | } 61 | 62 | certPem := pem.EncodeToMemory(&pem.Block{ 63 | Type: "CERTIFICATE", 64 | Bytes: certBytes, 65 | }) 66 | 67 | err = ioutil.WriteFile("attestation_certificate.pem", certPem, 0644) 68 | if err != nil { 69 | log.Fatal("cannot write certificate: ", err) 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /firmware/attestation.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/rakyll/statik -src=../certs -p=certs 2 | 3 | package main 4 | 5 | import ( 6 | "io/ioutil" 7 | 8 | "github.com/rakyll/statik/fs" 9 | ) 10 | 11 | var ( 12 | // X.509 attestation certificate, sent along in registration requests 13 | attestationCertificate []byte 14 | 15 | // ECDSA private key, used to sign registration requests 16 | attestationPrivkey []byte 17 | ) 18 | 19 | func readCertPrivkey() { 20 | statikFS, err := fs.New() 21 | notErr(err) 22 | 23 | aCert, err := statikFS.Open("/attestation_certificate.pem") 24 | notErr(err) 25 | 26 | aPk, err := statikFS.Open("/ecdsa_privkey.pem") 27 | notErr(err) 28 | 29 | aCertBytes, err := ioutil.ReadAll(aCert) 30 | notErr(err) 31 | 32 | aPkBytes, err := ioutil.ReadAll(aPk) 33 | notErr(err) 34 | 35 | attestationCertificate = aCertBytes 36 | attestationPrivkey = aPkBytes 37 | } 38 | -------------------------------------------------------------------------------- /firmware/certs/statik.go: -------------------------------------------------------------------------------- 1 | // Code generated by statik. DO NOT EDIT. 2 | 3 | package certs 4 | 5 | import ( 6 | "github.com/rakyll/statik/fs" 7 | ) 8 | 9 | 10 | func init() { 11 | data := "PK\x03\x04\x14\x00\x08\x00\x08\x00M\x80]Q\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x00attestation_certificate.pemUT\x05\x00\x01\x92\xe7\x9a_d\x91\xbd\x92\xa2@\x18Es\x9eb\xf3\xa9-\xd1\xd5A\x83 \xbe\xfe\xc3\xc6i\xc6n\x01\xc1\x0c\x95il\x04\xa5tl\xe0\xe9\xb7\xd6\xda\xda`\xe7F\xb7Nr\x82\xf3\xf3\xcf\x10\xf5y\xf8\x03S\x15q\xc61D\xf4I\x1d\xc19R\x03F^\x0b\x044\xc5\xa0\xcb;OkO\xe5M\x98\x18X!\xad\xdb\xb22\x1fk) \x18\xb8\x08y\xb3Xf$\x91\xd2\xa76\x08\x9c\x84\xd0\x9d@\x07\x1f\xc61\x85NHU\x9f\xab,UW\xce\xe2^\x9dP\xbc\x9f\xdc\xcf{\x03\x05\xb3n/\x08\xb5\xc2\xd0.\x8c\xf8L\x0cA\xce\xac;8\xffC\x81u\x87\x07\x08\x90\x0e\x13\x04Y\x04\xd59\x16\xaa\xb2\xcc>\xad\x84v\xc8\xcf\xb7\xaa<\xfa\x95N\xa2\xc0w8S\x8f|\x92| VYj\xb3\xe5\xea\xb2\xe3\x83q1\xc8\x8c\xff\xfd\x04\xe4\x81H\x0d\xf4\xe6N\xf7\xd7\xd1E~\x05\xa4l6\xf3\xed\xd2\xbe\x8f\x9d\xd3\x8a\x98\xebh'^\xd0m]\xbf\x98\x8a\xa5\x9bM0\xcc\x0e]\xba0\xbf\xfa\xcfx\xdf\x17E\xf9\xd9\xa9Q\xbbX\xcd\xdal\xdd\xa6\xb7G\xb2\x9cg\xd6\xbf\xa94\xf5\xb83z\x15X\x00X\xac\xff\x19)XN6\x00VI^\xc2\xfb\xb4\xf6\xdchY\xad\xc3|:\x89\xf2`j\xeby\xbaP\x1c3\xc4\x8fG\xeb\x14\xdb\xa8\xb8\xce\xdc\xc9\xb1\xf1\x14\x9cP\xb0uw\x8a\x98\xb1\xfdh\xf8\x83\x1f\x1e\xbaY\x11zo\xdcd\xea\xb1\xa1\x80Z\xc4m5\xee\xe5\xebl\x0doo\xce3$\x0d\xc9\xf7\xb8\xbf\x03\x00\x00\xff\xffPK\x07\x08qduX\x83\x01\x00\x00\xf9\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00M\x80]Q\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00 \x00ecdsa_privkey.pemUT\x05\x00\x01\x92\xe7\x9a_l\x8f1s\x820\x18@w~\x05\xbb\xd7\x8b=\xe1\x94\xc1!&_ \xd2x\x01\x8a\xfd\xe8\x08\x07Z\xb4\xc645Q\x7f}\xaf\xce}\xeb[\xde{\xfac\x05i\xb6 \x81\x85\xaa\xcc\xb6\xf4\x0d\xc2\x1c\x9a\x87\x08\xa4\xe8\x18-\x002\xd9\x9e\xe6\xf5\xc5\x1d\x18'\xae\x1e\x95\x1e\"2\x93\xee'\x16\xdf\xaa\xc1\xb2\xeb\x01q\xba3Gs?&\x9a\xea\x941\x93V2J\x02\xeaA\xe8\xba\xe0\xc5\x8e\x82\x9dF\xed\x99\xe8\xe2\xb2\xe6\xfbS\xb5x\x17\xfe\xf5\xf93\xe7\xe3\x99|\xc8\xc9\xca\xaa\xaf\xc9xx\xc1\xaaZ\xdf\xe3\xee\x8a\xc98\xbb\x0du\xd0\xde\xfa~?\\Kb\x92<6\x8d2h\xddV,\x1a\x9f\xda\x12q\x9e\x11\xbf\\\x06\x8fX\xd8\xf0\x7f\x1f~\x03\x00\x00\xff\xffPK\x07\x08D\x13\x8f\xfc\xc5\x00\x00\x00\xe3\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00M\x80]QqduX\x83\x01\x00\x00\xf9\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00attestation_certificate.pemUT\x05\x00\x01\x92\xe7\x9a_PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00M\x80]QD\x13\x8f\xfc\xc5\x00\x00\x00\xe3\x00\x00\x00\x11\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd5\x01\x00\x00ecdsa_privkey.pemUT\x05\x00\x01\x92\xe7\x9a_PK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00\x9a\x00\x00\x00\xe2\x02\x00\x00\x00\x00" 12 | fs.Register(data) 13 | } 14 | -------------------------------------------------------------------------------- /firmware/keyring.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/gsora/fidati/keyring" 4 | 5 | func genKeyring(secret []byte, counter keyring.Counter) *keyring.Keyring { 6 | return keyring.New(secret, counter) 7 | } 8 | -------------------------------------------------------------------------------- /firmware/leds/leds.go: -------------------------------------------------------------------------------- 1 | // +build usbarmory 2 | 3 | package leds 4 | 5 | import ( 6 | "context" 7 | "runtime" 8 | "time" 9 | 10 | usbarmory "github.com/usbarmory/tamago/board/usbarmory/mk2" 11 | ) 12 | 13 | // cancelFuncs holds references to the functions needed to stop 14 | // the LED blink. 15 | var cancelFuncs []context.CancelFunc 16 | 17 | // StartBlink starts the white and blue LED blinking. 18 | func StartBlink() { 19 | whiteCtx, whiteCancel := context.WithCancel(context.Background()) 20 | blueCtx, blueCancel := context.WithCancel(context.Background()) 21 | 22 | go func() { // start led blinking in a goroutine, don't block the main thread 23 | go blink(whiteCtx, "white") 24 | 25 | time.Sleep(1 * time.Second) 26 | runtime.Gosched() 27 | 28 | go blink(blueCtx, "blue") 29 | }() 30 | 31 | cancelFuncs = []context.CancelFunc{ 32 | whiteCancel, 33 | blueCancel, 34 | } 35 | } 36 | 37 | // StopBlink stops the blinking, and turns both LEDs off. 38 | func StopBlink() { 39 | for _, cf := range cancelFuncs { 40 | cf() 41 | } 42 | 43 | cancelFuncs = nil 44 | } 45 | 46 | // Panic stops blinking, turns both LEDs on. 47 | func Panic() { 48 | StopBlink() 49 | usbarmory.LED("blue", true) 50 | usbarmory.LED("white", true) 51 | } 52 | 53 | func blink(ctx context.Context, led string) { 54 | lastVal := true 55 | for { 56 | select { 57 | case <-ctx.Done(): 58 | usbarmory.LED(led, false) 59 | return 60 | default: 61 | runtime.Gosched() 62 | usbarmory.LED(led, lastVal) 63 | time.Sleep(1 * time.Second) 64 | lastVal = !lastVal 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /firmware/logs.go: -------------------------------------------------------------------------------- 1 | // +build !debug 2 | 3 | package main 4 | 5 | import ( 6 | "io/ioutil" 7 | "log" 8 | ) 9 | 10 | func enableLogs() { 11 | log.SetOutput(ioutil.Discard) 12 | } 13 | -------------------------------------------------------------------------------- /firmware/logs_debug.go: -------------------------------------------------------------------------------- 1 | // +build debug 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "os" 8 | 9 | usbarmory "github.com/usbarmory/tamago/board/usbarmory/mk2" 10 | ) 11 | 12 | func enableLogs() { 13 | usbarmory.EnableDebugAccessory() 14 | log.SetOutput(os.Stdout) 15 | log.Println("enabled debugging logs") 16 | } 17 | -------------------------------------------------------------------------------- /firmware/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "runtime" 7 | "runtime/debug" 8 | 9 | "github.com/gsora/fidati/firmware/leds" 10 | 11 | usbarmory "github.com/usbarmory/tamago/board/usbarmory/mk2" 12 | "github.com/usbarmory/tamago/nxp/imx6ul" 13 | 14 | _ "github.com/gsora/fidati/firmware/certs" 15 | ) 16 | 17 | var ( 18 | // Build is a string which contains build user, host and date. 19 | Build string 20 | 21 | // Revision contains the git revision (last hash and/or tag). 22 | Revision string 23 | 24 | banner string 25 | ) 26 | 27 | func init() { 28 | banner = fmt.Sprintf("%s/%s (%s) • %s %s", 29 | runtime.GOOS, runtime.GOARCH, runtime.Version(), 30 | Revision, Build) 31 | 32 | log.SetFlags(log.Lshortfile) 33 | enableLogs() 34 | 35 | model := imx6ul.Model() 36 | _, family, revMajor, revMinor := imx6ul.SiliconVersion() 37 | 38 | if !imx6ul.Native { 39 | log.Fatal("running fidati on emulated hardware is not supported") 40 | } 41 | 42 | if err := imx6ul.SetARMFreq(900); err != nil { 43 | log.Printf("WARNING: error setting ARM frequency: %v", err) 44 | } 45 | 46 | banner += fmt.Sprintf(" • %s %d MHz", model, imx6ul.ARMFreq()/1000000) 47 | 48 | log.Printf("imx6_soc: %s (%#x, %d.%d) @ %d MHz - native:%v", 49 | model, family, revMajor, revMinor, imx6ul.ARMFreq()/1000000, imx6ul.Native) 50 | 51 | err := usbarmory.SD.Detect() 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | readCertPrivkey() 57 | 58 | leds.StartBlink() 59 | } 60 | 61 | func main() { 62 | defer catchPanic() 63 | 64 | log.Println(banner) 65 | 66 | go rebootWatcher() 67 | 68 | //s := &sd{} 69 | 70 | /*if err := blank(); err != nil { 71 | panic(err) 72 | } 73 | 74 | var store *storage.Storage 75 | data, err := readStorageData(s) 76 | if err != nil && err != noData { 77 | panic(err) 78 | } else if err == noData { 79 | st, err := storage.New(s, nil) 80 | if err != nil { 81 | panic(err) 82 | } 83 | 84 | store = st 85 | } else { 86 | st, err := storage.New(s, data) 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | store = st 92 | }*/ 93 | 94 | counter, err := readSdCounter() 95 | if err != nil { 96 | panic(err) 97 | } 98 | 99 | k := genKeyring(attestationPrivkey, counter) 100 | startUSB(k) 101 | } 102 | 103 | func rebootWatcher() { 104 | buf := make([]byte, 1) 105 | 106 | for { 107 | runtime.Gosched() 108 | imx6ul.UART2.Read(buf) 109 | if buf[0] == 0 { 110 | continue 111 | } 112 | 113 | if buf[0] == 'r' { 114 | log.Println("rebooting...") 115 | imx6ul.Reset() 116 | } 117 | 118 | buf[0] = 0 119 | } 120 | } 121 | 122 | // catchPanic catches every panic(), sets the LEDs into error mode and prints the stacktrace. 123 | func catchPanic() { 124 | if r := recover(); r != nil { 125 | leds.Panic() 126 | fmt.Printf("panic: %v\n\n", r) 127 | fmt.Println(string(debug.Stack())) 128 | 129 | for { 130 | } // stuck here forever! 131 | } 132 | } 133 | 134 | // since we're in a critical configuration phase, panic on error. 135 | func notErr(e error) { 136 | if e != nil { 137 | panic(e) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /firmware/sd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math" 9 | 10 | usbarmory "github.com/usbarmory/tamago/board/usbarmory/mk2" 11 | ) 12 | 13 | const ( 14 | writeOffset = 1 15 | 16 | counterLBA = 1 17 | counterBlockAmount = 1 18 | ) 19 | 20 | var noData = errors.New("no data") 21 | 22 | // wrongOffset panics if writeOffset is greater than the maximum number of blocks available on the SD. 23 | func wrongOffset() { 24 | info := usbarmory.SD.Info() 25 | if info.Blocks < writeOffset { 26 | panic(fmt.Sprintf("specified base write offset %d bigger than SD total block number %d", writeOffset, info.Blocks)) 27 | } 28 | } 29 | 30 | // closestSectorNumber returns the closest number of sectors divisible by the microSD block size, 31 | // by rounding up. 32 | func closestSectorNumber(n int) int { 33 | m := float64(usbarmory.SD.Info().BlockSize) 34 | return int(math.Ceil(float64(n)/m) * m) 35 | } 36 | 37 | func sdWrite(offset int, data []byte) error { 38 | var nd []byte 39 | 40 | if len(data) < usbarmory.SD.Info().BlockSize { 41 | nd = make([]byte, usbarmory.SD.Info().BlockSize) 42 | copy(nd, data) 43 | } else if len(data) == usbarmory.SD.Info().BlockSize { 44 | nd = data 45 | } else { 46 | nd = make([]byte, closestSectorNumber(len(data))) 47 | copy(nd, data) 48 | } 49 | 50 | log.Printf("writing %v at lba %v", nd, offset) 51 | err := usbarmory.SD.WriteBlocks(offset, nd) 52 | log.Println("finished writing") 53 | return err 54 | } 55 | 56 | func sdRead(offset, numBlocks int) ([]byte, error) { 57 | ret := make([]byte, numBlocks) 58 | 59 | return ret, usbarmory.SD.ReadBlocks(offset, ret) 60 | } 61 | 62 | type sdCounter struct { 63 | counter uint32 64 | } 65 | 66 | func (s *sdCounter) UserPresence() bool { 67 | // we always say yes :)] 68 | return true 69 | } 70 | 71 | func readSdCounter() (*sdCounter, error) { 72 | cbytes, err := sdRead(counterLBA, counterBlockAmount) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return &sdCounter{ 78 | counter: binary.LittleEndian.Uint32(cbytes), 79 | }, nil 80 | } 81 | 82 | func (s *sdCounter) Increment(_ []byte, _ []byte, _ []byte) (uint32, error) { 83 | s.counter++ 84 | cbytes := make([]byte, 4) 85 | binary.LittleEndian.PutUint32(cbytes, s.counter) 86 | err := sdWrite(counterLBA, cbytes) 87 | if err != nil { 88 | return 0, err 89 | } 90 | 91 | return s.counter, nil 92 | } 93 | -------------------------------------------------------------------------------- /firmware/usb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/usbarmory/tamago/soc/nxp/usb" 5 | 6 | "github.com/gsora/fidati" 7 | "github.com/gsora/fidati/keyring" 8 | "github.com/gsora/fidati/u2fhid" 9 | "github.com/gsora/fidati/u2ftoken" 10 | ) 11 | 12 | func baseConfiguration(device *usb.Device) { 13 | // Supported Language Code Zero: English 14 | device.SetLanguageCodes([]uint16{0x0409}) 15 | 16 | // device descriptor 17 | device.Descriptor = &usb.DeviceDescriptor{} 18 | device.Descriptor.SetDefaults() 19 | 20 | // HID devices sets those in the Interface descriptor. 21 | device.Descriptor.DeviceClass = 0x0 22 | device.Descriptor.DeviceSubClass = 0x0 23 | device.Descriptor.DeviceProtocol = 0x0 24 | 25 | // http://pid.codes/1209/2702/ 26 | // Standard USB Armory {Vendor,Product}ID 27 | device.Descriptor.VendorId = 0x1209 28 | device.Descriptor.ProductId = 0x2702 29 | 30 | device.Descriptor.Device = 0x0001 31 | 32 | iManufacturer, err := device.AddString(`gsora`) 33 | notErr(err) 34 | device.Descriptor.Manufacturer = iManufacturer 35 | 36 | iProduct, err := device.AddString(`fidati`) 37 | notErr(err) 38 | device.Descriptor.Product = iProduct 39 | 40 | iSerial, err := device.AddString(`0.42`) 41 | notErr(err) 42 | device.Descriptor.SerialNumber = iSerial 43 | } 44 | 45 | func startUSB(keyring *keyring.Keyring) { 46 | device := &usb.Device{} 47 | 48 | token, err := u2ftoken.New(keyring, attestationCertificate, attestationPrivkey) 49 | notErr(err) 50 | 51 | hid, err := u2fhid.NewHandler(token) 52 | notErr(err) 53 | 54 | conf := fidati.DefaultConfiguration() 55 | 56 | baseConfiguration(device) 57 | 58 | err = device.AddConfiguration(&conf) 59 | notErr(err) 60 | 61 | err = fidati.ConfigureUSB(&conf, device, hid) 62 | notErr(err) 63 | 64 | usb.USB1.Init() 65 | usb.USB1.DeviceMode() 66 | usb.USB1.Reset() 67 | 68 | // never returns 69 | usb.USB1.Start(device) 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gsora/fidati 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/rakyll/statik v0.1.7 7 | github.com/stretchr/testify v1.7.1 8 | github.com/usbarmory/tamago v0.0.0-20220823080407-04f05cf2a5a3 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= 6 | github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | github.com/usbarmory/tamago v0.0.0-20220316100121-e9baee61883d h1:x8WcgA2q/Nf4ytDn5qW4uOguyglQ7mIlEy4HA0D0T1U= 11 | github.com/usbarmory/tamago v0.0.0-20220316100121-e9baee61883d/go.mod h1:Lok79mjbJnhoBGqhX5cCUsZtSemsQF5FNZW+2R1dRr8= 12 | github.com/usbarmory/tamago v0.0.0-20220823080407-04f05cf2a5a3 h1:OgWngXmohy/sxTnHm7uStq0YlMkH0J+JY2HnBCv6fn0= 13 | github.com/usbarmory/tamago v0.0.0-20220823080407-04f05cf2a5a3/go.mod h1:Lok79mjbJnhoBGqhX5cCUsZtSemsQF5FNZW+2R1dRr8= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /internal/flog/l.go: -------------------------------------------------------------------------------- 1 | // +build fidati_logs 2 | 3 | package flog 4 | 5 | import ( 6 | "log" 7 | "os" 8 | ) 9 | 10 | var Logger = log.New(os.Stdout, "fidati :: ", log.Lshortfile) 11 | -------------------------------------------------------------------------------- /internal/flog/l_nolog.go: -------------------------------------------------------------------------------- 1 | // +build !fidati_logs 2 | 3 | package flog 4 | 5 | import ( 6 | "io/ioutil" 7 | "log" 8 | ) 9 | 10 | var Logger = log.New(ioutil.Discard, "fidati :: ", log.Lshortfile) 11 | -------------------------------------------------------------------------------- /keyring/exports_test.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import "crypto/ecdsa" 4 | 5 | var RetrievePrivatekey = retrievePrivkey 6 | 7 | type NonceFuncType = func() ([]byte, error) 8 | type KeygenFuncType = func(b []byte) (*ecdsa.PrivateKey, error) 9 | 10 | func SetNonceFunc(f NonceFuncType) NonceFuncType { 11 | nonceFunc = f 12 | return nonce 13 | } 14 | 15 | func SetKeygenFunc(f KeygenFuncType) KeygenFuncType { 16 | keygenFunc = f 17 | return generateECKey 18 | } 19 | -------------------------------------------------------------------------------- /keyring/keyring.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/hmac" 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/binary" 11 | "errors" 12 | "fmt" 13 | "math/big" 14 | ) 15 | 16 | var nonceFunc = nonce 17 | var keygenFunc = generateECKey 18 | 19 | // Counter is some sort of interface to a counter (like, a monotonic counter) and to a 20 | // user presence confirmation device. 21 | type Counter interface { 22 | Increment(appID, challenge, keyHandle []byte) (uint32, error) 23 | UserPresence() bool 24 | } 25 | 26 | // Keyring represents a mechanism to derive deterministic relying party authentication private keys 27 | // given a master key. 28 | // A Keyring needs a Counter to be able to pass along the counter value recommended by the FIDO U2F standard. 29 | // Keyring implements the key wrapping method described by Yubico: https://www.yubico.com/blog/yubicos-u2f-key-wrapping/. 30 | type Keyring struct { 31 | Counter Counter 32 | MasterKey []byte 33 | } 34 | 35 | func (k *Keyring) validate() error { 36 | switch { 37 | case k.MasterKey == nil: 38 | return errors.New("master key is nil") 39 | case k.Counter == nil: 40 | return errors.New("counter is nil") 41 | default: 42 | return nil 43 | } 44 | } 45 | 46 | // New returns a Keyring pointer given a master key and a Counter. 47 | func New(mk []byte, counter Counter) *Keyring { 48 | return &Keyring{ 49 | MasterKey: mk, 50 | Counter: counter, 51 | } 52 | } 53 | 54 | // NonceFromKeyHandle returns the nonce from a given keyhandle. 55 | // Assumes SHA-256 as hashing function. 56 | func (k *Keyring) NonceFromKeyHandle(kh []byte) []byte { 57 | if len(kh) < sha256.Size { 58 | return nil 59 | } 60 | 61 | return kh[sha256.Size:] 62 | } 63 | 64 | // Register deterministically derives an ECDSA public key given an application ID. 65 | // It also returns a key handle (also deterministic) and an error. 66 | // If nonce is not nil, it will be used for the derivation process. 67 | func (k *Keyring) Register(appID []byte, nonce []byte) (*ecdsa.PublicKey, []byte, error) { 68 | if err := k.validate(); err != nil { 69 | return nil, nil, err 70 | } 71 | 72 | if appID == nil { 73 | return nil, nil, errors.New("appID is nil") 74 | } 75 | 76 | if nonce == nil { 77 | var err error 78 | nonce, err = nonceFunc() 79 | if err != nil { 80 | return nil, nil, err 81 | } 82 | } 83 | 84 | mac := hmac.New(sha256.New, k.MasterKey) 85 | _, err := mac.Write(appID) 86 | if err != nil { 87 | return nil, nil, err 88 | } 89 | 90 | _, err = mac.Write(nonce) 91 | if err != nil { 92 | return nil, nil, err 93 | } 94 | 95 | rpPrivKey := mac.Sum(nil) 96 | 97 | mac.Reset() 98 | 99 | _, err = mac.Write(appID) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | _, err = mac.Write(rpPrivKey) 105 | if err != nil { 106 | return nil, nil, err 107 | } 108 | 109 | keyHandle := append(mac.Sum(nil), nonce...) 110 | 111 | ecPrivKey, err := keygenFunc(rpPrivKey) 112 | if err != nil { 113 | return nil, nil, err 114 | } 115 | 116 | return &ecPrivKey.PublicKey, keyHandle, nil 117 | } 118 | 119 | // retrievePrivkey returns the private key associated to a given application ID and key handle. 120 | func retrievePrivkey(appID, keyHandle, masterKey []byte) (*ecdsa.PrivateKey, error) { 121 | if len(keyHandle) < 32 { 122 | return nil, errors.New("key handle is shorter than 32 bytes") 123 | } 124 | 125 | nonce := keyHandle[32:] 126 | 127 | mac := hmac.New(sha256.New, masterKey) 128 | _, err := mac.Write(appID) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | _, err = mac.Write(nonce) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | rpPrivKey := mac.Sum(nil) 139 | 140 | ecPrivKey, err := keygenFunc(rpPrivKey) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | return ecPrivKey, nil 146 | } 147 | 148 | // Authenticate returns a valid FIDO2 U2F authentication signature for the given application ID, 149 | // authentication challenge, key handle and a byte indicating whether user presence was confirmed or not. 150 | // It also returns the updated count to be used in the authentication message, and an error. 151 | func (k *Keyring) Authenticate(appID, challenge, keyHandle []byte, userPresence bool) ([]byte, uint32, error) { 152 | if err := k.validate(); err != nil { 153 | return nil, 0, err 154 | } 155 | 156 | switch { 157 | case appID == nil: 158 | return nil, 0, fmt.Errorf("appID is nil") 159 | case challenge == nil: 160 | return nil, 0, fmt.Errorf("challenge is nil") 161 | case keyHandle == nil: 162 | return nil, 0, fmt.Errorf("keyHandle is nil") 163 | } 164 | 165 | privKey, err := retrievePrivkey(appID, keyHandle, k.MasterKey) 166 | if err != nil { 167 | return nil, 0, fmt.Errorf("cannot derive private key from appID and keyHandle, %w", err) 168 | } 169 | 170 | userPresenceByte := byte(0) 171 | if userPresence { 172 | userPresenceByte = 1 173 | } 174 | 175 | count, err := k.Counter.Increment(appID, challenge, keyHandle) 176 | if err != nil { 177 | return nil, 0, fmt.Errorf("cannot increment counter, %w", err) 178 | } 179 | 180 | sp := signaturePayload( 181 | appID, 182 | count, 183 | challenge, 184 | userPresenceByte, 185 | ) 186 | 187 | sph := sha256.Sum256(sp) 188 | spHash := sph[:] 189 | 190 | sign, err := ecdsa.SignASN1(rand.Reader, privKey, spHash) 191 | if err != nil { 192 | return nil, 0, fmt.Errorf("cannot execute authentication signature, %w", err) 193 | } 194 | 195 | return sign, count, nil 196 | } 197 | 198 | // signaturePayload returns the byte slice to be signed to validate an authentication request. 199 | func signaturePayload(appParam []byte, counter uint32, challengeParam []byte, userPresenceByte byte) []byte { 200 | ret := new(bytes.Buffer) 201 | 202 | ret.Write(appParam) 203 | ret.WriteByte(userPresenceByte) 204 | counterBytes := [4]byte{} 205 | 206 | binary.BigEndian.PutUint32(counterBytes[:], counter) 207 | 208 | ret.Write(counterBytes[:]) 209 | ret.Write(challengeParam) 210 | 211 | return ret.Bytes() 212 | } 213 | 214 | // nonce returns a byte slice with 32 bytes of randomness inside. 215 | func nonce() ([]byte, error) { 216 | n := make([]byte, 32) 217 | _, err := rand.Read(n) 218 | return n, err 219 | } 220 | 221 | // generateECKey generates a ECDSA private key given b bytes. 222 | // This function is deterministic, and will return always the same *ecdsa.PrivateKey given 223 | // the same b bytes. 224 | func generateECKey(b []byte) (*ecdsa.PrivateKey, error) { 225 | // code adapted from https://golang.org/src/crypto/ecdsa/ecdsa.go?#L133 226 | // we had to hand-roll this because ecdsa.GenerateKey() expects 40 bytes of random data 227 | // instead of just 32. 228 | // We're basically using b as our privkey bytes representation. 229 | c := elliptic.P256() 230 | params := c.Params() 231 | var one = new(big.Int).SetInt64(1) 232 | k := new(big.Int).SetBytes(b) 233 | n := new(big.Int).Sub(params.N, one) 234 | k.Mod(k, n) 235 | k.Add(k, one) 236 | 237 | priv := new(ecdsa.PrivateKey) 238 | priv.PublicKey.Curve = c 239 | priv.D = k 240 | priv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes()) 241 | return priv, nil 242 | } 243 | -------------------------------------------------------------------------------- /keyring/keyring_test.go: -------------------------------------------------------------------------------- 1 | package keyring_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "errors" 7 | "testing" 8 | 9 | "github.com/gsora/fidati/keyring" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type testCounter struct { 14 | i uint32 15 | userPresent bool 16 | incrementMustFail bool 17 | } 18 | 19 | func (t *testCounter) Increment(appID []byte, challenge []byte, keyHandle []byte) (uint32, error) { 20 | if t.incrementMustFail { 21 | return 0, errors.New("cannot increment") 22 | } 23 | 24 | t.i++ 25 | return t.i, nil 26 | } 27 | 28 | func (t *testCounter) UserPresence() bool { 29 | return t.userPresent 30 | } 31 | 32 | var nonceErr = func() ([]byte, error) { 33 | return nil, errors.New("nonce generation error") 34 | } 35 | 36 | var keygenErr = func(_ []byte) (*ecdsa.PrivateKey, error) { 37 | return nil, errors.New("key generation error") 38 | } 39 | 40 | func TestNew(t *testing.T) { 41 | tests := []struct { 42 | name string 43 | mk []byte 44 | counter keyring.Counter 45 | }{ 46 | { 47 | "proper arguments (none nil)", 48 | []byte("key"), 49 | &testCounter{}, 50 | }, 51 | { 52 | "nil key", 53 | nil, 54 | &testCounter{}, 55 | }, 56 | { 57 | "nil counter", 58 | []byte("key"), 59 | nil, 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | require.NotNil(t, keyring.New(tt.mk, tt.counter)) 65 | }) 66 | } 67 | } 68 | 69 | func TestKeyring_Register(t *testing.T) { 70 | k := keyring.New([]byte("key"), &testCounter{}) 71 | 72 | tests := []struct { 73 | name string 74 | kr *keyring.Keyring 75 | appID []byte 76 | wantErr bool 77 | nonceFunc keyring.NonceFuncType 78 | keygenFunc keyring.KeygenFuncType 79 | }{ 80 | { 81 | "appID is not nil", 82 | k, 83 | []byte("appID"), 84 | false, 85 | nil, 86 | nil, 87 | }, 88 | { 89 | "appID is nil", 90 | k, 91 | nil, 92 | true, 93 | nil, 94 | nil, 95 | }, 96 | { 97 | "nonce generation fails", 98 | k, 99 | []byte("appID"), 100 | true, 101 | nonceErr, 102 | nil, 103 | }, 104 | { 105 | "key generation fails", 106 | k, 107 | []byte("appID"), 108 | true, 109 | nil, 110 | keygenErr, 111 | }, 112 | { 113 | "keyring has nil master key", 114 | &keyring.Keyring{ 115 | MasterKey: nil, 116 | Counter: &testCounter{}, 117 | }, 118 | []byte("appID"), 119 | true, 120 | nil, 121 | nil, 122 | }, 123 | { 124 | "keyring has nil counter", 125 | &keyring.Keyring{ 126 | MasterKey: []byte("key"), 127 | Counter: nil, 128 | }, 129 | []byte("appID"), 130 | true, 131 | nil, 132 | nil, 133 | }, 134 | { 135 | "keyring has both fields nil", 136 | &keyring.Keyring{ 137 | MasterKey: nil, 138 | Counter: nil, 139 | }, 140 | []byte("appID"), 141 | true, 142 | nil, 143 | nil, 144 | }, 145 | } 146 | for _, tt := range tests { 147 | t.Run(tt.name, func(t *testing.T) { 148 | if tt.nonceFunc != nil { 149 | oldNonceFunc := keyring.SetNonceFunc(tt.nonceFunc) 150 | defer func() { 151 | _ = keyring.SetNonceFunc(oldNonceFunc) 152 | }() 153 | } 154 | 155 | if tt.keygenFunc != nil { 156 | oldKeygenFunc := keyring.SetKeygenFunc(tt.keygenFunc) 157 | defer func() { 158 | _ = keyring.SetKeygenFunc(oldKeygenFunc) 159 | }() 160 | } 161 | 162 | pubKey, keyHandle, err := tt.kr.Register(tt.appID) 163 | 164 | if tt.wantErr { 165 | t.Log("error:", err) 166 | require.Error(t, err) 167 | require.Nil(t, pubKey) 168 | require.Nil(t, keyHandle) 169 | return 170 | } 171 | 172 | require.NoError(t, err) 173 | require.NotNil(t, pubKey) 174 | require.NotNil(t, keyHandle) 175 | }) 176 | } 177 | } 178 | 179 | func TestKeyring_RetrievePrivatekey(t *testing.T) { 180 | appID := bytes.Repeat([]byte{42}, 64) 181 | keyHandle := bytes.Repeat([]byte{43}, 64) 182 | masterKey := bytes.Repeat([]byte{44}, 64) 183 | 184 | firstIteration, err := keyring.RetrievePrivatekey(appID, keyHandle, masterKey) 185 | require.NoError(t, err) 186 | 187 | secondIteration, err := keyring.RetrievePrivatekey(appID, keyHandle, masterKey) 188 | require.NoError(t, err) 189 | 190 | require.True(t, firstIteration.Equal(secondIteration)) 191 | } 192 | 193 | func TestKeyring_Authenticate(t *testing.T) { 194 | tc := &testCounter{} 195 | k := keyring.New([]byte("key"), tc) 196 | 197 | data := []byte("data") 198 | keyHandle := bytes.Repeat([]byte{42}, 64) 199 | 200 | type args struct { 201 | appID []byte 202 | challenge []byte 203 | keyHandle []byte 204 | } 205 | 206 | tests := []struct { 207 | name string 208 | args args 209 | wantErr bool 210 | incrementFails bool 211 | keygenFunc keyring.KeygenFuncType 212 | kr *keyring.Keyring 213 | }{ 214 | { 215 | "appID is nil", 216 | args{ 217 | nil, 218 | data, 219 | keyHandle, 220 | }, 221 | true, 222 | false, 223 | nil, 224 | k, 225 | }, 226 | { 227 | "challenge is nil", 228 | args{ 229 | data, 230 | nil, 231 | keyHandle, 232 | }, 233 | true, 234 | false, 235 | nil, 236 | k, 237 | }, 238 | { 239 | "keyHandle is nil", 240 | args{ 241 | data, 242 | data, 243 | nil, 244 | }, 245 | true, 246 | false, 247 | nil, 248 | k, 249 | }, 250 | { 251 | "keyHandle is less than 32 bytes long", 252 | args{ 253 | data, 254 | data, 255 | data, 256 | }, 257 | true, 258 | false, 259 | nil, 260 | k, 261 | }, 262 | { 263 | "all fine but increment fails", 264 | args{ 265 | data, 266 | data, 267 | keyHandle, 268 | }, 269 | true, 270 | true, 271 | nil, 272 | k, 273 | }, 274 | { 275 | "all fine but keygen function fails", 276 | args{ 277 | data, 278 | data, 279 | keyHandle, 280 | }, 281 | true, 282 | false, 283 | keygenErr, 284 | k, 285 | }, 286 | { 287 | "all fine but keyring has nil master key", 288 | args{ 289 | data, 290 | data, 291 | keyHandle, 292 | }, 293 | true, 294 | false, 295 | nil, 296 | &keyring.Keyring{ 297 | MasterKey: nil, 298 | Counter: tc, 299 | }, 300 | }, 301 | { 302 | "all fine but keyring has nil counter", 303 | args{ 304 | data, 305 | data, 306 | keyHandle, 307 | }, 308 | true, 309 | false, 310 | nil, 311 | &keyring.Keyring{ 312 | MasterKey: []byte("key"), 313 | Counter: nil, 314 | }, 315 | }, 316 | { 317 | "all fine but keyring has nil counter and master key", 318 | args{ 319 | data, 320 | data, 321 | keyHandle, 322 | }, 323 | true, 324 | false, 325 | nil, 326 | &keyring.Keyring{ 327 | MasterKey: nil, 328 | Counter: nil, 329 | }, 330 | }, 331 | { 332 | "all fine", 333 | args{ 334 | data, 335 | data, 336 | keyHandle, 337 | }, 338 | false, 339 | false, 340 | nil, 341 | k, 342 | }, 343 | } 344 | for _, tt := range tests { 345 | t.Run(tt.name, func(t *testing.T) { 346 | tc.incrementMustFail = tt.incrementFails 347 | 348 | if tt.keygenFunc != nil { 349 | oldKeygenFunc := keyring.SetKeygenFunc(tt.keygenFunc) 350 | defer func() { 351 | _ = keyring.SetKeygenFunc(oldKeygenFunc) 352 | }() 353 | } 354 | 355 | authSig, counter, err := tt.kr.Authenticate(tt.args.appID, tt.args.challenge, tt.args.keyHandle, true) 356 | 357 | if tt.wantErr { 358 | t.Log("error:", err) 359 | require.Error(t, err) 360 | require.Nil(t, authSig) 361 | require.Zero(t, counter) 362 | return 363 | } 364 | 365 | require.NoError(t, err) 366 | require.NotNil(t, authSig) 367 | require.NotZero(t, counter) 368 | }) 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /u2fhid/cmd_msg.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | // handleMsg handles cmdMsg commands. 4 | func (h *Handler) handleMsg(session *session, pkt u2fPacket) ([][]byte, error) { 5 | return genPackets( 6 | h.token.HandleMessage(session.data[:session.total]), 7 | session.command, 8 | pkt.ChannelBytes(), 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /u2fhid/cmd_msg_test.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestHandler_handleMsg(t *testing.T) { 11 | 12 | tests := []struct { 13 | name string 14 | token Token 15 | errAssertion require.ErrorAssertionFunc 16 | packetsAssertion require.ValueAssertionFunc 17 | }{ 18 | { 19 | "underlying token returns no error", 20 | &fakeToken{ 21 | shouldReturnData: true, 22 | data: []byte("data"), 23 | }, 24 | require.NoError, 25 | require.NotEmpty, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | u, err := NewHandler(tt.token) 31 | require.NoError(t, err) 32 | 33 | s := &session{ 34 | data: bytes.Repeat([]byte{42}, 42), 35 | command: cmdMsg, 36 | total: 42, 37 | leftToRead: 0, 38 | lastSequence: 0, 39 | } 40 | 41 | p := initPacket{ 42 | ChannelID: [4]byte{ 43 | 1, 44 | 2, 45 | 3, 46 | 4, 47 | }, 48 | Cmd: cmdMsg, 49 | PayloadLength: 42, 50 | Data: bytes.Repeat([]byte{42}, 42), 51 | } 52 | 53 | data, err := u.handleMsg(s, p) 54 | tt.errAssertion(t, err) 55 | tt.packetsAssertion(t, data) 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /u2fhid/cmd_ping.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | // handlePing handles cmdPing commands. 4 | func handlePing(session *session, pkt u2fPacket) ([][]byte, error) { 5 | // U2FHID_PING echoes back whatever you throw at it. 6 | return genPackets(session.data, session.command, pkt.ChannelBytes()) 7 | } 8 | -------------------------------------------------------------------------------- /u2fhid/cmd_ping_test.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_handlePing(t *testing.T) { 11 | t.Run("ping returns whatever you throw at it", func(t *testing.T) { 12 | s := &session{ 13 | data: bytes.Repeat([]byte{42}, 42), 14 | command: cmdPing, 15 | total: 42, 16 | leftToRead: 0, 17 | lastSequence: 0, 18 | } 19 | 20 | p := initPacket{ 21 | ChannelID: [4]byte{ 22 | 1, 23 | 2, 24 | 3, 25 | 4, 26 | }, 27 | Cmd: cmdPing, 28 | PayloadLength: 42, 29 | Data: bytes.Repeat([]byte{42}, 42), 30 | } 31 | 32 | data, err := handlePing(s, p) 33 | require.NoError(t, err) 34 | 35 | require.Len(t, data, 1) 36 | 37 | elem := data[0] 38 | require.Len(t, elem, 64) 39 | require.NotEmpty(t, elem) 40 | 41 | // last 57 bytes are equal to p.Data 42 | eqData := make([]byte, 57) 43 | copy(eqData, p.Data) 44 | require.Equal(t, eqData, elem[7:]) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /u2fhid/errors.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | // u2fError represent some kind of error happened during the u2fhid processing. 10 | //go:generate stringer -type=u2fError 11 | type u2fError uint8 12 | 13 | const ( 14 | // no error 15 | none u2fError = 0 16 | 17 | // invalid command 18 | invalidCmd u2fError = 1 19 | 20 | // invalid parameter 21 | invalidPar u2fError = 2 22 | 23 | // invalid length 24 | invalidLen u2fError = 3 25 | 26 | // invalid sequence number 27 | invalidSeq u2fError = 4 28 | 29 | // timeout 30 | msgTimeout u2fError = 5 31 | 32 | // communication channel is busy 33 | channelBusy u2fError = 6 34 | 35 | // a channel lock is required 36 | lockRequired u2fError = 10 37 | 38 | // invalid channel id 39 | invalidCid u2fError = 11 40 | 41 | // other kind of error 42 | other u2fError = 127 43 | ) 44 | 45 | // generateError generates a u2fError payload ready to be sent on the wire. 46 | func generateError(code u2fError, pkt u2fPacket) [][]byte { 47 | b := new(bytes.Buffer) 48 | 49 | u := standardResponse{ 50 | Command: uint8(cmdError), 51 | ChannelID: pkt.ChannelBytes(), 52 | } 53 | 54 | binary.BigEndian.PutUint16(u.Count[:], uint16(1)) 55 | err := binary.Write(b, binary.LittleEndian, u) 56 | if err != nil { 57 | panic(fmt.Sprintf("cannot serialize msg payload, %v", err)) 58 | } 59 | 60 | final := append(b.Bytes(), uint8(code)) 61 | 62 | return [][]byte{ 63 | final, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /u2fhid/errors_test.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_generateError(t *testing.T) { 11 | t.Run("error packets are generated correctly", func(t *testing.T) { 12 | p := initPacket{ 13 | ChannelID: [4]byte{ 14 | 1, 15 | 2, 16 | 3, 17 | 4, 18 | }, 19 | Cmd: cmdPing, 20 | PayloadLength: 42, 21 | Data: bytes.Repeat([]byte{42}, 42), 22 | } 23 | 24 | b := generateError(other, p) 25 | 26 | require.Len(t, b, 1) 27 | 28 | pkt := b[0] 29 | 30 | require.Len(t, pkt, 8) // 7 bytes of header + 1 for error 31 | 32 | require.Equal(t, pkt[0:4], p.ChannelID[:]) // channelID 33 | require.Equal(t, pkt[4], uint8(cmdError)) // command 34 | require.Equal(t, pkt[5:7], []byte{0, 1}) // packet count 35 | require.Equal(t, pkt[7], uint8(other)) // error number 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /u2fhid/handler.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "fmt" 8 | "math" 9 | 10 | "github.com/gsora/fidati/internal/flog" 11 | ) 12 | 13 | // zeroPad pads b with as many zeroes as needed to have len(b) == 64. 14 | func zeroPad(b []byte) []byte { 15 | if len(b) == 64 { 16 | return b 17 | } 18 | 19 | nb := make([]byte, 64) 20 | copy(nb, b) 21 | 22 | return nb 23 | } 24 | 25 | // Tx handles USB endpoint data outtake. 26 | // res will always not be nil. 27 | func (h *Handler) Tx(buf []byte, lastErr error) (res []byte, err error) { 28 | h.stateLock.Lock() 29 | defer h.stateLock.Unlock() 30 | 31 | if h.state.outboundMsgs == nil || h.state.accumulatingMsgs { 32 | return 33 | } 34 | 35 | if h.state.lastOutboundIndex == 0 { 36 | flog.Logger.Println("found", len(h.state.outboundMsgs), "outbound messages") 37 | } 38 | 39 | if len(h.state.outboundMsgs) == 1 { 40 | b := &bytes.Buffer{} 41 | binary.Write(b, binary.LittleEndian, zeroPad(h.state.outboundMsgs[h.state.lastOutboundIndex])) 42 | 43 | res = b.Bytes() 44 | flog.Logger.Println(res) 45 | flog.Logger.Println("finished processing messages, clearing buffers") 46 | h.state.clear() 47 | return 48 | } 49 | 50 | if h.state.lastOutboundIndex == len(h.state.outboundMsgs) { 51 | flog.Logger.Println("finished processing messages, clearing buffers") 52 | h.state.clear() 53 | return 54 | } 55 | 56 | b := &bytes.Buffer{} 57 | binary.Write(b, binary.LittleEndian, zeroPad(h.state.outboundMsgs[h.state.lastOutboundIndex])) 58 | h.state.lastOutboundIndex++ 59 | 60 | res = b.Bytes() 61 | 62 | flog.Logger.Println("processed message", h.state.lastOutboundIndex) 63 | 64 | return 65 | } 66 | 67 | // Rx handles data intake, parses messages and builds responses. 68 | // res will always be nil. 69 | func (h *Handler) Rx(buf []byte, lastErr error) (res []byte, err error) { 70 | if buf == nil { 71 | return 72 | } 73 | 74 | h.stateLock.Lock() 75 | defer h.stateLock.Unlock() 76 | 77 | // From here onwards, all the call stack that originates from parseMsg has exclusive access to h.state. 78 | msgs, err := h.parseMsg(buf) 79 | if err != nil { 80 | flog.Logger.Println(err) 81 | h.state.clear() 82 | return 83 | } 84 | 85 | h.state.outboundMsgs = msgs 86 | 87 | return 88 | } 89 | 90 | // parseMsg parses msg and constructs a slice of messages ready to be sent over the wire. 91 | // Each response message is exactly 64 bytes in length. 92 | func (h *Handler) parseMsg(msg []byte) ([][]byte, error) { 93 | if len(msg) != 64 { // something's wrong 94 | return nil, fmt.Errorf("wrong message length, expected 64 but got %d", len(msg)) 95 | } 96 | 97 | cmd := msg[4] 98 | isInit := isInitPkt(cmd) 99 | 100 | flog.Logger.Println("msg ", msg) 101 | 102 | if isInit { 103 | return h.handleInitPacket(msg) 104 | } else { 105 | return h.handleContinuationPacket(msg) 106 | } 107 | } 108 | 109 | // handleContinuationPacket handles parsing and state update for continuation packets. 110 | func (h *Handler) handleContinuationPacket(msg []byte) ([][]byte, error) { 111 | flog.Logger.Println("found continuation packet") 112 | cp := parseContinuationPkt(msg) 113 | 114 | session, ok := h.state.sessions[cp.Channel()] 115 | if !ok { 116 | return nil, fmt.Errorf("new continuation packet with id 0x%X, which was not seen before", cp.ChannelID) 117 | } 118 | 119 | var seqError bool 120 | switch { 121 | case cp.SequenceNumber == 0: 122 | if session.packetZeroSeen { 123 | seqError = true 124 | } 125 | case cp.SequenceNumber-session.lastSequence != 1: 126 | seqError = true 127 | } 128 | 129 | if seqError { 130 | return nil, fmt.Errorf("found a continuation packet with non-sequential sequence number, expected %d but found %d", cp.SequenceNumber+1, session.lastSequence) 131 | } 132 | 133 | session.packetZeroSeen = true 134 | session.lastSequence = cp.SequenceNumber 135 | 136 | lastSize := len(session.data) 137 | // TODO: here we should count how many zeroes we should include in cp.Data, because some of them 138 | // are used in the U2F protocol. 139 | 140 | if session.leftToRead < continuationPacketDataLen { 141 | session.data = append(session.data, cp.Data[:session.leftToRead]...) 142 | session.leftToRead = 0 143 | } else { 144 | session.data = append(session.data, cp.Data...) 145 | session.leftToRead -= uint64(len(cp.Data)) 146 | } 147 | 148 | flog.Logger.Printf("read new %d bytes, last size %d, new size %d, total expected size %d", len(cp.Data), lastSize, len(session.data), session.total) 149 | 150 | if len(session.data) != int(session.total) { 151 | return nil, nil // we still need more data 152 | } 153 | 154 | flog.Logger.Printf("finished reading data for channel 0x%X, total bytes %d", cp.Channel(), len(session.data)) 155 | return h.packetBuilder(session, cp) 156 | } 157 | 158 | // handleInitPacket handles parsing and state setup for initialization packets. 159 | func (h *Handler) handleInitPacket(msg []byte) ([][]byte, error) { 160 | flog.Logger.Println("found init packet") 161 | ip := parseInitPkt(msg) 162 | 163 | s, ok := h.state.sessions[ip.Channel()] 164 | if !ok { 165 | s = &session{} 166 | } 167 | 168 | flog.Logger.Println("command:", ip.Cmd.String()) 169 | 170 | s.command = ip.Cmd 171 | s.total = uint64(ip.PayloadLength) 172 | s.data = make([]byte, 0, s.total) 173 | s.data = append(s.data, ip.Data...) 174 | s.leftToRead = uint64(int(ip.PayloadLength) - len(s.data)) 175 | 176 | h.state.sessions[ip.Channel()] = s 177 | 178 | if s.total <= initPacketDataLen { 179 | // handle everything as a single entity 180 | return h.packetBuilder(s, ip) 181 | } 182 | 183 | h.state.accumulatingMsgs = true 184 | 185 | return nil, nil 186 | } 187 | 188 | // numPackets returns the number of packets needed to properly respond to a message. 189 | func numPackets(rawMsgLen int) int { 190 | // 59 is the number of data bytes available in a continuation packet 191 | // 64 - (4 bytes channel id + 1 byte sequence number) 192 | return int(math.Ceil(float64(rawMsgLen) / float64(continuationPacketDataLen))) 193 | } 194 | 195 | // broadcastReq responds to broadcast messages, sent with channel id [255, 255, 255, 255]. 196 | func broadcastReq(ip initPacket) ([]byte, error) { 197 | if ip.Cmd != cmdInit { 198 | return nil, fmt.Errorf("found message for broadcast chan but command was %d instead of U2FHID_INIT", ip.Command()) 199 | } 200 | 201 | flog.Logger.Println("found cmdInit on broadcast channel") 202 | 203 | assignedChannelID := make([]byte, 4) 204 | _, err := rand.Read(assignedChannelID) 205 | if err != nil { 206 | return nil, fmt.Errorf("cannot generate random channel ID, %w", err) 207 | } 208 | 209 | flog.Logger.Println("created new channel id", assignedChannelID) 210 | 211 | b := new(bytes.Buffer) 212 | u := initResponse{ 213 | standardResponse: standardResponse{ 214 | Command: ip.Command(), 215 | ChannelID: ip.ChannelID, 216 | }, 217 | ProtocolVersion: 12, 218 | MajorDeviceVersion: 4, 219 | MinorDeviceVersion: 2, 220 | BuildDeviceVersion: 0, 221 | Capabilities: 0, 222 | } 223 | 224 | copy(u.Nonce[:], ip.Data) 225 | copy(u.AssignedChannelID[:], assignedChannelID) 226 | 227 | binary.BigEndian.PutUint16(u.Count[:], 17) 228 | err = binary.Write(b, binary.LittleEndian, u) 229 | if err != nil { 230 | return nil, fmt.Errorf("cannot serialize initResponse: %w", err) 231 | } 232 | 233 | flog.Logger.Println("finished broadcastReq") 234 | return b.Bytes(), nil 235 | } 236 | 237 | // packetBuilder builds response packages for a given session, depending on session.command. 238 | func (h *Handler) packetBuilder(session *session, pkt u2fPacket) ([][]byte, error) { 239 | flog.Logger.Println("message", u2fHIDCommand(pkt.Command())) 240 | 241 | if ch, handled := h.commandMappings[session.command]; handled { 242 | flog.Logger.Println("found command to be handled via command mappings:", session.command) 243 | 244 | pkts, err := genPackets( 245 | ch(session.data[:session.total]), 246 | session.command, 247 | pkt.ChannelBytes(), 248 | ) 249 | 250 | if err != nil { 251 | return nil, fmt.Errorf("error while handling msg, %w", err) 252 | } 253 | 254 | h.state.accumulatingMsgs = false 255 | h.state.lastChannelID = pkt.Channel() 256 | return pkts, nil 257 | } 258 | 259 | // use standard u2fhid commands 260 | switch session.command { 261 | case cmdInit: 262 | ip, ok := pkt.(initPacket) 263 | if !ok { 264 | return nil, fmt.Errorf("found cmdInit packet, but said packet cannot be read as one") 265 | } 266 | 267 | if ip.Channel() != broadcastChan { 268 | return nil, fmt.Errorf("found a cmdInit, but not on the broadcast channel") 269 | } 270 | 271 | h.state.lastChannelID = broadcastChan 272 | h.state.accumulatingMsgs = false 273 | 274 | ret, err := broadcastReq(ip) 275 | if err != nil { 276 | return nil, err 277 | } 278 | 279 | return [][]byte{ret}, nil 280 | case cmdPing: 281 | pkts, err := handlePing(session, pkt) 282 | if err != nil { 283 | return nil, fmt.Errorf("error while handling ping, %w", err) 284 | } 285 | 286 | h.state.accumulatingMsgs = false 287 | h.state.lastChannelID = pkt.Channel() 288 | return pkts, nil 289 | case cmdMsg: 290 | pkts, err := h.handleMsg(session, pkt) 291 | if err != nil { 292 | return nil, fmt.Errorf("error while handling msg, %w", err) 293 | } 294 | 295 | h.state.accumulatingMsgs = false 296 | h.state.lastChannelID = pkt.Channel() 297 | return pkts, nil 298 | default: 299 | flog.Logger.Printf("command %d not found, sending error payload", session.command) 300 | return generateError(invalidCmd, pkt), nil 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /u2fhid/handler_test.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_zeroPad(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | b []byte 15 | zeroAmount int 16 | want []byte 17 | }{ 18 | { 19 | "data must be padded", 20 | bytes.Repeat([]byte{1}, 42), 21 | 22, 22 | append(bytes.Repeat([]byte{1}, 42), make([]byte, 22)...), 23 | }, 24 | { 25 | "data must not be padded", 26 | bytes.Repeat([]byte{1}, 64), 27 | 0, 28 | bytes.Repeat([]byte{1}, 64), 29 | }, 30 | { 31 | "if more than 64 bytes are given, only the first 64 are returned", 32 | append(bytes.Repeat([]byte{1}, 64), bytes.Repeat([]byte{2}, 64)...), 33 | 0, 34 | bytes.Repeat([]byte{1}, 64), 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | p := zeroPad(tt.b) 40 | 41 | zeros := 0 42 | if len(tt.b) <= 64 { 43 | zeros = len(p[len(tt.b):]) 44 | } 45 | 46 | require.Equal(t, tt.zeroAmount, zeros) 47 | require.Equal(t, tt.want, p) 48 | }) 49 | } 50 | } 51 | 52 | func TestHandler_parseMsg(t *testing.T) { 53 | tests := []test{ 54 | { 55 | "more than 64 bytes means error", 56 | func(t *testing.T) { 57 | h, err := NewHandler(&fakeToken{}) 58 | require.NoError(t, err) 59 | 60 | b := make([]byte, 65) 61 | d, err := h.parseMsg(b) 62 | require.Nil(t, d) 63 | require.Error(t, err) 64 | require.Contains(t, err.Error(), "wrong message length") 65 | }, 66 | }, 67 | { 68 | "init packet not on broadcast channel returns error", 69 | func(t *testing.T) { 70 | h, err := NewHandler(&fakeToken{}) 71 | require.NoError(t, err) 72 | 73 | msg := zeroPad([]byte{1, 2, 3, 4, uint8(cmdInit), 0, 8, 1, 2, 3, 4, 5, 6, 7, 8}) 74 | 75 | d, err := h.parseMsg(msg) 76 | require.Error(t, err) 77 | require.Nil(t, d) 78 | require.Contains(t, err.Error(), "not on the broadcast channel") 79 | }, 80 | }, 81 | { 82 | "init packet is parsed correctly", 83 | func(t *testing.T) { 84 | h, err := NewHandler(&fakeToken{}) 85 | require.NoError(t, err) 86 | 87 | channelInt := binary.BigEndian.Uint32([]byte{255, 255, 255, 255}) 88 | 89 | msg := zeroPad([]byte{255, 255, 255, 255, uint8(cmdInit), 0, 8, 1, 2, 3, 4, 5, 6, 7, 8}) 90 | 91 | d, err := h.parseMsg(msg) 92 | require.NoError(t, err) 93 | require.NotNil(t, d) 94 | 95 | require.False(t, h.state.accumulatingMsgs) 96 | require.Contains(t, h.state.sessions, channelInt) 97 | 98 | s := h.state.sessions[channelInt] 99 | 100 | require.Equal(t, uint64(8), s.total) 101 | require.Equal(t, []byte{1, 2, 3, 4, 5, 6, 7, 8}, s.data[:8]) 102 | }, 103 | }, 104 | { 105 | "continuation packet without a previous init packet returns error", 106 | func(t *testing.T) { 107 | h, err := NewHandler(&fakeToken{}) 108 | require.NoError(t, err) 109 | 110 | msg := zeroPad([]byte{1, 2, 3, 4, 0, 8, 1, 2, 3, 4, 5, 6, 7, 8}) 111 | 112 | d, err := h.parseMsg(msg) 113 | require.Error(t, err) 114 | require.Nil(t, d) 115 | require.Contains(t, err.Error(), "which was not seen before") 116 | }, 117 | }, 118 | { 119 | "init packet and then continuation packet are parsed correctly", 120 | func(t *testing.T) { 121 | token := &fakeToken{} 122 | h, err := NewHandler(token) 123 | require.NoError(t, err) 124 | 125 | firstHalf := bytes.Repeat([]byte{1}, 57) 126 | secondHalf := bytes.Repeat([]byte{1}, 5) 127 | 128 | channel := []byte{1, 2, 3, 4} 129 | 130 | channelInt := binary.BigEndian.Uint32(channel) 131 | 132 | initNoPad := append(channel, uint8(cmdMsg)) 133 | initNoPad = append(initNoPad, []byte{0, 62}...) 134 | initNoPad = append(initNoPad, firstHalf...) 135 | init := zeroPad(initNoPad) 136 | 137 | contNoPad := append(channel, 0) 138 | contNoPad = append(contNoPad, secondHalf...) 139 | cont := zeroPad(contNoPad) 140 | 141 | d, err := h.parseMsg(init) 142 | require.NoError(t, err) 143 | require.Nil(t, d) 144 | 145 | require.True(t, h.state.accumulatingMsgs) 146 | require.Contains(t, h.state.sessions, channelInt) 147 | 148 | s := h.state.sessions[channelInt] 149 | 150 | require.Equal(t, uint64(57+5), s.total) 151 | require.Equal(t, cmdMsg, s.command) 152 | require.Equal(t, firstHalf, s.data) 153 | require.Equal(t, uint64(5), s.leftToRead) 154 | 155 | // set bogus data for token to return 156 | token.shouldReturnData = true 157 | token.data = append(firstHalf, secondHalf...) 158 | 159 | d, err = h.parseMsg(cont) 160 | require.NoError(t, err) 161 | require.NotNil(t, d) 162 | 163 | require.False(t, h.state.accumulatingMsgs) 164 | require.Contains(t, h.state.sessions, channelInt) 165 | 166 | s = h.state.sessions[channelInt] 167 | 168 | require.Equal(t, uint64(57+5), s.total) 169 | require.Equal(t, cmdMsg, s.command) 170 | require.Equal(t, append(firstHalf, secondHalf...), s.data) 171 | require.Equal(t, uint64(0), s.leftToRead) 172 | }, 173 | }, 174 | } 175 | 176 | for _, tt := range tests { 177 | t.Run(tt.name, tt.f) 178 | } 179 | } 180 | 181 | func TestHandler_handleContinuationPacket(t *testing.T) { 182 | tests := []test{ 183 | { 184 | "continuation packet with previously not seen channel ID", 185 | func(t *testing.T) { 186 | token := &fakeToken{} 187 | h, err := NewHandler(token) 188 | require.NoError(t, err) 189 | 190 | secondHalf := bytes.Repeat([]byte{1}, 5) 191 | 192 | channel := []byte{1, 2, 3, 4} 193 | 194 | contNoPad := append(channel, 0) 195 | contNoPad = append(contNoPad, secondHalf...) 196 | cont := zeroPad(contNoPad) 197 | 198 | d, err := h.handleContinuationPacket(cont) 199 | require.Error(t, err) 200 | require.Contains(t, err.Error(), "not seen before") 201 | 202 | require.Nil(t, d) 203 | }, 204 | }, 205 | { 206 | "continuation packet with previously seen channel ID has unexpected sequence number", 207 | func(t *testing.T) { 208 | token := &fakeToken{} 209 | h, err := NewHandler(token) 210 | require.NoError(t, err) 211 | 212 | firstHalf := bytes.Repeat([]byte{1}, 57) 213 | secondHalf := bytes.Repeat([]byte{1}, 59) 214 | 215 | channel := []byte{1, 2, 3, 4} 216 | 217 | initNoPad := append(channel, uint8(cmdMsg)) 218 | initNoPad = append(initNoPad, []byte{0, 255}...) 219 | initNoPad = append(initNoPad, firstHalf...) 220 | init := zeroPad(initNoPad) 221 | 222 | contNoPad := append(channel, 0) 223 | contNoPad = append(contNoPad, secondHalf...) 224 | cont := zeroPad(contNoPad) 225 | 226 | // send init to allocate channel 227 | d, err := h.parseMsg(init) 228 | require.NoError(t, err) 229 | require.Nil(t, d) 230 | 231 | // first continuation packet, with sequence number 0 232 | d, err = h.parseMsg(cont) 233 | require.NoError(t, err) 234 | require.Nil(t, d) 235 | 236 | // send a continuation packet with sequence id 2, must error 237 | contNoPad = make([]byte, 64) 238 | contNoPad = append(channel, 2) 239 | contNoPad = append(contNoPad, secondHalf...) 240 | cont = zeroPad(contNoPad) 241 | 242 | d, err = h.handleContinuationPacket(cont) 243 | require.Error(t, err) 244 | require.Contains(t, err.Error(), "non-sequential sequence number") 245 | require.Nil(t, d) 246 | 247 | // send a continuation packet with sequence id 0 (already seen), must error 248 | contNoPad = make([]byte, 64) 249 | contNoPad = append(channel, 0) 250 | contNoPad = append(contNoPad, secondHalf...) 251 | cont = zeroPad(contNoPad) 252 | 253 | d, err = h.handleContinuationPacket(cont) 254 | require.Error(t, err) 255 | require.Contains(t, err.Error(), "non-sequential sequence number") 256 | require.Nil(t, d) 257 | }, 258 | }, 259 | } 260 | 261 | for _, tt := range tests { 262 | t.Run(tt.name, tt.f) 263 | } 264 | } 265 | 266 | func TestHandler_handleInitPacket(t *testing.T) { 267 | tests := []test{ 268 | { 269 | "packet allocates completely and response can be built in one take", 270 | func(t *testing.T) { 271 | token := &fakeToken{} 272 | h, err := NewHandler(token) 273 | require.NoError(t, err) 274 | 275 | firstHalf := bytes.Repeat([]byte{1}, 57) 276 | 277 | channel := []byte{1, 2, 3, 4} 278 | 279 | initNoPad := append(channel, uint8(cmdMsg)) 280 | initNoPad = append(initNoPad, []byte{0, 57}...) 281 | initNoPad = append(initNoPad, firstHalf...) 282 | init := zeroPad(initNoPad) 283 | 284 | // set bogus data for token to return 285 | token.shouldReturnData = true 286 | token.data = firstHalf 287 | d, err := h.handleInitPacket(init) 288 | require.NoError(t, err) 289 | require.NotNil(t, d) 290 | 291 | channelInt := binary.BigEndian.Uint32(channel) 292 | s, found := h.state.sessions[channelInt] 293 | require.NotNil(t, s) 294 | require.True(t, found) 295 | require.False(t, h.state.accumulatingMsgs) 296 | }, 297 | }, 298 | { 299 | "packet response cannot be built in one take", 300 | func(t *testing.T) { 301 | token := &fakeToken{} 302 | h, err := NewHandler(token) 303 | require.NoError(t, err) 304 | 305 | firstHalf := bytes.Repeat([]byte{1}, 57) 306 | 307 | channel := []byte{1, 2, 3, 4} 308 | 309 | initNoPad := append(channel, uint8(cmdMsg)) 310 | initNoPad = append(initNoPad, []byte{0, 58}...) 311 | initNoPad = append(initNoPad, firstHalf...) 312 | init := zeroPad(initNoPad) 313 | 314 | d, err := h.handleInitPacket(init) 315 | require.NoError(t, err) 316 | require.Nil(t, d) 317 | 318 | channelInt := binary.BigEndian.Uint32(channel) 319 | s, found := h.state.sessions[channelInt] 320 | require.NotNil(t, s) 321 | require.True(t, found) 322 | require.True(t, h.state.accumulatingMsgs) 323 | }, 324 | }, 325 | } 326 | 327 | for _, tt := range tests { 328 | t.Run(tt.name, tt.f) 329 | } 330 | } 331 | 332 | func Test_numPackets(t *testing.T) { 333 | 334 | tests := []struct { 335 | name string 336 | rawMsgLen int 337 | want int 338 | }{ 339 | { 340 | "59 bytes, single message", 341 | 59, 342 | 1, 343 | }, 344 | { 345 | "0 bytes, no message", 346 | 0, 347 | 0, 348 | }, 349 | { 350 | "2*59+1 bytes, 3 messages", 351 | 119, 352 | 3, 353 | }, 354 | } 355 | for _, tt := range tests { 356 | t.Run(tt.name, func(t *testing.T) { 357 | require.Equal(t, tt.want, numPackets(tt.rawMsgLen)) 358 | }) 359 | } 360 | } 361 | 362 | func Test_broadcastReq(t *testing.T) { 363 | t.Run("initPacket command isn't cmdInit", func(t *testing.T) { 364 | i := initPacket{ 365 | ChannelID: [4]byte{ 366 | 255, 367 | 255, 368 | 255, 369 | 255, 370 | }, 371 | Cmd: cmdPing, 372 | PayloadLength: 0, 373 | Data: nil, 374 | } 375 | 376 | d, err := broadcastReq(i) 377 | require.Nil(t, d) 378 | require.Error(t, err) 379 | require.Contains(t, err.Error(), "instead of U2FHID_INIT") 380 | }) 381 | 382 | t.Run("initPacket command is cmdInit", func(t *testing.T) { 383 | i := initPacket{ 384 | ChannelID: [4]byte{ 385 | 255, 386 | 255, 387 | 255, 388 | 255, 389 | }, 390 | Cmd: cmdInit, 391 | PayloadLength: 8, 392 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 393 | } 394 | 395 | d, err := broadcastReq(i) 396 | require.NotNil(t, d) 397 | require.NoError(t, err) 398 | 399 | // channel is equal 400 | c := [4]byte{} 401 | copy(c[:], d[:4]) 402 | require.Equal(t, i.ChannelID, c) 403 | 404 | // command is cmdInit 405 | require.Equal(t, i.Command(), d[4]) 406 | 407 | // first byte of payload size is zero, second is 17 408 | require.Zero(t, d[5]) 409 | require.Equal(t, uint8(17), d[6]) 410 | 411 | // nonce is equal to what we put in data 412 | require.Equal(t, i.Data, d[7:7+8]) 413 | 414 | // assigned channel id isn't all zeroes 415 | require.NotEqual(t, []byte{0, 0, 0, 0}, d[15:19]) 416 | 417 | // remaining version bytes are equal 418 | require.Equal(t, uint8(12), d[19]) 419 | require.Equal(t, uint8(4), d[20]) 420 | require.Equal(t, uint8(2), d[21]) 421 | require.Equal(t, uint8(0), d[22]) 422 | require.Equal(t, uint8(0), d[23]) 423 | }) 424 | } 425 | 426 | func TestHandler_packetBuilder(t *testing.T) { 427 | tests := []test{ 428 | { 429 | "unrecognized command", 430 | func(t *testing.T) { 431 | token := &fakeToken{} 432 | h, err := NewHandler(token) 433 | require.NoError(t, err) 434 | 435 | s := session{ 436 | command: u2fHIDCommand(42), 437 | } 438 | 439 | dd, err := h.packetBuilder(&s, initPacket{}) 440 | require.NotNil(t, dd) 441 | require.NoError(t, err) 442 | require.Len(t, dd, 1) 443 | 444 | d := dd[0] 445 | 446 | t.Log(d) 447 | 448 | require.Equal(t, []byte{0, 0, 0, 0}, d[:4]) 449 | 450 | d = d[4:] 451 | require.Equal(t, uint8(cmdError), d[0]) 452 | require.Equal(t, uint8(0), d[1]) 453 | require.Equal(t, uint8(1), d[2]) 454 | }, 455 | }, 456 | { 457 | "session says cmdInit, but pkt isn't initPacket", 458 | func(t *testing.T) { 459 | token := &fakeToken{} 460 | h, err := NewHandler(token) 461 | require.NoError(t, err) 462 | 463 | s := session{ 464 | command: cmdInit, 465 | } 466 | 467 | dd, err := h.packetBuilder(&s, continuationPacket{}) 468 | require.Nil(t, dd) 469 | require.Error(t, err) 470 | require.Contains(t, err.Error(), "said packet cannot be read as one") 471 | }, 472 | }, 473 | { 474 | "session says cmdInit, but channel isn't broadcast", 475 | func(t *testing.T) { 476 | token := &fakeToken{} 477 | h, err := NewHandler(token) 478 | require.NoError(t, err) 479 | 480 | s := session{ 481 | command: cmdInit, 482 | } 483 | 484 | p := initPacket{ 485 | ChannelID: [4]byte{1, 2, 3, 4}, 486 | } 487 | 488 | dd, err := h.packetBuilder(&s, p) 489 | require.Nil(t, dd) 490 | require.Error(t, err) 491 | require.Contains(t, err.Error(), "not on the broadcast channel") 492 | }, 493 | }, 494 | { 495 | "session is cmdInit", 496 | func(t *testing.T) { 497 | token := &fakeToken{} 498 | h, err := NewHandler(token) 499 | require.NoError(t, err) 500 | 501 | s := session{ 502 | command: cmdInit, 503 | } 504 | 505 | i := initPacket{ 506 | ChannelID: [4]byte{ 507 | 255, 508 | 255, 509 | 255, 510 | 255, 511 | }, 512 | Cmd: cmdInit, 513 | PayloadLength: 8, 514 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 515 | } 516 | 517 | dd, err := h.packetBuilder(&s, i) 518 | require.NotNil(t, dd) 519 | require.NoError(t, err) 520 | }, 521 | }, 522 | { 523 | "session is cmdPing", 524 | func(t *testing.T) { 525 | token := &fakeToken{} 526 | h, err := NewHandler(token) 527 | require.NoError(t, err) 528 | 529 | i := initPacket{ 530 | ChannelID: [4]byte{ 531 | 1, 532 | 2, 533 | 3, 534 | 4, 535 | }, 536 | Cmd: cmdPing, 537 | PayloadLength: 8, 538 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 539 | } 540 | 541 | s := session{ 542 | command: cmdPing, 543 | data: i.Data, 544 | total: uint64(len(i.Data)), 545 | } 546 | 547 | dd, err := h.packetBuilder(&s, i) 548 | require.NoError(t, err) 549 | require.NotNil(t, dd) 550 | 551 | require.Len(t, dd, 1) 552 | 553 | d := dd[0][7:] 554 | 555 | d = bytes.Trim(d, "\x00") 556 | 557 | require.Equal(t, i.Data, d) 558 | }, 559 | }, 560 | { 561 | "session is cmdPing but something went wrong", 562 | func(t *testing.T) { 563 | token := &fakeToken{} 564 | h, err := NewHandler(token) 565 | require.NoError(t, err) 566 | 567 | i := initPacket{ 568 | ChannelID: [4]byte{ 569 | 1, 570 | 2, 571 | 3, 572 | 4, 573 | }, 574 | Cmd: cmdPing, 575 | PayloadLength: 8, 576 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8}, 577 | } 578 | 579 | s := session{ 580 | command: cmdPing, 581 | total: uint64(len(i.Data)), 582 | } 583 | 584 | dd, err := h.packetBuilder(&s, i) 585 | require.Error(t, err) 586 | require.Nil(t, dd) 587 | }, 588 | }, 589 | { 590 | "session is cmdMsg", 591 | func(t *testing.T) { 592 | token := &fakeToken{} 593 | h, err := NewHandler(token) 594 | require.NoError(t, err) 595 | 596 | firstHalf := bytes.Repeat([]byte{1}, 57) 597 | 598 | i := initPacket{ 599 | ChannelID: [4]byte{ 600 | 1, 601 | 2, 602 | 3, 603 | 4, 604 | }, 605 | Cmd: cmdMsg, 606 | PayloadLength: 57, 607 | Data: firstHalf, 608 | } 609 | 610 | s := session{ 611 | data: firstHalf, 612 | command: cmdMsg, 613 | total: 57, 614 | leftToRead: 0, 615 | lastSequence: 0, 616 | packetZeroSeen: false, 617 | } 618 | 619 | // set bogus data for token to return 620 | token.shouldReturnData = true 621 | token.data = firstHalf 622 | 623 | d, err := h.packetBuilder(&s, i) 624 | require.NoError(t, err) 625 | require.NotNil(t, d) 626 | }, 627 | }, 628 | { 629 | "session is cmdMsg but something goes wrong", 630 | func(t *testing.T) { 631 | token := &fakeToken{} 632 | h, err := NewHandler(token) 633 | require.NoError(t, err) 634 | 635 | firstHalf := bytes.Repeat([]byte{1}, 57) 636 | 637 | i := initPacket{ 638 | ChannelID: [4]byte{ 639 | 1, 640 | 2, 641 | 3, 642 | 4, 643 | }, 644 | Cmd: cmdMsg, 645 | PayloadLength: 57, 646 | Data: firstHalf, 647 | } 648 | 649 | s := session{ 650 | data: firstHalf, 651 | command: cmdMsg, 652 | total: 57, 653 | leftToRead: 0, 654 | lastSequence: 0, 655 | packetZeroSeen: false, 656 | } 657 | 658 | // set bogus data for token to return 659 | token.shouldReturnData = false 660 | token.data = firstHalf 661 | 662 | d, err := h.packetBuilder(&s, i) 663 | require.Error(t, err) 664 | require.Nil(t, d) 665 | }, 666 | }, 667 | } 668 | 669 | for _, tt := range tests { 670 | t.Run(tt.name, tt.f) 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /u2fhid/packets.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/gsora/fidati/internal/flog" 11 | ) 12 | 13 | const ( 14 | initPacketDataLen = 57 15 | continuationPacketDataLen = 59 16 | ) 17 | 18 | // continuationPacket is a U2FHID message packet for which the command byte has the seventh bit not set. 19 | // Base size = 5, data size = 64 - 5 = 59. 20 | type continuationPacket struct { 21 | ChannelID [4]byte 22 | SequenceNumber uint8 23 | Data []byte 24 | } 25 | 26 | // ChannelBytes implements u2fPacket interface. 27 | func (i continuationPacket) ChannelBytes() [4]byte { 28 | return i.ChannelID 29 | } 30 | 31 | // Command implements u2fPacket interface. 32 | func (i continuationPacket) Command() uint8 { 33 | return 0 34 | } 35 | 36 | // Length implements u2fPacket interface. 37 | func (i continuationPacket) Length() uint16 { 38 | return 0 39 | } 40 | 41 | // Count implements u2fPacket interface. 42 | func (i continuationPacket) Count() uint16 { 43 | return uint16(i.SequenceNumber) 44 | } 45 | 46 | // Channel implements u2fPacket interface. 47 | func (i continuationPacket) Channel() uint32 { 48 | return binary.BigEndian.Uint32(i.ChannelID[:]) 49 | } 50 | 51 | // String implements fmt.Stringer interface. 52 | func (i continuationPacket) String() string { 53 | s := strings.Builder{} 54 | s.WriteString(fmt.Sprintf("channel id: %v, ", i.ChannelID)) 55 | s.WriteString(fmt.Sprintf("sequence number: %d, ", i.SequenceNumber)) 56 | s.WriteString(fmt.Sprintf("data: %v", i.Data)) 57 | return s.String() 58 | } 59 | 60 | // Bytes returns the byte slice representation of a continuation packet. 61 | func (i continuationPacket) Bytes() []byte { 62 | s := struct { 63 | ChannelID [4]byte 64 | SequenceNumber uint8 65 | }{ 66 | i.ChannelID, 67 | i.SequenceNumber, 68 | } 69 | 70 | b := new(bytes.Buffer) 71 | err := binary.Write(b, binary.BigEndian, s) 72 | if err != nil { 73 | panic(fmt.Sprintf("cannot format continuationPacket, %s", err.Error())) 74 | } 75 | 76 | return append(b.Bytes(), i.Data...) 77 | } 78 | 79 | // parseContinuationPkt parses msg as a continuation packet, and returns a continuationPacket instance. 80 | // FIDO U2F HID Protocol Specification, pg 4, "2.4 Message- and packet structure" 81 | func parseContinuationPkt(msg []byte) continuationPacket { 82 | i := continuationPacket{ 83 | SequenceNumber: msg[4], 84 | } 85 | 86 | copy(i.ChannelID[:], msg[0:4]) 87 | 88 | i.Data = msg[5:] 89 | 90 | return i 91 | } 92 | 93 | // initPacket is a U2FHID message packet for which the command byte has the seventh bit set. 94 | type initPacket struct { 95 | ChannelID [4]byte 96 | Cmd u2fHIDCommand 97 | PayloadLength uint16 98 | Data []byte 99 | } 100 | 101 | // ChannelBytes implements u2fPacket interface. 102 | func (i initPacket) ChannelBytes() [4]byte { 103 | return i.ChannelID 104 | } 105 | 106 | // Command implements u2fPacket interface. 107 | func (i initPacket) Command() uint8 { 108 | return uint8(i.Cmd) 109 | } 110 | 111 | // Length implements u2fPacket interface. 112 | func (i initPacket) Length() uint16 { 113 | return i.PayloadLength 114 | } 115 | 116 | // Count implements u2fPacket interface. 117 | func (i initPacket) Count() uint16 { 118 | return 0 119 | } 120 | 121 | // Channel implements u2fPacket interface. 122 | func (i initPacket) Channel() uint32 { 123 | return binary.BigEndian.Uint32(i.ChannelID[:]) 124 | } 125 | 126 | // String implements fmt.Stringer interface. 127 | func (i initPacket) String() string { 128 | s := strings.Builder{} 129 | s.WriteString(fmt.Sprintf("channel id: %v, ", i.ChannelID)) 130 | s.WriteString(fmt.Sprintf("command: %d, ", i.Command())) 131 | s.WriteString(fmt.Sprintf("payload length: %d, ", i.PayloadLength)) 132 | s.WriteString(fmt.Sprintf("data: %v", i.Data)) 133 | return s.String() 134 | } 135 | 136 | // parseInitPkt parses msg as a init packet, and returns a initPacket instance. 137 | // FIDO U2F HID Protocol Specification, pg 4, "2.4 Message- and packet structure" 138 | func parseInitPkt(msg []byte) initPacket { 139 | i := initPacket{ 140 | Cmd: u2fHIDCommand(msg[4]), 141 | } 142 | 143 | copy(i.ChannelID[:], msg[0:4]) 144 | 145 | i.PayloadLength = binary.BigEndian.Uint16([]byte{msg[5], msg[6]}) 146 | 147 | i.Data = msg[7:] 148 | 149 | return i 150 | } 151 | 152 | // isInitPkt returns true if the seventh bit of cmd is set. 153 | // FIDO U2F HID Protocol Specification, pg 4, "2.4 Message- and packet structure" 154 | func isInitPkt(cmd uint8) bool { 155 | return cmd>>7 == 1 156 | } 157 | 158 | // genPackets generates response packets for msg, command cmd and channel id chanID. 159 | func genPackets(msg []byte, cmd u2fHIDCommand, chanID [4]byte) ([][]byte, error) { 160 | if msg == nil { 161 | return nil, errors.New("message is nil") 162 | } 163 | 164 | numPktsNoInitial := numPackets(len(msg) - 64) // we exclude a packet, which will be built separately 165 | 166 | ret := make([][]byte, 0, numPktsNoInitial+1) 167 | 168 | flog.Logger.Println("expected number of packets:", numPktsNoInitial+1) 169 | 170 | sequence := 0 171 | for i, packetPayload := range split(initPacketDataLen, continuationPacketDataLen, msg) { 172 | if i == 0 { 173 | b := new(bytes.Buffer) 174 | u := standardResponse{ 175 | Command: uint8(cmd), 176 | ChannelID: chanID, 177 | } 178 | 179 | binary.BigEndian.PutUint16(u.Count[:], uint16(len(msg))) 180 | flog.Logger.Println("length", uint16(len(msg)), "bytes", u.Count) 181 | err := binary.Write(b, binary.LittleEndian, u) 182 | if err != nil { 183 | return nil, fmt.Errorf("cannot serialize msg payload, %w", err) 184 | } 185 | 186 | initPingMsg := append(b.Bytes(), packetPayload...) 187 | ret = append(ret, initPingMsg) 188 | 189 | flog.Logger.Println("built packet", i) 190 | continue 191 | } 192 | 193 | var cc continuationPacket 194 | cc.SequenceNumber = uint8(sequence) 195 | cc.ChannelID = chanID 196 | cc.Data = packetPayload 197 | ret = append(ret, cc.Bytes()) 198 | flog.Logger.Println("built packet", i) 199 | sequence++ 200 | } 201 | 202 | return ret, nil 203 | } 204 | 205 | // split splits msg into 64 bytes units. 206 | func split(sizeFirst int, sizeRest int, msg []byte) [][]byte { 207 | if msg == nil { 208 | return [][]byte{} 209 | } 210 | 211 | numPktsNoInitial := numPackets(len(msg) - 57) // we exclude a packet, which will be built separately 212 | 213 | pktAmount := numPktsNoInitial + 1 214 | ret := make([][]byte, 0, pktAmount) 215 | 216 | if len(msg) < sizeFirst { // whole message can stay in a single packet 217 | r := make([]byte, sizeFirst) 218 | copy(r, msg) 219 | ret = append(ret, r) 220 | pktAmount = -1 221 | } 222 | 223 | lastIndex := 0 224 | 225 | for i := 0; i < pktAmount; i++ { 226 | if i == 0 { 227 | ret = append(ret, msg[0:sizeFirst]) 228 | lastIndex = sizeFirst 229 | continue 230 | } 231 | 232 | if i+1 == pktAmount { 233 | ret = append(ret, msg[lastIndex:]) 234 | return ret 235 | } 236 | 237 | ret = append(ret, msg[lastIndex:lastIndex+sizeRest]) 238 | lastIndex = lastIndex + sizeRest 239 | } 240 | 241 | return ret 242 | } 243 | -------------------------------------------------------------------------------- /u2fhid/packets_test.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_continuationPacket(t *testing.T) { 12 | cp := continuationPacket{ 13 | ChannelID: [4]byte{ 14 | 1, 15 | 2, 16 | 3, 17 | 4, 18 | }, 19 | SequenceNumber: 42, 20 | Data: bytes.Repeat([]byte("data"), 42), 21 | } 22 | 23 | tests := []struct { 24 | name string 25 | f func(*testing.T) 26 | }{ 27 | { 28 | "ChannelBytes", 29 | func(t *testing.T) { 30 | require.Equal(t, [4]byte{1, 2, 3, 4}, cp.ChannelBytes()) 31 | }, 32 | }, 33 | { 34 | "Command", 35 | func(t *testing.T) { 36 | require.Zero(t, cp.Command()) 37 | }, 38 | }, 39 | { 40 | "Length", 41 | func(t *testing.T) { 42 | require.Zero(t, cp.Length()) 43 | }, 44 | }, 45 | { 46 | "Count", 47 | func(t *testing.T) { 48 | require.Equal(t, uint16(cp.SequenceNumber), cp.Count()) 49 | }, 50 | }, 51 | { 52 | "Channel", 53 | func(t *testing.T) { 54 | require.Equal(t, uint32(16909060), cp.Channel()) 55 | }, 56 | }, 57 | { 58 | "Bytes", 59 | func(t *testing.T) { 60 | b := cp.Bytes() 61 | require.Equal(t, []byte{1, 2, 3, 4}, b[:4]) 62 | require.Equal(t, uint8(42), b[4]) 63 | require.Equal(t, bytes.Repeat([]byte("data"), 42), b[5:]) 64 | }, 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run(tt.name, tt.f) 70 | } 71 | } 72 | 73 | func Test_parseContinuationPkt(t *testing.T) { 74 | t.Run("parses correctly", func(t *testing.T) { 75 | b := []byte{1, 2, 3, 4, 42, 42, 42, 42, 42} 76 | var cp continuationPacket 77 | 78 | require.NotPanics(t, func() { 79 | cp = parseContinuationPkt(b) 80 | }) 81 | 82 | require.Equal(t, [4]byte{1, 2, 3, 4}, cp.ChannelID) 83 | require.Equal(t, uint8(42), cp.SequenceNumber) 84 | require.Equal(t, []byte{42, 42, 42, 42}, cp.Data) 85 | }) 86 | } 87 | 88 | func Test_initPacket(t *testing.T) { 89 | cp := initPacket{ 90 | ChannelID: [4]byte{ 91 | 1, 92 | 2, 93 | 3, 94 | 4, 95 | }, 96 | Cmd: cmdInit, 97 | PayloadLength: 42, 98 | Data: bytes.Repeat([]byte("data"), 42), 99 | } 100 | 101 | tests := []struct { 102 | name string 103 | f func(*testing.T) 104 | }{ 105 | { 106 | "ChannelBytes", 107 | func(t *testing.T) { 108 | require.Equal(t, [4]byte{1, 2, 3, 4}, cp.ChannelBytes()) 109 | }, 110 | }, 111 | { 112 | "Command", 113 | func(t *testing.T) { 114 | require.Equal(t, uint8(cmdInit), cp.Command()) 115 | }, 116 | }, 117 | { 118 | "Length", 119 | func(t *testing.T) { 120 | require.Equal(t, uint16(42), cp.Length()) 121 | }, 122 | }, 123 | { 124 | "Count", 125 | func(t *testing.T) { 126 | require.Zero(t, cp.Count()) 127 | }, 128 | }, 129 | { 130 | "Channel", 131 | func(t *testing.T) { 132 | require.Equal(t, uint32(16909060), cp.Channel()) 133 | }, 134 | }, 135 | } 136 | 137 | for _, tt := range tests { 138 | t.Run(tt.name, tt.f) 139 | } 140 | } 141 | 142 | func Test_parseInitPkt(t *testing.T) { 143 | t.Run("parses correctly", func(t *testing.T) { 144 | b := []byte{1, 2, 3, 4, 11, 11, 11, 42, 42, 42, 42} 145 | var cp initPacket 146 | 147 | require.NotPanics(t, func() { 148 | cp = parseInitPkt(b) 149 | }) 150 | 151 | require.Equal(t, [4]byte{1, 2, 3, 4}, cp.ChannelID) 152 | require.Equal(t, u2fHIDCommand(11), cp.Cmd) 153 | require.Equal(t, uint16(2827), cp.PayloadLength) 154 | require.Equal(t, []byte{42, 42, 42, 42}, cp.Data) 155 | }) 156 | } 157 | 158 | func Test_isInitPkt(t *testing.T) { 159 | tests := []struct { 160 | name string 161 | cmd uint8 162 | assertion require.BoolAssertionFunc 163 | }{ 164 | { 165 | "is init packet", 166 | 0b10000000, 167 | require.True, 168 | }, 169 | { 170 | "is not init packet", 171 | 0b00000001, 172 | require.False, 173 | }, 174 | } 175 | for _, tt := range tests { 176 | t.Run(tt.name, func(t *testing.T) { 177 | tt.assertion(t, isInitPkt(tt.cmd)) 178 | }) 179 | } 180 | } 181 | 182 | func Test_split(t *testing.T) { 183 | tests := []struct { 184 | name string 185 | msg []byte 186 | numPackets int 187 | }{ 188 | { 189 | "nil msg", 190 | nil, 191 | 0, 192 | }, 193 | { 194 | "all in one packet", 195 | bytes.Repeat([]byte{1}, 40), 196 | 1, 197 | }, 198 | { 199 | "multiple packets", 200 | bytes.Repeat([]byte{1}, 180), 201 | 4, 202 | }, 203 | } 204 | for _, tt := range tests { 205 | t.Run(tt.name, func(t *testing.T) { 206 | d := [][]byte{} 207 | 208 | require.NotPanics(t, func() { 209 | d = split(initPacketDataLen, continuationPacketDataLen, tt.msg) 210 | }) 211 | 212 | require.Equal(t, tt.numPackets, len(d)) 213 | }) 214 | } 215 | } 216 | 217 | func Test_genPackets(t *testing.T) { 218 | cmd, chanID := cmdInit, [4]byte{1, 2, 3, 4} 219 | 220 | tests := []struct { 221 | name string 222 | msg []byte 223 | pktAmount int 224 | errAssertion require.ErrorAssertionFunc 225 | }{ 226 | { 227 | "empty message", 228 | nil, 229 | 0, 230 | require.Error, 231 | }, 232 | { 233 | "single packet generated", 234 | bytes.Repeat([]byte{1}, 12), 235 | 1, 236 | require.NoError, 237 | }, 238 | { 239 | "multiple packet generated", 240 | bytes.Repeat([]byte{1}, 4242), 241 | 72, 242 | require.NoError, 243 | }, 244 | } 245 | for _, tt := range tests { 246 | t.Run(tt.name, func(t *testing.T) { 247 | data, err := genPackets(tt.msg, cmd, chanID) 248 | 249 | tt.errAssertion(t, err) 250 | 251 | if data == nil { 252 | return 253 | } 254 | 255 | require.Equal(t, tt.pktAmount, len(data)) // exclude the init packet 256 | 257 | lastIndex := -1 258 | for i, pkt := range data { 259 | if i == 0 { 260 | // first packet has length 261 | l := pkt[5:7] 262 | l16 := binary.BigEndian.Uint16(l) 263 | require.Equal(t, uint16(len(tt.msg)), l16) 264 | continue 265 | } 266 | 267 | li := int(pkt[4]) 268 | if lastIndex == -1 { 269 | lastIndex = li 270 | continue 271 | } 272 | 273 | // each sequence number must be distant exactly one from the last 274 | difference := li - lastIndex 275 | require.Equal(t, 1, difference) 276 | lastIndex = li 277 | } 278 | }) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /u2fhid/responses.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | // initResponse represents the standard response to a cmdInit command. 4 | type initResponse struct { 5 | standardResponse 6 | Nonce [8]byte 7 | AssignedChannelID [4]byte 8 | ProtocolVersion uint8 9 | MajorDeviceVersion uint8 10 | MinorDeviceVersion uint8 11 | BuildDeviceVersion uint8 12 | Capabilities uint8 13 | } 14 | 15 | // standardResponse is the response header used by all response command. 16 | type standardResponse struct { 17 | ChannelID [4]byte 18 | Command uint8 19 | Count [2]byte 20 | } 21 | -------------------------------------------------------------------------------- /u2fhid/types.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "sync" 9 | ) 10 | 11 | // Token represents a unit which can handle U2F messages. 12 | type Token interface { 13 | // HandleMessage handles cmdMsg payloads, and return an appropriate response 14 | // for the underlying command. 15 | HandleMessage([]byte) []byte 16 | } 17 | 18 | // u2fHIDReport is a byte slice holding a standard U2F HID report. 19 | type u2fHIDReport []byte 20 | 21 | // Bytes returns the byte slice representation of r. 22 | func (r *u2fHIDReport) Bytes() []byte { 23 | buf := new(bytes.Buffer) 24 | err := binary.Write(buf, binary.BigEndian, r) 25 | if err != nil { 26 | panic(fmt.Sprintf("cannot format u2f hid report, %v", err)) 27 | } 28 | return buf.Bytes() 29 | } 30 | 31 | // DefaultReport is the standard report descriptor for a USB HID FIDO2 token. 32 | // https://chromium.googlesource.com/chromiumos/platform2/+/master/u2fd/u2fhid.cc 33 | var DefaultReport = u2fHIDReport{ 34 | 0x06, 0xD0, 0xF1, /* Usage Page (FIDO Alliance), FIDO_USAGE_PAGE */ 35 | 0x09, 0x01, /* Usage (U2F HID Auth. Device) FIDO_USAGE_U2FHID */ 36 | 0xA1, 0x01, /* Collection (Application), HID_APPLICATION */ 37 | 0x09, 0x20, /* Usage (Input Report Data), FIDO_USAGE_DATA_IN */ 38 | 0x15, 0x00, /* Logical Minimum (0) */ 39 | 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ 40 | 0x75, 0x08, /* Report Size (8) */ 41 | 0x95, 0x40, /* Report Count (64), HID_INPUT_REPORT_BYTES */ 42 | 0x81, 0x02, /* Input (Data, Var, Abs), Usage */ 43 | 0x09, 0x21, /* Usage (Output Report Data), FIDO_USAGE_DATA_OUT */ 44 | 0x15, 0x00, /* Logical Minimum (0) */ 45 | 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ 46 | 0x75, 0x08, /* Report Size (8) */ 47 | 0x95, 0x40, /* Report Count (64), HID_OUTPUT_REPORT_BYTES */ 48 | 0x91, 0x02, /* Output (Data, Var, Abs), Usage */ 49 | 0xC0, /* End Collection */ 50 | } 51 | 52 | //go:generate stringer -type=u2fHIDCommand 53 | type u2fHIDCommand int 54 | 55 | func (c u2fHIDCommand) isVendorCommand() bool { 56 | u := uint8(c) 57 | return u >= VendorCommandFirst || u <= VendorCommandLast 58 | } 59 | 60 | const ( 61 | broadcastChan = 0xffffffff 62 | 63 | // mandatory commands 64 | cmdPing u2fHIDCommand = 0x80 | 0x01 65 | cmdMsg u2fHIDCommand = 0x80 | 0x03 66 | cmdInit u2fHIDCommand = 0x80 | 0x06 67 | cmdError u2fHIDCommand = 0x80 | 0x3f 68 | 69 | // optional commands 70 | cmdLock u2fHIDCommand = 0x80 | 0x04 71 | cmdWink u2fHIDCommand = 0x80 | 0x08 72 | cmdSync u2fHIDCommand = 0x80 | 0x3c 73 | 74 | // VendorCommandFirst is the first admissible vendor command identifier. 75 | VendorCommandFirst = 0x80 | 0x40 76 | 77 | // VendorCommandLast is the last admissible vendor command identifier. 78 | VendorCommandLast = 0x80 | 0x7f 79 | ) 80 | 81 | type CommandHandler func([]byte) []byte 82 | 83 | // Handler holds methods for sending and receiving packets. 84 | type Handler struct { 85 | // token instance 86 | token Token 87 | 88 | // u2fhid state 89 | state *u2fHIDState 90 | stateLock sync.Mutex 91 | 92 | // mapping between u2fHIDCommands and Token instances 93 | commandMappings map[u2fHIDCommand]CommandHandler 94 | } 95 | 96 | // NewHandler returns a new Handler instance with a given u2ftoken.Token. 97 | // Token cannot be nil. 98 | func NewHandler(token Token) (*Handler, error) { 99 | if token == nil { 100 | return nil, errors.New("token is nil") 101 | } 102 | 103 | return &Handler{ 104 | token: token, 105 | commandMappings: make(map[u2fHIDCommand]CommandHandler), 106 | state: &u2fHIDState{ 107 | sessions: map[uint32]*session{}, 108 | }, 109 | }, nil 110 | } 111 | 112 | // AddMapping adds a new CommandHandler mapping for a given command. 113 | // Returns error if there's already a mapping for command, or if it is not defined 114 | // between VendorCommandFirst and VendorCommandLast. 115 | // Each mapping will be handled like a cmdMsg, meaning that the input for ch will be the whole session 116 | // data, while its output will be framed and sent over the wire. 117 | func (h *Handler) AddMapping(command u2fHIDCommand, ch CommandHandler) error { 118 | if _, mappingExists := h.commandMappings[command]; mappingExists { 119 | return errors.New("command mapping already exists") 120 | } 121 | 122 | if !command.isVendorCommand() { 123 | return errors.New("command must be between U2FHID_VENDOR_FIRST and U2FHID_VENDOR_LAST") 124 | } 125 | 126 | h.commandMappings[command] = ch 127 | 128 | return nil 129 | } 130 | 131 | // u2fPacket is implemented by U2F HID packets, and exposes methods that must be implemented 132 | // to retrieve channel id, command, length, packet count and so on. 133 | type u2fPacket interface { 134 | Channel() uint32 135 | ChannelBytes() [4]byte 136 | Command() uint8 137 | Length() uint16 138 | Count() uint16 139 | } 140 | 141 | // session holds informations about a single operation currently happening (MSG, PING...). 142 | type session struct { 143 | data []byte 144 | command u2fHIDCommand 145 | total uint64 146 | leftToRead uint64 147 | lastSequence uint8 148 | packetZeroSeen bool 149 | } 150 | 151 | // clear clears a session, setting everything to their default values. 152 | func (s *session) clear() { 153 | s.data = nil 154 | s.command = 0 155 | s.total = 0 156 | s.leftToRead = 0 157 | s.lastSequence = 0 158 | s.packetZeroSeen = false 159 | } 160 | 161 | // u2fHIDState holds the global state of the U2FHID token, keeping track of whether it is still accumulating messages, 162 | // all the outbound messages, all the sessions. 163 | type u2fHIDState struct { 164 | outboundMsgs [][]byte 165 | lastOutboundIndex int 166 | accumulatingMsgs bool 167 | sessions map[uint32]*session 168 | lastChannelID uint32 169 | } 170 | 171 | // clear deletes the last channel id session, and sets outbound messages and its index, and channel id to zero. 172 | func (u *u2fHIDState) clear() { 173 | sess, ok := u.sessions[u.lastChannelID] 174 | if ok { 175 | sess.clear() 176 | } 177 | 178 | u.outboundMsgs = nil 179 | u.lastOutboundIndex = 0 180 | u.lastChannelID = 0 181 | u.accumulatingMsgs = false 182 | } 183 | -------------------------------------------------------------------------------- /u2fhid/types_test.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_u2fHIDReport_Bytes(t *testing.T) { 10 | t.Run("hid report bytes are serialized", func(t *testing.T) { 11 | require.NotNil(t, DefaultReport.Bytes()) 12 | }) 13 | } 14 | 15 | func TestNewHandler(t *testing.T) { 16 | 17 | tests := []struct { 18 | name string 19 | token Token 20 | errAssertion require.ErrorAssertionFunc 21 | dataAssertion require.ValueAssertionFunc 22 | }{ 23 | { 24 | "token is not nil", 25 | &fakeToken{}, 26 | require.NoError, 27 | require.NotNil, 28 | }, 29 | { 30 | "token is nil", 31 | nil, 32 | require.Error, 33 | require.Nil, 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | got, err := NewHandler(tt.token) 39 | tt.errAssertion(t, err) 40 | tt.dataAssertion(t, got) 41 | }) 42 | } 43 | } 44 | 45 | func Test_session_clear(t *testing.T) { 46 | t.Run("state values are set to their default values", func(t *testing.T) { 47 | s := &session{ 48 | data: []byte("some data"), 49 | command: cmdMsg, 50 | total: 42, 51 | leftToRead: 0, 52 | lastSequence: 42, 53 | } 54 | 55 | s.clear() 56 | 57 | require.Equal(t, session{}, *s) 58 | }) 59 | } 60 | 61 | func Test_u2fHIDState_clear(t *testing.T) { 62 | t.Run("state values are set to their default values", func(t *testing.T) { 63 | u := &u2fHIDState{ 64 | outboundMsgs: [][]byte{ 65 | []byte("some data"), 66 | }, 67 | lastOutboundIndex: 42, 68 | accumulatingMsgs: true, 69 | sessions: map[uint32]*session{ 70 | 42: { 71 | data: []byte("some data"), 72 | command: cmdMsg, 73 | total: 42, 74 | leftToRead: 0, 75 | lastSequence: 42, 76 | }, 77 | }, 78 | lastChannelID: 42, 79 | } 80 | 81 | u.clear() 82 | 83 | require.Nil(t, u.outboundMsgs) 84 | require.Zero(t, u.lastOutboundIndex) 85 | require.False(t, u.accumulatingMsgs) 86 | require.NotNil(t, u.sessions[42]) 87 | require.Len(t, u.sessions, 1) 88 | require.Zero(t, u.lastChannelID) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /u2fhid/u2ferror_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=u2fError"; DO NOT EDIT. 2 | 3 | package u2fhid 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[none-0] 12 | _ = x[invalidCmd-1] 13 | _ = x[invalidPar-2] 14 | _ = x[invalidLen-3] 15 | _ = x[invalidSeq-4] 16 | _ = x[msgTimeout-5] 17 | _ = x[channelBusy-6] 18 | _ = x[lockRequired-10] 19 | _ = x[invalidCid-11] 20 | _ = x[other-127] 21 | } 22 | 23 | const ( 24 | _u2fError_name_0 = "noneinvalidCmdinvalidParinvalidLeninvalidSeqmsgTimeoutchannelBusy" 25 | _u2fError_name_1 = "lockRequiredinvalidCid" 26 | _u2fError_name_2 = "other" 27 | ) 28 | 29 | var ( 30 | _u2fError_index_0 = [...]uint8{0, 4, 14, 24, 34, 44, 54, 65} 31 | _u2fError_index_1 = [...]uint8{0, 12, 22} 32 | ) 33 | 34 | func (i u2fError) String() string { 35 | switch { 36 | case i <= 6: 37 | return _u2fError_name_0[_u2fError_index_0[i]:_u2fError_index_0[i+1]] 38 | case 10 <= i && i <= 11: 39 | i -= 10 40 | return _u2fError_name_1[_u2fError_index_1[i]:_u2fError_index_1[i+1]] 41 | case i == 127: 42 | return _u2fError_name_2 43 | default: 44 | return "u2fError(" + strconv.FormatInt(int64(i), 10) + ")" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /u2fhid/u2fhidcommand_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=u2fHIDCommand"; DO NOT EDIT. 2 | 3 | package u2fhid 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[cmdPing-129] 12 | _ = x[cmdMsg-131] 13 | _ = x[cmdInit-134] 14 | _ = x[cmdError-191] 15 | _ = x[cmdLock-132] 16 | _ = x[cmdWink-136] 17 | _ = x[cmdSync-188] 18 | } 19 | 20 | const ( 21 | _u2fHIDCommand_name_0 = "cmdPing" 22 | _u2fHIDCommand_name_1 = "cmdMsgcmdLock" 23 | _u2fHIDCommand_name_2 = "cmdInit" 24 | _u2fHIDCommand_name_3 = "cmdWink" 25 | _u2fHIDCommand_name_4 = "cmdSync" 26 | _u2fHIDCommand_name_5 = "cmdError" 27 | ) 28 | 29 | var ( 30 | _u2fHIDCommand_index_1 = [...]uint8{0, 6, 13} 31 | ) 32 | 33 | func (i u2fHIDCommand) String() string { 34 | switch { 35 | case i == 129: 36 | return _u2fHIDCommand_name_0 37 | case 131 <= i && i <= 132: 38 | i -= 131 39 | return _u2fHIDCommand_name_1[_u2fHIDCommand_index_1[i]:_u2fHIDCommand_index_1[i+1]] 40 | case i == 134: 41 | return _u2fHIDCommand_name_2 42 | case i == 136: 43 | return _u2fHIDCommand_name_3 44 | case i == 188: 45 | return _u2fHIDCommand_name_4 46 | case i == 191: 47 | return _u2fHIDCommand_name_5 48 | default: 49 | return "u2fHIDCommand(" + strconv.FormatInt(int64(i), 10) + ")" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /u2fhid/utils_test.go: -------------------------------------------------------------------------------- 1 | package u2fhid 2 | 3 | import "testing" 4 | 5 | type test struct { 6 | name string 7 | f func(*testing.T) 8 | } 9 | 10 | type fakeToken struct { 11 | shouldReturnData bool 12 | data []byte 13 | } 14 | 15 | func (f *fakeToken) HandleMessage(b []byte) []byte { 16 | if f.shouldReturnData { 17 | return f.data 18 | } 19 | 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /u2ftoken/cmd_authenticate.go: -------------------------------------------------------------------------------- 1 | package u2ftoken 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | 8 | "github.com/gsora/fidati/internal/flog" 9 | ) 10 | 11 | const ( 12 | // we expect no less than minimumLen bytes when parsing an Authenticate request. 13 | minimumLen = 32 + 32 + 1 // control byte + challenge param + app param + key handle len 14 | 15 | controlCheckOnly = 0x07 16 | controlEnforceUserPresenceAndSign = 0x03 17 | controlDontEnforceUserPresenceAndSign = 0x08 18 | ) 19 | 20 | func (t *Token) handleAuthenticate(req Request) (Response, error) { 21 | if len(req.Data) < minimumLen { 22 | return Response{}, errWrongLength 23 | } 24 | 25 | controlByte := req.Parameters.First 26 | 27 | flog.Logger.Println("control byte ", controlByte) 28 | 29 | flog.Logger.Println("data", hex.EncodeToString(req.Data)) 30 | challengeParam := req.Data[0:32] 31 | appID := req.Data[32:64] 32 | khLen := req.Data[64] 33 | 34 | flog.Logger.Printf("challenge len %d, app param len %d, khlen %d, total len data %d\n", len(challengeParam), len(appID), khLen, len(req.Data)) 35 | 36 | if len(req.Data) != int(minimumLen+khLen) { 37 | flog.Logger.Printf("len request data %d different from minimumLen+khLen %d", len(req.Data), int(minimumLen+khLen)) 38 | // total data len must be equal to minimumLen + khLen (headers + length of the key handle) 39 | return Response{}, errWrongLength 40 | } 41 | 42 | keyHandle := req.Data[minimumLen : minimumLen+khLen] 43 | 44 | flog.Logger.Println("requesting appID:", hex.EncodeToString(appID)) 45 | flog.Logger.Println("requesting keyHandle:", hex.EncodeToString(keyHandle)) 46 | 47 | // check that appID derives the same keyHandle we received 48 | nonce := t.keyring.NonceFromKeyHandle(keyHandle) 49 | if nonce == nil { 50 | flog.Logger.Println("cannot obtain nonce from provided key handle") 51 | return Response{}, errWrongData 52 | } 53 | 54 | _, derivedKeyHandle, err := t.keyring.Register(appID, nonce) 55 | if err != nil { 56 | flog.Logger.Println("cannot register key:", err) 57 | return Response{}, errWrongData 58 | } 59 | 60 | flog.Logger.Println("generated keyhandle from appID:", hex.EncodeToString(derivedKeyHandle)) 61 | 62 | if !bytes.Equal(derivedKeyHandle, keyHandle) { 63 | flog.Logger.Println("derived key handle and provided key handle don't match") 64 | return Response{}, errWrongData 65 | } 66 | 67 | userPresence := t.keyring.Counter.UserPresence() 68 | 69 | // we only handle those two cases because the last one basically means 70 | // "authenticate, thanks" 71 | switch controlByte { 72 | case controlCheckOnly: 73 | return Response{}, errConditionNotSatisfied 74 | case controlEnforceUserPresenceAndSign: 75 | if !userPresence { 76 | flog.Logger.Println("control byte asked to enforce user presence, but it wasn't present") 77 | return Response{}, errConditionNotSatisfied 78 | } 79 | } 80 | 81 | userPresenceByte := byte(0) 82 | if userPresence { 83 | userPresenceByte = 1 84 | } 85 | 86 | sign, ni, err := t.keyring.Authenticate(appID, challengeParam, keyHandle, userPresence) 87 | if err != nil { 88 | return Response{}, errWrongData 89 | } 90 | 91 | resp := new(bytes.Buffer) 92 | resp.WriteByte(userPresenceByte) 93 | 94 | counterBytes := [4]byte{} 95 | binary.BigEndian.PutUint32(counterBytes[:], ni) 96 | 97 | resp.Write(counterBytes[:]) 98 | resp.Write(sign) 99 | 100 | return Response{ 101 | Data: resp.Bytes(), 102 | StatusCode: noError.Bytes(), 103 | }, nil 104 | } 105 | -------------------------------------------------------------------------------- /u2ftoken/cmd_register.go: -------------------------------------------------------------------------------- 1 | package u2ftoken 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "encoding/hex" 10 | 11 | "github.com/gsora/fidati/internal/flog" 12 | ) 13 | 14 | const ( 15 | // we expect no more than 64 bytes in the Data section of our Request. 16 | expectedDataLen = 64 17 | ) 18 | 19 | func (t *Token) handleRegister(req Request) (Response, error) { 20 | if len(req.Data) != expectedDataLen { 21 | flog.Logger.Printf("message length is %d instead of %d\n", len(req.Data), expectedDataLen) 22 | return Response{}, errWrongLength 23 | } 24 | 25 | if !t.keyring.Counter.UserPresence() { 26 | flog.Logger.Println("user presence during registration is required") 27 | return Response{}, errConditionNotSatisfied 28 | } 29 | 30 | challengeParam := req.Data[:32] 31 | appID := req.Data[32:] 32 | 33 | newKey, keyHandle, err := t.keyring.Register(appID, nil) 34 | if err != nil { 35 | return Response{}, err 36 | } 37 | 38 | pubkey := elliptic.Marshal(elliptic.P256(), newKey.X, newKey.Y) 39 | 40 | resp := new(bytes.Buffer) 41 | 42 | resp.WriteByte(0x05) 43 | resp.Write(pubkey) 44 | 45 | resp.WriteByte(byte(len(keyHandle))) 46 | 47 | resp.Write(keyHandle) 48 | resp.Write(t.attestationCertificate) 49 | 50 | sigPayload := buildSigPayload( 51 | appID, 52 | challengeParam, 53 | keyHandle, 54 | pubkey, 55 | ) 56 | 57 | flog.Logger.Println("data", hex.EncodeToString(req.Data)) 58 | flog.Logger.Println("registered appID:", hex.EncodeToString(appID)) 59 | flog.Logger.Println("registered keyhandle:", hex.EncodeToString(keyHandle)) 60 | 61 | sph := sha256.Sum256(sigPayload) 62 | spHash := sph[:] 63 | 64 | sign, err := ecdsa.SignASN1(rand.Reader, t.attestationPrivkey, spHash) 65 | if err != nil { 66 | return Response{}, err 67 | } 68 | 69 | flog.Logger.Println("sign len:", len(sign)) 70 | resp.Write(sign) 71 | 72 | rb := resp.Bytes() 73 | flog.Logger.Println("response bytes:", hex.EncodeToString(rb)) 74 | 75 | return Response{ 76 | Data: rb, 77 | StatusCode: noError.Bytes(), 78 | }, nil 79 | } 80 | 81 | func buildSigPayload(appParam []byte, challenge []byte, key []byte, pubKey []byte) []byte { 82 | p := new(bytes.Buffer) 83 | p.WriteByte(0x00) 84 | p.Write(appParam) 85 | p.Write(challenge) 86 | p.Write(key) 87 | p.Write(pubKey) 88 | 89 | return p.Bytes() 90 | } 91 | -------------------------------------------------------------------------------- /u2ftoken/cmd_version.go: -------------------------------------------------------------------------------- 1 | package u2ftoken 2 | 3 | // this is the standard response a U2F relying party expects when it sends a 4 | // Version command. 5 | const versionPayload = "U2F_V2" 6 | 7 | var versionString = []byte(versionPayload) 8 | 9 | func (*Token) handleVersion(req Request) (Response, error) { 10 | return Response{ 11 | Data: versionString, 12 | StatusCode: noError.Bytes(), 13 | }, nil 14 | } 15 | -------------------------------------------------------------------------------- /u2ftoken/command_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=command"; DO NOT EDIT. 2 | 3 | package u2ftoken 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Register-1] 12 | _ = x[Authenticate-2] 13 | _ = x[Version-3] 14 | } 15 | 16 | const _command_name = "RegisterAuthenticateVersion" 17 | 18 | var _command_index = [...]uint8{0, 8, 20, 27} 19 | 20 | func (i command) String() string { 21 | i -= 1 22 | if i >= command(len(_command_index)-1) { 23 | return "command(" + strconv.FormatInt(int64(i+1), 10) + ")" 24 | } 25 | return _command_name[_command_index[i]:_command_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /u2ftoken/errorcode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=errorCode"; DO NOT EDIT. 2 | 3 | package u2ftoken 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[noError-36864] 12 | _ = x[errConditionNotSatisfied-27013] 13 | _ = x[errWrongData-27264] 14 | _ = x[errWrongLength-26368] 15 | _ = x[errClaNotSupported-28160] 16 | _ = x[errInsNotSupported-27904] 17 | } 18 | 19 | const ( 20 | _errorCode_name_0 = "errWrongLength" 21 | _errorCode_name_1 = "errConditionNotSatisfied" 22 | _errorCode_name_2 = "errWrongData" 23 | _errorCode_name_3 = "errInsNotSupported" 24 | _errorCode_name_4 = "errClaNotSupported" 25 | _errorCode_name_5 = "noError" 26 | ) 27 | 28 | func (i errorCode) String() string { 29 | switch { 30 | case i == 26368: 31 | return _errorCode_name_0 32 | case i == 27013: 33 | return _errorCode_name_1 34 | case i == 27264: 35 | return _errorCode_name_2 36 | case i == 27904: 37 | return _errorCode_name_3 38 | case i == 28160: 39 | return _errorCode_name_4 40 | case i == 36864: 41 | return _errorCode_name_5 42 | default: 43 | return "errorCode(" + strconv.FormatInt(int64(i), 10) + ")" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /u2ftoken/handle_message.go: -------------------------------------------------------------------------------- 1 | package u2ftoken 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gsora/fidati/internal/flog" 7 | ) 8 | 9 | // a ready-made instance of the errConditionNotSatisfied error. 10 | var notSatisfied = errorResponse(errConditionNotSatisfied).Bytes() 11 | 12 | // HandleMessage handles a message, and returns a response byte slice. 13 | func (t *Token) HandleMessage(data []byte) []byte { 14 | req, err := t.ParseRequest(data) 15 | if err != nil { 16 | flog.Logger.Printf("cannot parse request, %s", err) 17 | return notSatisfied 18 | } 19 | 20 | var resp Response 21 | var handleErr error 22 | 23 | flog.Logger.Printf("request: %+v", req) 24 | 25 | switch req.Command { 26 | case Version: 27 | resp, handleErr = t.handleVersion(req) 28 | case Register: 29 | resp, handleErr = t.handleRegister(req) 30 | case Authenticate: 31 | resp, handleErr = t.handleAuthenticate(req) 32 | default: 33 | return notSatisfied 34 | } 35 | 36 | if handleErr != nil { 37 | var err errorCode 38 | 39 | if !errors.As(handleErr, &err) { 40 | // this is a strange error, flog.Logger.it and return ErrConditionNotSatisfied 41 | flog.Logger.Println("non-u2f error detected:", handleErr) 42 | return notSatisfied 43 | } 44 | 45 | return errorResponse(err).Bytes() 46 | } 47 | 48 | flog.Logger.Println("response len: ", len(resp.Bytes())) 49 | 50 | respBytes, err := buildResponse(req, resp) 51 | if err != nil { 52 | flog.Logger.Println("cannot build response:", err) 53 | return notSatisfied 54 | } 55 | 56 | return respBytes 57 | } 58 | -------------------------------------------------------------------------------- /u2ftoken/types.go: -------------------------------------------------------------------------------- 1 | package u2ftoken 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "encoding/binary" 7 | "fmt" 8 | 9 | "github.com/gsora/fidati/attestation" 10 | "github.com/gsora/fidati/keyring" 11 | ) 12 | 13 | // command represents a U2F standard command. 14 | // See https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf for more 15 | // details. 16 | //go:generate stringer -type=command 17 | type command uint8 18 | 19 | const ( 20 | _ command = iota 21 | // Register registers a new relying party. 22 | Register 23 | 24 | // Authenticate authenticates a relying party with the associated identity, stored in the token. 25 | Authenticate 26 | 27 | // Version returns the standard "U2F_V2" version string. 28 | Version 29 | ) 30 | 31 | const ( 32 | // The command completed successfully without error. 33 | noError errorCode = 0x9000 34 | 35 | // The request was rejected due to test-of-user-presence being required. 36 | errConditionNotSatisfied errorCode = 0x6985 37 | 38 | // The request was rejected due to an invalid key handle. 39 | errWrongData errorCode = 0x6A80 40 | 41 | // The length of the request was invalid. 42 | errWrongLength errorCode = 0x6700 43 | 44 | // The Class byte of the request is not supported. 45 | errClaNotSupported errorCode = 0x6E00 46 | 47 | // The Instruction of the request is not supported. 48 | errInsNotSupported errorCode = 0x6D00 49 | ) 50 | 51 | // errorCode represents a U2F standard error code. 52 | //go:generate stringer -type=errorCode 53 | type errorCode uint16 54 | 55 | // Error implements the error interface. 56 | func (ec errorCode) Error() string { 57 | return ec.String() 58 | } 59 | 60 | // Bytes returns the byte array representation of c. 61 | func (ec errorCode) Bytes() [2]byte { 62 | var ret [2]byte 63 | binary.BigEndian.PutUint16(ret[:], uint16(ec)) 64 | return ret 65 | } 66 | 67 | // errorResponse returns a Response struct which wraps errCode. 68 | func errorResponse(errCode errorCode) Response { 69 | return Response{ 70 | Data: []byte{}, 71 | StatusCode: errCode.Bytes(), 72 | } 73 | } 74 | 75 | // Params holds the two APDU standard request parameters. 76 | type Params struct { 77 | First uint8 78 | Second uint8 79 | } 80 | 81 | // Request represents a standard APDU request. 82 | type Request struct { 83 | Command command 84 | Parameters Params 85 | MaxResponseBytes uint16 86 | Data []byte 87 | } 88 | 89 | // Response represents a standard APDU response. 90 | type Response struct { 91 | Data []byte 92 | StatusCode [2]byte 93 | } 94 | 95 | // Bytes returns the byte slice representation of r. 96 | func (r Response) Bytes() []byte { 97 | buf := new(bytes.Buffer) 98 | binary.Write(buf, binary.BigEndian, r.Data) 99 | buf.Write(r.StatusCode[:]) 100 | return buf.Bytes() 101 | } 102 | 103 | // Token represents a U2F token. 104 | // It handles request parsing and composition, key storage orchestration. 105 | type Token struct { 106 | keyring *keyring.Keyring 107 | attestationCertificate []byte 108 | attestationPrivkey *ecdsa.PrivateKey 109 | } 110 | 111 | // New returns a new Token instance with k as Keyring. 112 | // attCert must be a PEM-encoded certificate, while attPrivKey must be a X.509-encoded 113 | // ECDSA private key. 114 | func New(k *keyring.Keyring, attCert, attPrivKey []byte) (*Token, error) { 115 | cert, _, err := attestation.ParseCertificate(attCert) 116 | if err != nil { 117 | return nil, fmt.Errorf("cannot parse attestation certificate, %w", err) 118 | } 119 | 120 | key, err := attestation.ParseKey(attPrivKey) 121 | if err != nil { 122 | return nil, fmt.Errorf("cannot parse attestation certificate, %w", err) 123 | } 124 | 125 | return &Token{ 126 | keyring: k, 127 | attestationCertificate: cert, 128 | attestationPrivkey: key, 129 | }, nil 130 | } 131 | 132 | // ParseRequest parses req as a U2F request. 133 | // It returns a Request instance filled with the appropriate data from req, and an error. 134 | func (t *Token) ParseRequest(req []byte) (Request, error) { 135 | var ret Request 136 | 137 | if req == nil { 138 | return Request{}, fmt.Errorf("request bytes are nil") 139 | } 140 | 141 | if req[0] != 0 { 142 | return Request{}, fmt.Errorf("first byte of request must be zero") 143 | } 144 | 145 | ret.Command = command(req[1]) 146 | ret.Parameters = Params{ 147 | First: req[2], 148 | Second: req[3], 149 | } 150 | 151 | if req[4] != 0 { 152 | return Request{}, fmt.Errorf("fifth byte is not zero, must always be") 153 | } 154 | 155 | dataLen := binary.BigEndian.Uint16(req[5:7]) 156 | 157 | if dataLen != 0 { 158 | ret.Data = req[7 : dataLen+7] // first 6 bytes are header tags, minus one for array indexing reasons :-) 159 | } 160 | 161 | // Ne initial offset = 6 (header bytes) + dataLen 162 | // Ne end offset = len(req) 163 | 164 | neBytes := req[(5 + dataLen):] 165 | 166 | if len(neBytes) == 3 { 167 | return Request{}, fmt.Errorf("Ne bytes are %d long while we were expecting 3 bytes", len(neBytes)) 168 | } 169 | 170 | ret.MaxResponseBytes = binary.BigEndian.Uint16(neBytes) 171 | 172 | return ret, nil 173 | } 174 | 175 | // buildResponse returns a byte slice containing APDU bytes to appropriately respond to the associated Request. 176 | func buildResponse(_ Request, resp Response) ([]byte, error) { 177 | return resp.Bytes(), nil 178 | 179 | } 180 | -------------------------------------------------------------------------------- /usb.go: -------------------------------------------------------------------------------- 1 | package fidati 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/usbarmory/tamago/soc/nxp/usb" 10 | "github.com/gsora/fidati/internal/flog" 11 | "github.com/gsora/fidati/u2fhid" 12 | ) 13 | 14 | const ( 15 | hidRequestSetIdle = 10 16 | hidRequestTypeGetDescriptor = 0x21 17 | descriptorTypeGetReport = 0x22 18 | ) 19 | 20 | // hidDescriptor represents a HID standard descriptor. 21 | // Device Class Definition for Human Interface Devices (HID) Version 1.11, pg 22. 22 | type hidDescriptor struct { 23 | Length uint8 24 | Type uint8 25 | bcdHID uint16 26 | CountryCode uint8 27 | NumDescriptors uint8 28 | ReportDescriptorType uint8 29 | DescriptorLength uint16 30 | } 31 | 32 | // setDefaults sets some standard properties for hidDescriptor. 33 | func (d *hidDescriptor) setDefaults() { 34 | d.Length = 0x09 35 | d.Type = 0x21 36 | d.bcdHID = 0x101 37 | } 38 | 39 | // bytes converts the descriptor structure to byte array format. 40 | func (d *hidDescriptor) bytes() []byte { 41 | buf := new(bytes.Buffer) 42 | binary.Write(buf, binary.LittleEndian, d) 43 | return buf.Bytes() 44 | } 45 | 46 | // configureDevice configures device to use hidSetup Setup function, and adds an HID InterfaceDescriptor to conf 47 | // along with the needed Endpoints. 48 | func configureDevice(device *usb.Device, conf *usb.ConfigurationDescriptor, u2fHandler *u2fhid.Handler) error { 49 | device.Setup = hidSetup(device) 50 | 51 | id, err := addInterface(device, conf) 52 | if err != nil { 53 | return fmt.Errorf("cannot add U2F USB Interface, %w", err) 54 | } 55 | 56 | endpoints := addEndpoints(id) 57 | endpoints.in.Function = u2fHandler.Tx 58 | endpoints.out.Function = u2fHandler.Rx 59 | 60 | addHIDClassDescriptor(id) 61 | 62 | // device qualifier 63 | device.Qualifier = &usb.DeviceQualifierDescriptor{} 64 | device.Qualifier.SetDefaults() 65 | device.Qualifier.NumConfigurations = uint8(len(device.Configurations)) 66 | 67 | return nil 68 | } 69 | 70 | // addInterface adds a Interface Descriptor with 2 endpoints, with HID interface class. 71 | func addInterface(device *usb.Device, conf *usb.ConfigurationDescriptor) (*usb.InterfaceDescriptor, error) { 72 | id := &usb.InterfaceDescriptor{} 73 | id.SetDefaults() 74 | 75 | id.NumEndpoints = 2 76 | id.InterfaceClass = 0x03 77 | id.InterfaceSubClass = 0x0 78 | id.InterfaceProtocol = 0x0 79 | 80 | var err error 81 | id.Interface, err = device.AddString("fidati interface descriptor") 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | conf.AddInterface(id) 87 | return id, nil 88 | } 89 | 90 | // endpoints is a convenience struct, holds input and output endpoints. 91 | type endpoints struct { 92 | in *usb.EndpointDescriptor 93 | out *usb.EndpointDescriptor 94 | } 95 | 96 | // addEndpoints adds an input and output endpoint to conf, returns a endpoints instance to let 97 | // the caller determine their behavior. 98 | func addEndpoints(conf *usb.InterfaceDescriptor) endpoints { 99 | var e endpoints 100 | 101 | e.in = &usb.EndpointDescriptor{} 102 | e.in.SetDefaults() 103 | e.in.Attributes = 0x03 104 | e.in.EndpointAddress = 0x81 105 | e.in.MaxPacketSize = 512 106 | e.in.Interval = 1 107 | 108 | e.out = &usb.EndpointDescriptor{} 109 | e.out.SetDefaults() 110 | e.out.Attributes = 0x03 111 | e.out.EndpointAddress = 0x01 112 | e.out.MaxPacketSize = 512 113 | e.out.Interval = 1 114 | 115 | conf.Endpoints = append(conf.Endpoints, e.out, e.in) 116 | 117 | return e 118 | } 119 | 120 | // addHIDClassDescriptor adds a HID class descriptor to conf. 121 | // The report descriptor length is len(u2fhid.DefaultReport). 122 | func addHIDClassDescriptor(conf *usb.InterfaceDescriptor) { 123 | hid := hidDescriptor{} 124 | hid.setDefaults() 125 | hid.CountryCode = 0x0 126 | hid.NumDescriptors = 0x01 127 | hid.ReportDescriptorType = 0x22 128 | 129 | hid.DescriptorLength = uint16(len(u2fhid.DefaultReport)) 130 | 131 | conf.ClassDescriptors = append(conf.ClassDescriptors, hid.bytes()) 132 | } 133 | 134 | // hidSetup returns a custom setup function for device. 135 | func hidSetup(device *usb.Device) usb.SetupFunction { 136 | return func(setup *usb.SetupData) (in []byte, ack, done bool, err error) { 137 | bDescriptorType := setup.Value & 0xff 138 | 139 | flog.Logger.Println("descriptor type:", bDescriptorType, setup) 140 | 141 | if setup.Request == usb.SET_FEATURE { 142 | // stall here 143 | err = errors.New("should stall") 144 | done = true 145 | return 146 | } 147 | 148 | if int(setup.RequestType) & ^0x80 == hidRequestTypeGetDescriptor { 149 | if setup.Request == hidRequestSetIdle { 150 | ack = true 151 | done = true 152 | return 153 | } 154 | } 155 | 156 | if setup.Request == usb.GET_DESCRIPTOR { 157 | if bDescriptorType == descriptorTypeGetReport { 158 | in = u2fhid.DefaultReport.Bytes() 159 | done = true 160 | return 161 | } 162 | } 163 | 164 | return 165 | } 166 | } 167 | 168 | // DefaultConfiguration returns a usb.ConfigurationDescriptor ready to be used for ConfigureUSB. 169 | func DefaultConfiguration() usb.ConfigurationDescriptor { 170 | cd := usb.ConfigurationDescriptor{} 171 | cd.SetDefaults() 172 | cd.Attributes = 160 173 | 174 | return cd 175 | } 176 | 177 | // ConfigureUSB configures device and config to be used as a FIDO2 U2F token. 178 | func ConfigureUSB(config *usb.ConfigurationDescriptor, device *usb.Device, u2fHandler *u2fhid.Handler) error { 179 | return configureDevice(device, config, u2fHandler) 180 | } 181 | --------------------------------------------------------------------------------