diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9a851fe..7604146 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,12 +26,12 @@ jobs: run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: check for SPDX tags - run: ./tools/spdx-ensure - - name: make run: make + - name: tests + run: go test ./... + reuse-compliance-check: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 96fe276..5cfad5c 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -7,7 +7,7 @@ on: permissions: contents: read - # Optional: allow read access to pull request. Use with `only-new-issues` option. + # Optional: allow read access to pull requests. Use with `only-new-issues` option. # pull-requests: read jobs: @@ -15,40 +15,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: - go-version: '1.21' - cache: false + go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v8 with: - # Require: The version of golangci-lint to use. - # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. - # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. - version: v1.55.2 - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - # - # Note: By default, the `.golangci.yml` file should be at the root of the repository. - # The location of the configuration file can be changed by using `--config=` - # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true, then all caching functionality will be completely disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true, then the action won't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true, then the action won't cache or restore ~/.cache/go-build. - # skip-build-cache: true - - # Optional: The mode to install golangci-lint. It can be 'binary' or 'goinstall'. - # install-mode: "goinstall" + version: v2.4 diff --git a/.gitignore b/.gitignore index 0a5f004..2b505f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /dist /show-pubkey /tkey-verification +/tkey-sigsum-submit +/tkey-verify /certs diff --git a/.golangci.yml b/.golangci.yml index fd71358..f25ce91 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,37 +1,92 @@ +version: "2" linters: - presets: - # found in: golangci-lint help linters - - bugs - - comment - - complexity - - error - - format - - import - - metalinter - - module - - performance - - sql - # - style # turned off, can be too much - - test - - unused + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - contextcheck + - dupword + - durationcheck + - errchkjson + - errorlint + - exhaustive + - fatcontext + - gocheckcompilerdirectives + - gochecksumtype + - gocritic + - gocyclo + - godox + - gomoddirectives + - gomodguard + - gosec + - gosmopolitan + - loggercheck + - maintidx + - makezero + - misspell + - musttag + - nilerr + - nilnesserr + - perfsprint + - prealloc + - protogetter + - reassign + - recvcheck + - revive + - rowserrcheck + - spancheck + - sqlclosecheck + - testableexamples + - testifylint + - thelper + - tparallel + - usetesting + - wrapcheck + - zerologlint disable: - cyclop + - depguard + - exhaustruct - funlen - - gci - gocognit - - gofumpt - - nestif - - exhaustruct # TODO? annoying for now - godot - - depguard + - nestif - noctx - unparam - + - err113 + - paralleltest + - recvcheck + - testpackage + settings: + govet: + enable: + - shadow + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + rules: + - linters: + - revive + text: "var-naming: avoid meaningless package names" issues: max-issues-per-linter: 0 max-same-issues: 0 - -linters-settings: - govet: - enable: - - shadow +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index e8f507c..98368eb 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,20 @@ shasum = sha512sum CGO = 0 .PHONY: all -all: tkey-verification +all: tkey-sigsum-submit tkey-verification tkey-verify # APP_VERSION ?= $(shell git describe --dirty --always | sed -n "s/^v\(.*\)/\1/p") APP_VERSION ?= $(shell git describe --dirty --always | sed -n "s/^v\(.*\)/\1/p") + # .PHONY to let go-build handle deps and rebuilds +.PHONY: tkey-sigsum-submit +tkey-sigsum-submit: + CGO_ENABLED=$(CGO) go build -ldflags "-w -X main.version=$(APP_VERSION) -buildid=" -trimpath -buildvcs=false ./cmd/tkey-sigsum-submit + +.PHONY: tkey-verify +tkey-verify: + CGO_ENABLED=$(CGO) go build -ldflags "-w -X main.version=$(APP_VERSION) -buildid=" -trimpath -buildvcs=false ./cmd/tkey-verify + .PHONY: tkey-verification tkey-verification: CGO_ENABLED=$(CGO) go build -ldflags "-w -X main.version=$(APP_VERSION) -buildid=" -trimpath -buildvcs=false ./cmd/tkey-verification @@ -14,7 +23,7 @@ tkey-verification: .PHONY: podman podman: - podman run --arch=amd64 --rm --mount type=bind,source=$(CURDIR),target=/src -w /src -it ghcr.io/tillitis/tkey-builder:4 make -j + podman run --arch=amd64 --rm --mount type=bind,source=$(CURDIR),target=/src -w /src -it ghcr.io/tillitis/tkey-builder:5rc2 make -j .PHONY: check-digests check-digests: @@ -23,14 +32,22 @@ check-digests: $(shasum) -c verisigner-v0.0.3.bin.sha512 .PHONY: man -man: doc/tkey-verification.1 +man: doc/tkey-verification.1 doc/tkey-verify.1 doc/tkey-sigsum-submit.1 doc/tkey-verification.1: doc/tkey-verification.scd scdoc < $^ > $@ +doc/tkey-verify.1: doc/tkey-verify.scd + scdoc < $^ > $@ + +doc/tkey-sigsum-submit.1: doc/tkey-sigsum-submit.scd + scdoc < $^ > $@ + .PHONY: clean clean: + rm -f tkey-sigsum-submit rm -f tkey-verification + rm -f tkey-verify .PHONY: lint lint: diff --git a/README.md b/README.md index 39deaad..c5fc83b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ # Tillitis TKey Verification -`tkey-verification` is a tool used for signing a TKey identity and -verifying that the same TKey still has the same identity later. +`tkey-verification` and its associated programs are tools used for +signing a TKey identity and verifying that the same TKey still has the +same identity later. See [Design of the TKey verification +process](doc/design.md) for all the details. The verification of this identity does not prove that the TKey hasn't been tampered with, only that the identity of an app running on it is the same and, to a lesser degree, that it still runs the same firmware. -*Note well*: If your TKey hasn't been provisioned by Tillitis, for -example your IT department, you will need to run their version of the -`tkey-verification` program instead of this one. +*Note well*: If your TKey wasn't provisioned by Tillitis, and instead +by another "vendor" like your IT department, you will need to run +their version of the `tkey-verification` program instead of this one. ## Installation @@ -18,246 +20,145 @@ example your IT department, you will need to run their version of the instructions](https://www.tillitis.se/applications/tkey-device-verification/) on Tillitis' web. -You can download a release of the tool at: +There are three programs: - https://github.com/tillitis/tkey-verification/releases +- tkey-verification: Used for provisioning by the vendor. +- tkey-sigsum-submit: Used for submitting signed requests to a Sigsum + log by the vendor. +- tkey-verify: Used to verify a Tillitis TKey. -Or do: +You can download releases of the tools at: + +https://github.com/tillitis/tkey-verification/releases + +Or use `go install`. If you're and end user you probably only want +`tkey-verify`, like this: ``` -$ go install github.com/tillitis/tkey-verification/cmd/tkey-verification@latest +$ go install github.com/tillitis/tkey-verification/cmd/tkey-verify@latest +``` + +If you want to build static binaries from source running `make` or +`make podman` using our tkey-builder OCI image should suffice, but +note [Releases of tkey-verification and reproducible +builds](#releases-of-tkey-verification-and-reproducible-builds) below. + +## Usage by end user + +For the typical end user with network access, insert the TKey and run: + ``` +$ tkey-verify +``` + +For more advanced use, see the man page in `doc/tkey-verify.1`. + +## Usage by vendor + +For use during provisioning, use the `tkey-verification` and +`tkey-sigsum-submit` programs. See the man pages in +`doc/tkey-verification.1` and `doc/tkey-sigsum-submit.1` for details. + +Basically, `tkey-verification` has two sub-commands, *serve-signer* +for use as a server on a HSM-like machine with a vendor TKey, and +*remote-sign* for the provisioning station. + +The server produces submission files with a signed request to log to a +Sigsum transparency log. -if you have a Go compiler. Please note that if you install like this -you won't get the tag in `--version`. +Use `tkey-sigsum-request` to submit the submission files to the log, +collect Sigsum proofs, and to produce verification files. -## Terminology +Finally, publish the verification files in the URL `tkey-verify` uses. +For Tillitis, this is: -- "device under verification": The device the vendor is provisioning - or the user is verifying. -- "device signature": A signature made on the device under - verification with the signer device app. -- Unique Device Identifier (UDI): A unique identifier present in all - TKeys. The 1st half identifies the revision of the hardware, the 2nd - half is a serial number. -- "signing server": An HSM-like machine providing signatures over - messages and producing files to be uploaded to some database. -- "signer": A device app used for confirming the TKey's identity, - right now either verisigner (source in older versions in this - repository, look for verisigner tags) or - [tkey-device-signer](https://github.com/tillitis/tkey-device-signer). -- "signer public key": The public key of signer running on the device - under verification. -- "vendor public key": The public key of the vendor, typically - Tillitis, corresponding to a private key in a TKey in the signing - server. -- "vendor signature": A signature made by the signing server. +https://tkey.tillitis.se/verify/UDI-in-hex -## What is verified? +like this: -What does verifying a TKey with `tkey-verification` prove? +https://tkey.tillitis.se/verify/0133708100000002 -To explain the verification and what it proves, first we need a brief -explanation on how the TKey works. The TKey uses measured boot. The -measured boot guarantees that every app that starts on the TKey gets a -unique identity called the Compound Device Identity (CDI). +This diagram contains an overview of how data flows during +provisioning at Tillitis: -The CDI is a combination of: +![Data flow during provisioning](signing-procedure.svg) -- a per-device unique secret embedded in the hardware, the Unique - Device Secret (UDS), - -- the integrity of the software before it is started, as measured by - immutable and thefore trusted firmware, and, +## Test setup -- an optional User Supplied Secret (not used in this case). +You need two TKeys. It might be easier to use one physical TKey and +one emulated (see [QEMU chapter in Tillitis Developer +Handbook](https://dev.tillitis.se/tools/#qemu-emulator). One is used +for the vendor's signing TKey. The other is a TKey that you want to +sign and then verify as genuine. You need to know the serial port +device paths for both of them. -The CDI is computed like this: +Here we are using `run-tkey-qemu` from +[tkey-devtools](https://github.com/tillitis/tkey-devtools) as the +vendor TKey: ``` -CDI = blake2s(UDS, blake2s(application), USS) +$ run-tkey-qemu +$ ./tkey-verification serve-signer --config tkey-verification.yaml.example-serve-signer --port ./tkey-qemu-pty ``` -where `blake2s` is the hash function BLAKE2s from [RFC -7693](https://www.rfc-editor.org/info/rfc7693). - -There are two parts to verifying a TKey. First what the vendor does -during provisioning, then what the user does during verification: - -- Provisioning: The vendor signes a message containing the Unique - Device Identifier (UDI), a digest over the observed firmware, and - the signer's public key. - - The message itself is not published, but the signature and some - metadata is (see [Verification file](#verification-file) for - details). The message can later be recreated by the verification - process. - -- Verification: `tkey-verification` first recreates the message (UDI, - firmware digest, signer's public key), checks the vendor signature - over the message, and finally does a challenge/response to prove - that the device under verification has the corresponding private - key. - -**Proven**: It's now proven that the currently used TKey device, -running this device app is the same as the TKey device (or at least a -device with the exact same UDS) that was running the same device app -during provisioning. - -Less strongly shown: - -- The firmware check assumes that a hash digest over the part of - memory where the firmware is supposed to be suffices. In a - manipulated TKey the real firmware might be somewhere else, with a - copy of the expected firmware in the right place in the memory map. -- The authenticity of the RISC-V softcore isn't proven, but it was at - least able to run the device app successfully. -- The rest of the FPGA design, except the UDS, but at least it worked - as expected with the loaded signer app. - -Not proved at all: - -- USB controller CH552 firmware and hardware. -- PCB design. - -## Why is the signed message not published? - -The message that is signed by the vendor key is not published. It is, -instead, recreated when verifying a TKey. This makes it impossible for -anyone else to verify the message, including for the vendor, if the -vendor haven't stored the message somewhere else. - -When designing this system we were afraid that publishing, or even -keeping, the signer public key in a way that ties it very strongly to -a certain UDI would be bad for the user. - -Since we're currently using the ordinary -[signer](https://github.com/tillitis/tkey-device-signer) device app -publishing the signer public key from this specific TKey would be -equivalent of publishing the user's public SSH key (if they choose not -to use an USS) to the entire world. We were not comfortable in doing -that even if it would, in a way, be a way of doing hardware -attestation of the TKey. We might need to revisit this. - -Note that we always recommend using an USS for all your own use of the -TKey! - -## Weaknesses - -- The entire device is not proven. -- The distribution of the vendor public key is sensitive. Since all - trust is placed in the vendor's signature, all fails if the end user - is tricked to use the wrong vendor public key. It's right now - embedded in tkey-verification. -- The distribution of the tkey-verification client app is sensitive, - since if it is malicious it can just say "TKey is genuine!" without - actually doing much else. - - However, the build of tkey-verification is reproducible if using - pinned versions of tools (but currently not the macOS binary). The - same verification can also be done independently by other tools. - -## Security Protocol - -### During provisioning - -1. Retrieve the UDI from the device under verification. -2. Run the signer with a specific tag on the device under - verification. This creates a unique key pair. -3. Retrieve signer public key from the device under verification. -4. Ask signer to sign a random challenge. -5. Verify the signature of the random challenge with the retrieved - public key to let the device prove that it has the corresponding - private key. -6. Ask signer for a digest of the firmware binary (in ROM). Consult - the internal firmware database to verify that the TKey, according - to its hardware revision, is running the expected firmware. -7. Sign a message consisting of the UDI, firmware digest, and signer - public key with a vendor signature. -8. Publish the [Verification file](#verification-file) containing the - tag and digest of the signer program used, the vendor signature, - and the timestamp when signature was made. This should be indexed - by the UDI. - -The following diagram contains an overview of how data flows during -provisioning: - -![Data flow during provisioning](/signing-procedure.svg) - -### Verifying - -1. Retrieve the UDI from the device under verification. -2. Get the [Verification file](#verification-file) with the vendor - signature, signer tag and digest for this UDI. -3. Run the signer with the same tag and digest on the device under - verification. -4. Retrieve the signer public key. -5. Ask signer to sign a random challenge. -6. Verify the signature of the random challenge with the signer public - key thus proving that the device under verification has access to the - corresponding private key. -7. Ask signer for a digest of the firmware binary (in ROM). Consult - the internal firmware database to verify that the TKey, according - to its hardware revision, is running the expected firmware. -8. Recreate the message of UDI, firmware digest and signer public key. -9. Verify the vendor signature of the message, thus proving that the - UDI, the firmware, and this private/public key pair was the same - during vendor signing. - -Note that the exact same signer binary that was used for producing the -signer signature during provisioning *must* be used when verifying it. -If a different signer is used then the device private/public key will -not match, even if the TKey is the same. A verifier must check the -"hash" field and complain if it does not have a signer binary with the -same digest. - -## Building tkey-verification - -Build the `tkey-verification` tool with the test file containing -public key(s) for vendor signing/verify. +Insert the device under verification, the TKey to be signed by the +vendor, then: ``` -$ cp test-vendor-signing-pubkeys.txt cmd/tkey-verification/vendor-signing-pubkeys.txt -$ make +$ ./tkey-verification remote-sign --config tkey-verification.yaml.example-remote-sign +Auto-detected serial port /dev/ttyACM0 +Connecting to device on serial port /dev/ttyACM0 ... +Firmware name0:'tk1 ' name1:'mkdf' version:4 +Loading verisigner-app built from tag:verisigner-v0.0.1 hash:9598910ec9ebe2504a5f894de6f8e067… ... +App loaded. +App name0:'veri' name1:'sign' version:1 +TKey UDI: 0x0001020304050607(BE) VendorID: 0x0010 ProductID: 8 ProductRev: 3 +TKey firmware with size:3204 and verified hash:31accb1c40febc2bf02f48656a943336… +Remote Sign was successful ``` -See below if you need to get hold of a different public key. +The signing server should now have signed and saved a submit request +file under `signatures` with a filename generated from the Unique +Device Identifier, typically something like `0133704100000015` if from +Tillitis, but `0001020304050607` if the bitstream has been built +directly from +[tillitis-key1](https://github.com/tillitis/tillitis-key1). -The device apps used for signing is included in binary form under -`cmd/tkey-verification/bins/`. See more info under [Building included -device apps](#building-included-device-apps) if you want to build them -yourself. - -## Maintenance +To submit the TKey Identity using the submit request file, first +create two empty directories to hold the processed submit request file +and the generated verification file. Then run `tkey-sigsum-submit`: -See [Implementation notes](doc/implementation-notes.md) for more -in-depth notes on the program. +``` +$ mkdir processed-submissions +$ mkdir verifications +$ ./tkey-sigsum-submit -m signatures -n processed-submissions -d verifications +2025/05/04 12:04:34 [INFO] Attempting to submit checksum#1 to log: https://test.sigsum.org/barreleye +2025/05/04 12:04:35 [INFO] Attempting to retrieve proof for checksum#1 +``` -- `tkey-verification` contains an in-code database mapping known - hardware revisions (first half of the Unique Device Identifier) to - their expected firmware size and hash. This needs to be maintained - in - [cmd/tkey-verification/firmwares.go](cmd/tkey-verification/firmwares.go) - in `NewFirmwares()`. +There should now be a verification file in the `verifications` directory. -## Building included device apps +Before trying to verify you need to remove and re-insert the device +under verification to get it back to firmware mode. Then run +`tkey-verify -d verifications` to point to local verification files in +a directory rather than requesting them over the Internet: -The device apps used for signing is included in binary form under -`cmd/tkey-verification/bins/`. +``` +$ ./tkey-verify -d verifications +TKey UDI: 0x0001020304050607(BE) VendorID: 0x0010 ProductID: 8 ProductRev: 3 +Verified Sigsum proof. Submit key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sQ sigsum key -Reproducible versions of the device app `verisigner` binary included -in this repo can be built from earlier verisigner tags. Checkout the -wanted tag and follow the instructions there. +TKey is genuine! +``` -The [signer](https://github.com/tillitis/tkey-device-signer) binary -can be built reproducible from the tags mentioned in the `bins` -directory. Please note that you have to build without touch -requirement. +For the complete set of commands, see the manual pages +[tkey-verify(1)](doc/tkey-verify.1) and +[tkey-verification(1)](doc/tkey-verification.1) ## Certificates -For your convenience we include a test CA and some test certificates -in `certs`. They expire 10 years after being generated. +For your testing convenience we include a test CA and some test +certificates in `certs`. They expire 10 years after being generated. If you need to rebuild CA, server, and client certs you can use any ordinary X.509 certificate tools like GnuTLS's certtool or OpenSSL to @@ -270,130 +171,116 @@ tool](https://github.com/square/certstrap) and run: $ make certs ``` -## Testing - -- You need 1 TKey and 1 QEMU machine running to try this out (or 2 - TKeys, or 2 QEMU machines, if you manage to get that working). One - is Tillitis' (vendor's) signing TKey, and the other is a TKey that - you want to sign and then verify as genuine. You need to know the - serial port device paths for these. - -- Run the signing server on QEMU (see [the Tillitis Developer - Handbook](https://dev.tillitis.se/tools/#qemu-emulator) for more - information on how to run QEMU). Notice the port QEMU provides when - starting. - - ``` - ./tkey-verification serve-signer --config tkey-verification.yaml.example-serve-signer --port /dev/pts/12 - ``` - -- Insert the device under verification, the TKey to be signed and verified. - -- Get the signing server to sign for a device under verification (here - a hardware TKey) - - ``` - $ ./tkey-verification remote-sign --config tkey-verification.yaml.example-remote-sign - Auto-detected serial port /dev/ttyACM0 - Connecting to device on serial port /dev/ttyACM0 ... - Firmware name0:'tk1 ' name1:'mkdf' version:4 - Loading verisigner-app built from tag:verisigner-v0.0.1 hash:9598910ec9ebe2504a5f894de6f8e067… ... - App loaded. - App name0:'veri' name1:'sign' version:1 - TKey UDI: 0x0001020304050607(BE) VendorID: 0x0010 ProductID: 8 ProductRev: 3 - TKey firmware with size:3204 and verified hash:31accb1c40febc2bf02f48656a943336… - Remote Sign was successful - ``` - -- The signing server should now have signed and saved a verification - file under `signatures` with a filename generated from the Unique - Device Identifier, typically something like `0133704100000015` if - from Tillitis, but `0001020304050607` if the bitstream has been - built directly from - [tillitis-key1](https://github.com/tillitis/tillitis-key1). - -- Before trying to verify you need to remove and re-insert the device - under verification to get it back to firmware mode. - `tkey-verification` always requires to load the signer itself. Then - try to verify against local files in a directory using `verify -d - signatures` (the default is to query a web server): - - ``` - $ ./tkey-verification verify -d signatures - TKey UDI: 0x0001020304050607(BE) VendorID: 0x0010 ProductID: 8 ProductRev: 3 - Reading verification data from file signatures/0001020304050607 ... - TKey is genuine! - ``` - -For the complete set of commands, see the manual page -[tkey-verification(1)](doc/tkey-verification.1). - -## Creating the vendor public keys file - -The vendor's public key is built into the tkey-verification binary -from a text file. - -For each public key, the tag and hash digest of the device app used -when extracting the public key is also provided. The signing server -needs this so that its TKey can have the correct private key when -signing. Note that these tags per public key are independent from and -can be different from the tag used for device signing. - -A test file is provided in `test-vendor-signing-pubkeys.txt`. It -contains the default public key of our QEMU machine, which is -generated when running verisigner v0.0.3. - -If you want to use some other key(s) this is how: - -If you're just testing start a QEMU as a signing endpoint. See above. - -Get the public key from the TKey in the signing server. We provide a -command in `tkey-verification`, `show-pubkey`, for that. The path to -the app binary to use must be given as an argument. - -Example: +## Maintenance -``` -./tkey-verification show-pubkey --port /dev/pts/10 --app cmd/tkey-verification/bins/signer-v1.0.1.bin -Public Key, app tag, and app hash for vendor-signing-pubkeys.txt follows on stdout: -03a7bd3be67cb466869904ec14b9974ebcc6e593abdc4151315ace2511b9c94d signer-v1.0.1 cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4 -``` +There are some compiled-in assets. Most of these are in +[internal/data/data.go](internal/data/data.go). -Enter that line into a file, for instance `other-pubkey.txt`. Then build everything with this file: +- Application binaries: The device apps used for both vendor signing + and device authentication. See below how to maintain. -``` -$ cat other-pubkey.txt >> cmd/tkey-verification/vendor-signing-pubkeys.txt -$ make -``` +- Vendor public keys: The public key from a vendor TKey used for + verification. Might be several, including historical keys. *No + longer* used for signing, but still used for verification. See the + constant `VendorPubKeys`. + +- Sigsum configuration including the Sigsum submit keys which replaces + the old vendor public keys, see the `SigsumConf` and `PolicyStr` + constants. + +- Known firmwares: Expected firmwares for all known TKey models from + the vendor (first half of the Unique Device Identifier). See the + `FirmwaresConf` constant. + +Commands are responsible for initializing their own assets. For +instance, `tkey-verify` needs all of the above, `remote-sign` needs +the application binaries and the firmwares, `serve-signer` needs +application binaries and the Sigsum configuration. + +## Included device app binaries + +The device apps used for signing is included in binary form under +`internal/appbins/bins`. They are stored like this: + +- `name-tag`, like `signer-v1.0.1.bin`: the actual device app binary. +- `name-tag.sha512`, like `signer-v1.0.1.bin.sha512`: the SHA-512 + digest of the above file to help ensure we don't make mistakes. + +The source code from device app `verisigner` binary included under +`bins` can be found using the `verisigner-` tags in this repo. + +The source code for the `signer-*` binaries is from: + +https://github.com/tillitis/tkey-device-signer + +Make reproducible builds of these binaries using the tag and building +with `TKEY_SIGNER_APP_NO_TOUCH=yes`. -## Verification file +To start using a new device app: -The computer running `tkey-verification serve-sign` generates files -in a directory `signatures/` which is named after the Unique Device -Identifier (in hex), for example `signatures/0133704100000015`. This -file is needed in order to be able to verify a TKey. +- Make sure there is a signed tag on the app's repo. +- Build it in a reproducible way (typically using the tkey-builder OCI + image) with `TKEY_SIGNER_APP_NO_TOUCH=yes`. +- Copy the binary and the hash digest file to `internal/appbins/bins/` + with matching names, like `signer-v1.0.1.bin` and the corresponding + `signer-v1.0.1.bin.sha512`. They are built in during compile time + and expects this exact name structure. +- Update the `README.md` in bins to document where this device app + came from. -The file contains: +Then, if this app is meant to be used for TKey authentication: -- timestamp: RFC3339 UTC timestamp when the signature was done. -- apptag: The Git tag of the signer app used on the device under - verification, -- apphash: The hash of the signer app binary used on the device under - verification. Stored in hexadecimal. -- signature: Vendor signature of the message (described above). Stored - in hexadecimal. +- Set a new active device app for device authentication in the + `remote-sign` configuration file by setting the new app's hash in + `signingapphash`. -Example file content: +If the app is meant to be used for the Sigsum submit key, see below. + +## Creating the Sigsum keys + +The vendor's public keys are built into the `tkey-verification` and +`tkey-verify` binaries. Look for `SigsumConf` in +`internal/data/data.go`. + +For each public key, provide, in this order: + +- Name of the key. +- SSH public key corresponding to the private key of that particular + device running the device app mentioned below. +- Tag of the app to run. +- SHA-512 digest of the device app binary. +- Start time of the key. +- End time. + +Note that the device app *must* be known. See [Included device app +binaries](#included-device-app-binaries). + +A test vendor key is provided in `internal/data/data.go`. It contains +the default public key of our QEMU emulator, which is generated when +running verisigner v0.0.3. + +If you want to use some other key(s), insert the vendor TKey you want +to use, then list the public key of the device app you want to use: ``` -{ - "timestamp": "2023-03-03T09:31:51Z", - "apptag": "verisigner-v0.0.1", - "apphash": "9598910ec9ebe2504a5f894de6f8e0677dc94c156c7bd6f7e805a35354b3c85daa4ca66ab93f4d75221b501def457b4cafc933c6cdcf16d1eb8ccba6cccf6630", - "signature": "db4e7a72b720b33f6d4887df0f9dcdd6988ca8adb6b0042d8e8c92b5be3e4e39d908f166d093f3ab20880102d43a2b0c8e31178ab7cdb59977dcf7204116cc0c" -} +./tkey-verification show-pubkey --app internal/appbins/bins/signer-v1.0.1.bin +Public Key, app tag, and app hash for embedded vendor pubkeys follows on stdout: +03a7bd3be67cb466869904ec14b9974ebcc6e593abdc4151315ace2511b9c94d signer-v1.0.1 cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4 +SSH version: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOnvTvmfLRmhpkE7BS5l068xuWTq9xBUTFaziURuclN sigsum key ``` +Enter the SSH key into `SigsumConf` in `internal/data/data.go`. Then +build everything. + +To set this Sigsum key to be the active one used for vendor signing, +change the configuration file for `serve-signer` and change +`activekey`. + +## Example verification and submit request files + +- [Verification file](cmd/tkey-sigsum-submit/testdata/0001020304050607-ver-valid). +- [Submission file](cmd/tkey-sigsum-submit/testdata/0001020304050607-subm-valid). + ## Releases of tkey-verification and reproducible builds `tkey-verification` is released with the help of GoReleaser, see @@ -440,8 +327,8 @@ Until Nov 6, 2024, the license was GPL-2.0 Only. Please note that this project embeds binaries that are released under GPL-2.0 Only, see -[cmd/tkey-verification/bins/README.md](cmd/tkey-verification/bins/README.md) -for more details. +[internal/appbins/bins/README.md](internal/appbins/bins/README.md) for +more details. External source code we have imported are isolated in their own directories. They may be released under other licenses. This is noted diff --git a/RELEASE.md b/RELEASE.md index bb44da0..08ac91a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,22 @@ # Release notes +## Unreleased version + +- Add Sigsum transparency log support. See https://sigsum.org/ +- Remove vendor signing with vendor keys and instead uses the Sigsum + key pair. Vendor key support is retained for verification. +- Introduce lifetime of vendor keys, but limited to Sigsum submit + keys. +- Split out verification by end user to its own program: tkey-verify. +- Introduce new program: tkey-sigsum-submit. +- Lots of refactoring, again. +- Include new signer app, signer-tkey-verification1, built from + https://github.com/tillitis/tkey-device-signer without touch. This + version supports both the older Bellatrix and the upcoming Castor + TKeys. +- Bump Go package tkeyclient to v1.2.0 +- Change license to BSD-2-Clause. + ## v1.0.0 This version brings a lot of refactoring, aiming at simplifying diff --git a/REUSE.toml b/REUSE.toml index c63630b..6c97e24 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -26,19 +26,29 @@ path = [ "certs/tillitis.crl", "certs/tillitis.crt", "certs/tillitis.key", - "cmd/tkey-verification/bins/README.md", - "cmd/tkey-verification/bins/signer-v1.0.1.bin", - "cmd/tkey-verification/bins/signer-v1.0.1.bin.sha512", - "cmd/tkey-verification/bins/verisigner-v0.0.3.bin", - "cmd/tkey-verification/bins/verisigner-v0.0.3.bin.deps", - "cmd/tkey-verification/bins/verisigner-v0.0.3.bin.sha512", - "cmd/tkey-verification/vendor-signing-pubkeys.txt", - "doc/implementation-notes.md", + "cmd/tkey-sigsum-submit/testdata/0001020304050607-subm-valid", + "cmd/tkey-sigsum-submit/testdata/0001020304050607-ver-valid", + "cmd/tkey-sigsum-submit/testdata/000102030400DEAD-subm-invalid-sig", + "cmd/tkey-sigsum-submit/testdata/policy", + "doc/design.md", "doc/tkey-verification.1", "doc/tkey-verification.scd", + "doc/tkey-verify.1", + "doc/tkey-verify.scd", + "doc/tkey-sigsum-submit.1", + "doc/tkey-sigsum-submit.scd", "go.mod", "go.sum", "release-builds/tkey-verification_0.0.2_linux-amd64.sha512", + "internal/appbins/bins/README.md", + "internal/appbins/bins/signer-tkey-verification-1.bin", + "internal/appbins/bins/signer-tkey-verification-1.bin.sha512", + "internal/appbins/bins/signer-v1.0.1.bin", + "internal/appbins/bins/signer-v1.0.1.bin.sha512", + "internal/appbins/bins/verisigner-v0.0.3.bin", + "internal/appbins/bins/verisigner-v0.0.3.bin.deps", + "internal/appbins/bins/verisigner-v0.0.3.bin.sha512", + "internal/ssh/README.md", "release-builds/tkey-verification_0.0.2_macos-amd64.sha512", "release-builds/tkey-verification_0.0.2_macos-arm64.sha512", "release-builds/tkey-verification_0.0.2_macos-universal.sha512", @@ -48,7 +58,6 @@ path = [ "release-builds/tkey-verification_0.0.3_macos-arm64.sha512", "release-builds/tkey-verification_0.0.3_macos-universal.sha512", "release-builds/tkey-verification_0.0.3_windows-amd64.exe.sha512", - "test-vendor-signing-pubkeys.txt", "tkey-verification.yaml.example-remote-sign", "tkey-verification.yaml.example-serve-signer" ] @@ -58,8 +67,8 @@ SPDX-License-Identifier = "BSD-2-Clause" [[annotations]] path = [ - "cmd/tkey-verification/bins/signer-v1.0.1.bin", - "cmd/tkey-verification/bins/verisigner-v0.0.3.bin", + "internal/appbins/bins/signer-v1.0.1.bin", + "internal/appbins/bins/verisigner-v0.0.3.bin", ] SPDX-FileCopyrightText = "2022 Tillitis AB " SPDX-License-Identifier = "GPL-2.0-only" diff --git a/cmd/tkey-sigsum-submit/main.go b/cmd/tkey-sigsum-submit/main.go new file mode 100644 index 0000000..491d612 --- /dev/null +++ b/cmd/tkey-sigsum-submit/main.go @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package main + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "os" + "path" + + "github.com/spf13/pflag" + "github.com/tillitis/tkey-verification/internal/sigsum" + "github.com/tillitis/tkey-verification/internal/submission" + "github.com/tillitis/tkey-verification/internal/util" + "github.com/tillitis/tkey-verification/internal/verification" + "sigsum.org/sigsum-go/pkg/proof" + "sigsum.org/sigsum-go/pkg/requests" + "sigsum.org/sigsum-go/pkg/submit" +) + +const progname = "tkey-sigsum-submit" + +var version string +var le = log.New(os.Stderr, "", 0) + +func main() { + var verificationsDir, submissionsDir, processedSubmissionsDir string + var helpOnly, versionOnly bool + + pflag.CommandLine.SetOutput(os.Stderr) + pflag.CommandLine.SortFlags = false + pflag.StringVarP(&submissionsDir, "submissions-dir", "m", "", + "Read and log submission data from each file in `DIRECTORY`") + pflag.StringVarP(&processedSubmissionsDir, "processed-submissions-dir", "n", "", + "Move submission data files to `DIRECTORY` after submission to log") + pflag.StringVarP(&verificationsDir, "verifications-dir", "d", "", + "Write verification data to a file in `DIRECTORY` ") + pflag.BoolVar(&helpOnly, "help", false, "Output this help") + pflag.BoolVar(&versionOnly, "version", false, "Output version information") + pflag.Usage = usage + pflag.Parse() + + if helpOnly { + pflag.Usage() + os.Exit(0) + } + + if versionOnly { + fmt.Printf("%s %s\n", progname, util.Version(version)) + os.Exit(0) + } + + if verificationsDir == "" || submissionsDir == "" || processedSubmissionsDir == "" { + le.Printf("Missing arguments: -d, -m, -n\n\n") + pflag.Usage() + os.Exit(1) + } + + if verificationsDir == processedSubmissionsDir { + le.Printf("processed-submissions-dir and verification-dir cannot be the same.\n\n") + pflag.Usage() + os.Exit(1) + } + + var log sigsum.Log + err := log.FromEmbedded() + if err != nil { + le.Fatalf("Sigsum configuration missing") + } + + submit := SigsumSubmit{ + submDir: submissionsDir, + doneSubmDir: processedSubmissionsDir, + verDir: verificationsDir, + log: log, + } + + err = submit.processSubmissions() + if err != nil { + le.Fatalf("Submission failed: %v", err) + } +} + +type SigsumSubmit struct { + submDir, verDir, doneSubmDir string + log sigsum.Log + + // HTTPClient specifies the HTTP client to use when making requests to the + // log. If nil, a default client is created. + HTTPClient *http.Client +} + +func (s SigsumSubmit) processSubmissions() error { + verFileCount, err := os.ReadDir(s.verDir) + if err != nil { + return fmt.Errorf("failed to read directory '%s': %w", s.verDir, err) + } + if len(verFileCount) != 0 { + return errors.New("verification directory must be empty") + } + + doneSubmFileCount, err := os.ReadDir(s.doneSubmDir) + if err != nil { + return fmt.Errorf("failed to read directory '%s': %w", s.doneSubmDir, err) + } + if len(doneSubmFileCount) != 0 { + return errors.New("processed submission directory must be empty") + } + + entries, err := os.ReadDir(s.submDir) + if err != nil { + return fmt.Errorf("failed to read directory '%s': %w", s.submDir, err) + } + + for _, entry := range entries { + var submission submission.Submission + err = submission.FromFile(path.Join(s.submDir, entry.Name())) + if err != nil { + return fmt.Errorf("invalid submission file: %w", err) + } + } + + for _, entry := range entries { + err = s.processSubmissionFile(entry.Name()) + if err != nil { + return fmt.Errorf("failed to process submission file: %w", err) + } + } + + return nil +} + +func (s SigsumSubmit) processSubmissionFile(fn string) error { + submissionPath := path.Join(s.submDir, fn) + doneSubmissionPath := path.Join(s.doneSubmDir, fn) + verificationPath := path.Join(s.verDir, fn) + + var submission submission.Submission + err := submission.FromFile(submissionPath) + if err != nil { + return fmt.Errorf("failed to open submission file: %w", err) + } + + submitConfig := submit.Config{ + Policy: s.log.Policy, + HTTPClient: s.HTTPClient, + } + + proof, err := logDevice(submission.Request, submitConfig) + if err != nil { + return fmt.Errorf("failed to log device: %w", err) + } + + verification := verification.Verification{ + Type: verification.VerProof, + Timestamp: submission.Timestamp, + AppTag: submission.AppTag, + AppHash: submission.AppHash, + Proof: proof, + } + + _, err = verification.VerifyProofDigest(submission.Request.Message, s.log) + if err != nil { + return fmt.Errorf("got invalid proof: %w", err) + } + + err = verification.ToFile(verificationPath) + if err != nil { + return fmt.Errorf("failed to store verification file: %w", err) + } + + err = os.Rename(submissionPath, doneSubmissionPath) + if err != nil { + return fmt.Errorf("failed to move verification file: %w", err) + } + + return nil +} + +func logDevice(req requests.Leaf, submitConfig submit.Config) (proof.SigsumProof, error) { + ctx := context.Background() + reqs := []requests.Leaf{req} + + proofs, err := submit.SubmitLeafRequests(ctx, &submitConfig, reqs) + if err != nil { + return proof.SigsumProof{}, fmt.Errorf("failed to submit sigsum log request: %w", err) + } + if len(proofs) != 1 { + return proof.SigsumProof{}, fmt.Errorf("expected one proof from log, got %d", len(proofs)) + } + + return proofs[0], nil +} + +func usage() { + desc := fmt.Sprintf(`Usage: %s + +Takes a sigsum submit request from each file in the "submissions-dir" +directory and submits it to the log. Each submission file generates a +corresponding verification file. The verification files are written to +the "verifications-dir" directory, with the same name as the submission +file. +`, progname) + + le.Printf("%s\n\nFlags:\n%s\n", desc, pflag.CommandLine.FlagUsagesWrapped(86)) +} diff --git a/cmd/tkey-sigsum-submit/main_test.go b/cmd/tkey-sigsum-submit/main_test.go new file mode 100644 index 0000000..c6a4dd3 --- /dev/null +++ b/cmd/tkey-sigsum-submit/main_test.go @@ -0,0 +1,366 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package main + +import ( + "bytes" + "fmt" + "io" + "net/http" + "strings" + "sync" + + "os" + "path" + "testing" + + "github.com/tillitis/tkey-verification/internal/sigsum" +) + +// Test Sigsum submit key corresponding to verisigner-0.3 running on QEMU with test UDS. +const TestSigsumConf = ` +tillitis-sigsum-test +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sQ sigsum key +verisigner-v0.0.3 +f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 +2024-09-16T08:10:33+02:00 +2125-09-16T08:11:33+02:00 +` + +func Test_processSubmissionFileShouldGenerateVerificationFileFromSubmissionFile(t *testing.T) { + submit := SigsumSubmit{ + submDir: t.TempDir(), + doneSubmDir: t.TempDir(), + verDir: t.TempDir(), + HTTPClient: &http.Client{Transport: ts.NewFakeTransport()}, + } + + policyStr, err := os.ReadFile("testdata/policy") + if err != nil { + t.Fatal(err) + } + + var log sigsum.Log + + if err = log.FromString(TestSigsumConf, string(policyStr)); err != nil { + t.Fatal(err) + } + + submit.log = log + + fn := "0001020304050607" + submFile := path.Join(submit.submDir, fn) + verFile := path.Join(submit.verDir, fn) + + copyFile(submFile, "testdata/0001020304050607-subm-valid") + + err = submit.processSubmissionFile(fn) + if err != nil { + t.Fatalf("Got error when running processSubmissionFile: %v", err) + } + + assertFileContentsEqual(t, verFile, "testdata/0001020304050607-ver-valid") +} + +func Test_processSubmissionDir(t *testing.T) { + + type Params []struct { + name string + preSubmFiles map[string]string // Sample files to copy from testdata to submissions dir before running. Maps source->destination filename + preDoneSubmFiles map[string]string // Sample files to copy from testdata to processed-submissions dir before running. Maps source->destination filename + preVerFiles map[string]string // Sample files to copy from testdata to verification dir before running. Maps source->destination filename + postSubmFiles map[string]string // Sample files to look for in submissions dir after running. Maps source->destination filename + postDoneSubmFiles map[string]string // Sample files to look for in processed submissions dir after running. Maps source->destination filename + postVerFiles map[string]string // Sample files to look for in verifications dir after running. Maps source->destination filename + errString string // Expected error string. Use empty string if error is expected to be nil. + } + + tests := Params{ + { + name: "One valid submission file generates one verification file", + preSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + preDoneSubmFiles: map[string]string{}, + preVerFiles: map[string]string{}, + postSubmFiles: map[string]string{}, + postDoneSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + postVerFiles: map[string]string{"0001020304050607-ver-valid": "0001020304050607"}, + errString: "", + }, + { + name: "Should abort if verification directory is not empty on start", + preSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + preDoneSubmFiles: map[string]string{}, + preVerFiles: map[string]string{"0001020304050607-ver-valid": "0001020304050607"}, + postSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + postDoneSubmFiles: map[string]string{}, + postVerFiles: map[string]string{"0001020304050607-ver-valid": "0001020304050607"}, + errString: "verification directory must be empty", + }, + { + name: "Should abort if processed submissions directory is not empty on start", + preSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + preDoneSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + preVerFiles: map[string]string{}, + postSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + postDoneSubmFiles: map[string]string{"0001020304050607-subm-valid": "0001020304050607"}, + postVerFiles: map[string]string{}, + errString: "processed submission directory must be empty", + }, + { + name: "Should abort if any submission file is invalid", + preSubmFiles: map[string]string{ + "0001020304050607-subm-valid": "0001020304050607", + "000102030400DEAD-subm-invalid-sig": "000102030400DEAD", + }, + preDoneSubmFiles: map[string]string{}, + preVerFiles: map[string]string{}, + postSubmFiles: map[string]string{ + "0001020304050607-subm-valid": "0001020304050607", + "000102030400DEAD-subm-invalid-sig": "000102030400DEAD", + }, + postDoneSubmFiles: map[string]string{}, + postVerFiles: map[string]string{}, + errString: "invalid submission file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + + submit := SigsumSubmit{ + submDir: path.Join(tempDir, "submissions"), + doneSubmDir: path.Join(tempDir, "processed"), + verDir: path.Join(tempDir, "verifications"), + HTTPClient: &http.Client{Transport: ts.NewFakeTransport()}, + } + + copySamplesToDir(submit.submDir, tt.preSubmFiles) + copySamplesToDir(submit.verDir, tt.preVerFiles) + copySamplesToDir(submit.doneSubmDir, tt.preDoneSubmFiles) + + policyStr, err := os.ReadFile("testdata/policy") + if err != nil { + t.Fatal(err) + } + + var log sigsum.Log + + if err = log.FromString(TestSigsumConf, string(policyStr)); err != nil { + t.Fatal(err) + } + + submit.log = log + + err = submit.processSubmissions() + + assertErrorMsgStartsWith(t, err, tt.errString) + assertDirContainsOnly(t, submit.submDir, tt.postSubmFiles) + assertDirContainsOnly(t, submit.doneSubmDir, tt.postDoneSubmFiles) + assertDirContainsOnly(t, submit.verDir, tt.postVerFiles) + }) + } +} + +func assertErrorMsgStartsWith(t *testing.T, err error, errString string) { + t.Helper() + + if errString == "" { + // Expecting nil + if err != nil { + t.Logf("Unexpected error %v", err) + t.Fail() + } + } else { + // Expecting error + if err == nil || !strings.HasPrefix(err.Error(), errString) { + t.Logf("Unexpected error '%v', should start with '%v'", err, errString) + t.Fail() + } + } +} + +// Assert that directory dir only contains the files in specified in samples. +// The contents of each file is checked for equality with the contents of its +// corresponding sample file located in the testdata directory. +func assertDirContainsOnly(t *testing.T, dir string, samples map[string]string) { + t.Helper() + + assertFileCount(t, dir, len(samples)) + for sampleFn, createdFn := range samples { + createdFile := path.Join(dir, createdFn) + samplePath := path.Join("testdata", sampleFn) + assertFileContentsEqual(t, createdFile, samplePath) + } +} + +// Copy files from directory testdata to dstDir. `samples` maps source filename +// to destination filename. +func copySamplesToDir(dstDir string, samples map[string]string) { + mustCreateDir(dstDir) + for srcFn, dstFn := range samples { + dstPath := path.Join(dstDir, dstFn) + srcPath := path.Join("testdata", srcFn) + copyFile(dstPath, srcPath) + } +} + +func copyFile(dstPath string, srcPath string) { + srcData, err := os.ReadFile(srcPath) + if err != nil { + msg := fmt.Sprintf("Could not copy file: %v", err) + panic(msg) + } + + err = os.WriteFile(dstPath, srcData, 0600) + if err != nil { + msg := fmt.Sprintf("Could not copy to file: %v", err) + panic(msg) + } +} + +func assertFileCount(t *testing.T, dir string, wantCount int) { + t.Helper() + + count := len(mustListFiles(dir)) + if count != wantCount { + t.Logf("Folder '%s' contains %d files, wanted %d.", dir, count, wantCount) + t.Fail() + } +} + +func mustListFiles(dir string) []os.DirEntry { + entries, err := os.ReadDir(dir) + if err != nil { + msg := fmt.Sprintf("Failed to read directory '%s': %v", dir, err) + panic(msg) + } + + return entries +} + +func mustCreateDir(path string) { + err := os.Mkdir(path, 0700) + if err != nil { + msg := fmt.Sprintf("Failed to create directory '%s': %v", path, err) + panic(msg) + } +} + +func assertFileContentsEqual(t *testing.T, aPath string, bPath string) { + t.Helper() + + aData, err := os.ReadFile(aPath) + if err != nil { + t.Fatalf("Could not read file: %v", err) + } + + bData, err := os.ReadFile(bPath) + if err != nil { + t.Fatalf("Could not read file: %v", err) + } + + if !bytes.Equal(aData, bData) { + t.Logf("Contents of '%s' and '%s' are not equal", aPath, bPath) + t.Fail() + } +} + +// fakeTransport is an HTTP Transport faking a Sigsum Log. Just enough behavior +// is faked to make it possible for a client to add a predetermined leaf and +// generate a predetermined proof. +// +// Will panic if the 'add-leaf' request body has unexpected content. +// Will panic when accessing an unexpected URL. +// Will panic on run time errors. +type fakeTransport struct { + cacheIndex int +} + +func (ft fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) { + const addLeafURL = "http://sigsumlog.test/add-leaf" + const expectedAddLeafRequestBody = `message=f23e454ee9c9627dd1a80f6ab2e1565fa0cda3a7c91f853eb8099ff645674719 +signature=b4f9eabdcb6b05d259e964ba6fa427c178b5586d30e6b4026287656c8a7ee2674af33d2c05701ea8f98458fe7c54b787c7a73c0fda6f09046bcf7604cea86c00 +public_key=50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10 +` + const getTreeHeadURL = "http://sigsumlog.test/get-tree-head" + const getTreeHeadResponseBody = `size=4684 +root_hash=07e183bd7b31636eee13edba7ee64cc586363aea9e7cdd1579c047e2643a87b7 +signature=9af6929d26d4dfb94802cba6f1cd988ac7165b73bfb1bbd1922175a771b5408056e2605e386a231b13ce7dda089db07beb35e2b387e88fa69e322f8839b01804 +cosignature=0d4f46a219ab309cea48cde9712e7d8486fc99802f873175eeab70fb84b4f5a4 1756811283 8aff7c90eeeb74080bd81948afeb83ba5f25868ed06bb6103da45ecbc70c0dd8a195f6ab792bfa70a0f1af7d7dbd1c5beb480fc13d18f695a6fff9a8bbbb4709 +` + const getInclusionProofURL = "http://sigsumlog.test/get-inclusion-proof/4684/eca101878ff4cad7aac85f70ed41e5d9e04544b84be6b3ab2834df49496680e6" + const getInclusionProofResponseBody = `leaf_index=4683 +node_hash=deed2b128c089094c865ce893aa9898f131460ea539f5ed38dd5a8054e087fd8 +node_hash=ab8d1b7eb823ad50ffe619017aa14f66cde57a47a6d1739aa06bd64cbbffc91f +node_hash=1e2ef1212c516b59b7d3948958be90d8ef58acc427d3b4a558757041c6507db0 +node_hash=72347e82644eea74e0e3806c16a40b396353c934850b98d9013658f13b99ecb9 +node_hash=9103c094b7a2cbf2da7c1e1d492906ac0b9062cb0dee3a8e20f5fbff5b219c79 +node_hash=9ca6b461d616cf790a32a967574087298abb4cd0c3da938b7fed143b7d92b5ec +` + + switch req.URL.String() { + case addLeafURL: + body, err := io.ReadAll(req.Body) + if err != nil { + panic("Fake server could not read 'add-leaf' body") + } + + if string(body) != expectedAddLeafRequestBody { + panic("Fake server got unexpected 'add-leaf' body") + } + + if ts.getLeafWasCalled(ft) { + return &http.Response{StatusCode: http.StatusOK}, nil + } + ts.setLeafWasCalled(ft) + + return &http.Response{StatusCode: http.StatusAccepted}, nil + case getTreeHeadURL: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(getTreeHeadResponseBody)), + }, nil + case getInclusionProofURL: + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(getInclusionProofResponseBody)), + }, nil + } + + errStr := "Fake server got unexpected URL: %s" + req.URL.String() + panic(errStr) +} + +// transportStates is a thread safe storage used by fakeTransport to store +// state between requests +type transportStates struct { + addLeafWasCalled []bool + lock sync.Mutex +} + +func (ts *transportStates) NewFakeTransport() fakeTransport { + ts.lock.Lock() + defer ts.lock.Unlock() + + i := len(ts.addLeafWasCalled) + ts.addLeafWasCalled = append(ts.addLeafWasCalled, false) + return fakeTransport{cacheIndex: i} +} + +func (ts *transportStates) setLeafWasCalled(ft fakeTransport) { + ts.lock.Lock() + defer ts.lock.Unlock() + + ts.addLeafWasCalled[ft.cacheIndex] = true +} + +func (ts *transportStates) getLeafWasCalled(ft fakeTransport) bool { + ts.lock.Lock() + defer ts.lock.Unlock() + + return ts.addLeafWasCalled[ft.cacheIndex] +} + +var ts = transportStates{} diff --git a/cmd/tkey-sigsum-submit/testdata/000102030400DEAD-subm-invalid-sig b/cmd/tkey-sigsum-submit/testdata/000102030400DEAD-subm-invalid-sig new file mode 100644 index 0000000..e9611d9 --- /dev/null +++ b/cmd/tkey-sigsum-submit/testdata/000102030400DEAD-subm-invalid-sig @@ -0,0 +1 @@ +{"timestamp":"2025-09-02T10:56:48Z","apptag":"signer-v1.0.1","apphash":"cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4","request":"message=f23e454ee9c9627dd1a80f6ab2e1565fa0cda3a7c91f853eb8099ff645674719\nsignature=b4f9eabdcb6b05d259e964ba6fa427c178b5586d30e6b4026287656c8a7ee2674af33d2c05701ea8f98458fe7c54b787c7a73c0fda6f09046bcf000000000000\npublic_key=50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10\n"} diff --git a/cmd/tkey-sigsum-submit/testdata/0001020304050607-subm-valid b/cmd/tkey-sigsum-submit/testdata/0001020304050607-subm-valid new file mode 100644 index 0000000..4464e0f --- /dev/null +++ b/cmd/tkey-sigsum-submit/testdata/0001020304050607-subm-valid @@ -0,0 +1 @@ +{"timestamp":"2025-09-02T10:56:48Z","apptag":"signer-v1.0.1","apphash":"cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4","request":"message=f23e454ee9c9627dd1a80f6ab2e1565fa0cda3a7c91f853eb8099ff645674719\nsignature=b4f9eabdcb6b05d259e964ba6fa427c178b5586d30e6b4026287656c8a7ee2674af33d2c05701ea8f98458fe7c54b787c7a73c0fda6f09046bcf7604cea86c00\npublic_key=50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10\n"} diff --git a/cmd/tkey-sigsum-submit/testdata/0001020304050607-ver-valid b/cmd/tkey-sigsum-submit/testdata/0001020304050607-ver-valid new file mode 100644 index 0000000..8ae3ada --- /dev/null +++ b/cmd/tkey-sigsum-submit/testdata/0001020304050607-ver-valid @@ -0,0 +1 @@ +{"timestamp":"2025-09-02T10:56:48Z","apptag":"signer-v1.0.1","apphash":"cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4","proof":"version=2\nlog=4e89cc51651f0d95f3c6127c15e1a42e3ddf7046c5b17b752689c402e773bb4d\nleaf=e7f5de9e44de09b425853f0b267a06fe3eca528c96c8fabd521e5cbfaec83806 b4f9eabdcb6b05d259e964ba6fa427c178b5586d30e6b4026287656c8a7ee2674af33d2c05701ea8f98458fe7c54b787c7a73c0fda6f09046bcf7604cea86c00\n\nsize=4684\nroot_hash=07e183bd7b31636eee13edba7ee64cc586363aea9e7cdd1579c047e2643a87b7\nsignature=9af6929d26d4dfb94802cba6f1cd988ac7165b73bfb1bbd1922175a771b5408056e2605e386a231b13ce7dda089db07beb35e2b387e88fa69e322f8839b01804\ncosignature=0d4f46a219ab309cea48cde9712e7d8486fc99802f873175eeab70fb84b4f5a4 1756811283 8aff7c90eeeb74080bd81948afeb83ba5f25868ed06bb6103da45ecbc70c0dd8a195f6ab792bfa70a0f1af7d7dbd1c5beb480fc13d18f695a6fff9a8bbbb4709\n\nleaf_index=4683\nnode_hash=deed2b128c089094c865ce893aa9898f131460ea539f5ed38dd5a8054e087fd8\nnode_hash=ab8d1b7eb823ad50ffe619017aa14f66cde57a47a6d1739aa06bd64cbbffc91f\nnode_hash=1e2ef1212c516b59b7d3948958be90d8ef58acc427d3b4a558757041c6507db0\nnode_hash=72347e82644eea74e0e3806c16a40b396353c934850b98d9013658f13b99ecb9\nnode_hash=9103c094b7a2cbf2da7c1e1d492906ac0b9062cb0dee3a8e20f5fbff5b219c79\nnode_hash=9ca6b461d616cf790a32a967574087298abb4cd0c3da938b7fed143b7d92b5ec\n"} diff --git a/cmd/tkey-sigsum-submit/testdata/policy b/cmd/tkey-sigsum-submit/testdata/policy new file mode 100644 index 0000000..6ba6a9d --- /dev/null +++ b/cmd/tkey-sigsum-submit/testdata/policy @@ -0,0 +1,8 @@ +log 4644af2abd40f4895a003bca350f9d5912ab301a49c77f13e5b6d905c20a5fe6 http://sigsumlog.test + +witness testwitness 5c35281928e9da396beede5f22a2251a589e6ac6de52a4de85de8634ffccaf6d + +group demo-quorum-rule all testwitness + +quorum demo-quorum-rule + diff --git a/cmd/tkey-verification/api.go b/cmd/tkey-verification/api.go index 8527bdd..cefd800 100644 --- a/cmd/tkey-verification/api.go +++ b/cmd/tkey-verification/api.go @@ -7,14 +7,17 @@ import ( "crypto/ed25519" "crypto/sha512" "encoding/hex" - "encoding/json" "errors" "fmt" "os" "sync" "time" + "github.com/tillitis/tkey-verification/internal/submission" "github.com/tillitis/tkey-verification/internal/tkey" + sigsumcrypto "sigsum.org/sigsum-go/pkg/crypto" + "sigsum.org/sigsum-go/pkg/requests" + "sigsum.org/sigsum-go/pkg/types" ) const MessageLen = tkey.UDISize + sha512.Size + ed25519.PublicKeySize @@ -42,7 +45,7 @@ func (*API) Ping(_ *struct{}, _ *struct{}) error { type Args struct { UDIBE []byte AppTag string - AppHash []byte + AppHash [sha512.Size]byte Message []byte } @@ -78,16 +81,16 @@ func (api *API) Sign(args *Args, _ *struct{}) error { return ErrWrongLen } - signature, err := api.tk.Sign(args.Message) + signer := TkeySigsumSigner{api.tk} + sigsumMsg := sigsumcrypto.HashBytes(args.Message) + signature, err := types.SignLeafMessage(signer, sigsumMsg[:]) if err != nil { - le.Printf("tkey.Sign failed: %v", err) - return ErrSignFailed } - if !ed25519.Verify(api.vendorPubKey, args.Message, signature) { - le.Printf("Vendor signature failed verification\n") - + leafReq := requests.Leaf{Message: sigsumMsg, Signature: signature, PublicKey: signer.Public()} + _, err = leafReq.Verify() + if err != nil { return ErrVerificationFailed } @@ -99,20 +102,15 @@ func (api *API) Sign(args *Args, _ *struct{}) error { return ErrSigExist } - json, err := json.Marshal(Verification{ - Timestamp: time.Now().UTC().Format(time.RFC3339), + subm := submission.Submission{ + Timestamp: time.Now().UTC(), AppTag: args.AppTag, - AppHash: hex.EncodeToString(args.AppHash), - Signature: hex.EncodeToString(signature), - }) - if err != nil { - le.Printf("JSON Marshal failed: %v", err) - - return ErrInternal + AppHash: args.AppHash, + Request: leafReq, } - //nolint:gosec - if err = os.WriteFile(fn, append(json, '\n'), 0o644); err != nil { + err = subm.ToFile(fn) + if err != nil { le.Printf("WriteFile %s failed: %v", fn, err) return ErrInternal @@ -122,3 +120,36 @@ func (api *API) Sign(args *Args, _ *struct{}) error { return nil } + +type TkeySigsumSigner struct { + tk tkey.TKey +} + +func (s TkeySigsumSigner) Public() sigsumcrypto.PublicKey { + pubkey, err := s.tk.GetPubkey() + if err != nil { + s.tk.Close() + le.Fatal("GetPubKey failed: %w", err) + } + if len(pubkey) != sigsumcrypto.PublicKeySize { + le.Fatalf("internal error, unexpected public key size %d: ", len(pubkey)) + } + var ret sigsumcrypto.PublicKey + copy(ret[:], pubkey) + + return ret +} + +func (s TkeySigsumSigner) Sign(msg []byte) (sigsumcrypto.Signature, error) { + sig, err := s.tk.Sign(msg) + if err != nil { + return sigsumcrypto.Signature{}, fmt.Errorf("%w", err) + } + if len(sig) != sigsumcrypto.SignatureSize { + return sigsumcrypto.Signature{}, fmt.Errorf("internal error, unexpected signature size %d: ", len(sig)) + } + var ret sigsumcrypto.Signature + copy(ret[:], sig) + + return ret, nil +} diff --git a/cmd/tkey-verification/common.go b/cmd/tkey-verification/common.go index 4098d4d..74707ff 100644 --- a/cmd/tkey-verification/common.go +++ b/cmd/tkey-verification/common.go @@ -5,41 +5,29 @@ package main import ( "bytes" - "crypto/ed25519" - "crypto/sha512" + "errors" + "fmt" + "github.com/tillitis/tkey-verification/internal/firmware" "github.com/tillitis/tkey-verification/internal/tkey" ) -func buildMessage(udiBE, fwHash, pubKey []byte) ([]byte, error) { - var buf bytes.Buffer +func verifyFirmwareHash(tk tkey.TKey, firmwares firmware.Firmwares) (firmware.Firmware, error) { + var expectedFW firmware.Firmware + var err error - if l := len(udiBE); l != tkey.UDISize { - return nil, ErrWrongLen - } - buf.Write(udiBE) - - if l := len(fwHash); l != sha512.Size { - return nil, ErrWrongLen - } - buf.Write(fwHash) - - if l := len(pubKey); l != ed25519.PublicKeySize { - return nil, ErrWrongLen + expectedFW, err = firmwares.GetFirmware(tk.Udi) + if err != nil { + return expectedFW, errors.New("no firmware for UDI") } - buf.Write(pubKey) - - return buf.Bytes(), nil -} -func verifyFirmwareHash(expectedFW Firmware, tk tkey.TKey) (Firmware, error) { fwHash, err := tk.GetFirmwareHash(expectedFW.Size) if err != nil { - return Firmware{}, IOError{path: "TKey", err: err} + return expectedFW, fmt.Errorf("couldn't get firmware digest from TKey: %w", err) } if !bytes.Equal(expectedFW.Hash[:], fwHash) { le.Printf("TKey does not have expected firmware hash %0x…, but instead %0x…", expectedFW.Hash[:16], fwHash[:16]) - return Firmware{}, ErrWrongFirmware + return expectedFW, ErrWrongFirmware } return expectedFW, nil diff --git a/cmd/tkey-verification/config.go b/cmd/tkey-verification/config.go index 52c62eb..3de7844 100644 --- a/cmd/tkey-verification/config.go +++ b/cmd/tkey-verification/config.go @@ -5,6 +5,7 @@ package main import ( "crypto/tls" + "fmt" "os" "gopkg.in/yaml.v2" @@ -16,11 +17,11 @@ type Server struct { } type ServerConfig struct { - CACert string `yaml:"cacert"` - ServerCert string `yaml:"servercert"` - ServerKey string `yaml:"serverkey"` - ListenAddr string `yaml:"listen"` - VendorSigningAppHash string `yaml:"vendorapphash"` + CACert string `yaml:"cacert"` + ServerCert string `yaml:"servercert"` + ServerKey string `yaml:"serverkey"` + ListenAddr string `yaml:"listen"` + ActiveKey string `yaml:"activekey"` } type ProvConfig struct { @@ -36,12 +37,12 @@ func loadServeSignerConfig(fn string) (ServerConfig, error) { rawConfig, err := os.ReadFile(fn) if err != nil { - return conf, IOError{path: fn, err: err} + return conf, fmt.Errorf("couldn't read config file: %w", err) } err = yaml.Unmarshal(rawConfig, &conf) if err != nil { - return conf, ParseError{what: "config", err: err} + return conf, fmt.Errorf("parse error in config file: %w", err) } return conf, nil @@ -52,12 +53,12 @@ func loadRemoteSignConfig(fn string) (ProvConfig, error) { rawConfig, err := os.ReadFile(fn) if err != nil { - return conf, IOError{path: fn, err: err} + return conf, fmt.Errorf("couldn't read config file: %w", err) } err = yaml.Unmarshal(rawConfig, &conf) if err != nil { - return conf, ParseError{what: "config", err: err} + return conf, fmt.Errorf("parse error in config file: %w", err) } return conf, nil diff --git a/cmd/tkey-verification/error.go b/cmd/tkey-verification/error.go index c1c25af..eb4d30c 100644 --- a/cmd/tkey-verification/error.go +++ b/cmd/tkey-verification/error.go @@ -3,8 +3,6 @@ package main -import "fmt" - type constError string func (err constError) Error() string { @@ -24,72 +22,3 @@ const ( ErrIO = constError("I/O error") ErrWrongFirmware = constError("not expected firmware") ) - -// More complex errors get their own type below - -type MissingError struct { - what string -} - -func (e MissingError) Error() string { - return fmt.Sprintf("missing: %v", e.what) -} - -type EqualError struct { - one interface{} - two interface{} -} - -func (e EqualError) Error() string { - return fmt.Sprintf("not equal: %v != %v", e.one, e.two) -} - -type IOError struct { - path string - err error -} - -func (e IOError) Error() string { - return fmt.Sprintf("I/O error on %v: %v", e.path, e.err) -} - -func (e IOError) Unwrap() error { - return e.err -} - -type ParseError struct { - what string - err error -} - -func (e ParseError) Error() string { - return fmt.Sprintf("couldn't parse %v: %v", e.what, e.err) -} - -func (e ParseError) Unwrap() error { - return e.err -} - -type SimpleParseError struct { - msg string -} - -func (e SimpleParseError) Error() string { - return e.msg -} - -type ExistError struct { - what string -} - -func (e ExistError) Error() string { - return fmt.Sprintf("already exists: %v", e.what) -} - -type RangeError struct { - what string -} - -func (e RangeError) Error() string { - return fmt.Sprintf("out of range: %v", e.what) -} diff --git a/cmd/tkey-verification/main.go b/cmd/tkey-verification/main.go index bdfafa0..a4afed7 100644 --- a/cmd/tkey-verification/main.go +++ b/cmd/tkey-verification/main.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/spf13/pflag" + "github.com/tillitis/tkey-verification/internal/util" "github.com/tillitis/tkeyclient" ) @@ -18,7 +19,6 @@ const progname = "tkey-verification" const signaturesDir = "signatures" const ( - defaultBaseURL = "https://tkey.tillitis.se/verify" defaultConfigFile = "./tkey-verification.yaml" ) @@ -34,31 +34,21 @@ type Device struct { func main() { var dev Device - var baseURL, baseDir, configFile, binPath string - var checkConfigOnly, verbose, showURLOnly, versionOnly, build, helpOnly bool - - if version == "" { - version = readBuildInfo() - } + var configFile, binPath string + var checkConfigOnly, verbose, versionOnly, build, helpOnly bool pflag.CommandLine.SetOutput(os.Stderr) pflag.CommandLine.SortFlags = false pflag.StringVar(&dev.Path, "port", "", "Set serial port device `PATH`. If this is not passed, auto-detection will be attempted.") pflag.IntVarP(&dev.Speed, "speed", "s", tkeyclient.SerialSpeed, - "Set serial port `speed` in bits per second.") + "Set serial port `SPEED` in bits per second.") pflag.BoolVar(&verbose, "verbose", false, "Enable verbose output.") pflag.StringVar(&configFile, "config", defaultConfigFile, "`PATH` to configuration file (commands: serve-signer, remote-sign).") pflag.BoolVar(&checkConfigOnly, "check-config", false, "Only check that the configuration is usable, then exit (commands: serve-signer, remote-sign).") - pflag.BoolVarP(&showURLOnly, "show-url", "u", false, - "Only output the URL to the verification data that should be downloaded (command: verify).") - pflag.StringVarP(&baseDir, "base-dir", "d", "", - "Read verification data from a file located in `DIRECTORY` and named after the TKey UDI in hex, instead of from a URL. You can for example first use \"verify --show-url\" and download the verification file manually on some other computer, then transfer the file back and use \"verify --base-dir .\" (command: verify).") - pflag.StringVar(&baseURL, "base-url", defaultBaseURL, - "Set the base `URL` of verification server for fetching verification data (command: verify).") pflag.StringVarP(&binPath, "app", "a", "", "`PATH` to the device app to show vendor signing pubkey (command: show-pubkey).") pflag.BoolVar(&versionOnly, "version", false, "Output version information.") @@ -72,7 +62,7 @@ func main() { os.Exit(0) } if versionOnly { - fmt.Printf("%s %s\n", progname, version) + fmt.Printf("%s %s\n", progname, util.Version(version)) os.Exit(0) } @@ -93,16 +83,6 @@ func main() { cmd := pflag.Args()[0] - // Use Lookup to tell if user changed string with default value - if cmd == "verify" && (pflag.CommandLine.Lookup("config").Changed || checkConfigOnly) { - le.Printf("Cannot use --config/--check-config with this command.\n") - os.Exit(2) - } - if cmd != "verify" && (showURLOnly || baseDir != "" || pflag.CommandLine.Lookup("base-url").Changed) { - le.Printf("Cannot use --show-url/--base-dir/--base-url with this command.\n") - os.Exit(2) - } - // Command funcs exit to OS themselves for now switch cmd { case "serve-signer": @@ -125,14 +105,6 @@ func main() { remoteSign(conf, dev, verbose) - case "verify": - if baseDir != "" && (showURLOnly || pflag.CommandLine.Lookup("base-url").Changed) { - le.Printf("Cannot combine --base-dir and --show-url/--base-url\n") - os.Exit(2) - } - - verify(dev, verbose, showURLOnly, baseDir, baseURL) - case "show-pubkey": if binPath == "" { le.Printf("Needs the path to an app, use `--app PATH`\n") diff --git a/cmd/tkey-verification/remotesign.go b/cmd/tkey-verification/remotesign.go index b75d8f5..2566a52 100644 --- a/cmd/tkey-verification/remotesign.go +++ b/cmd/tkey-verification/remotesign.go @@ -4,24 +4,60 @@ package main import ( - "crypto/ed25519" - "crypto/rand" + "crypto/sha512" "crypto/tls" "fmt" "net" "net/rpc" "os" + "github.com/tillitis/tkey-verification/internal/appbins" + "github.com/tillitis/tkey-verification/internal/data" + "github.com/tillitis/tkey-verification/internal/firmware" "github.com/tillitis/tkey-verification/internal/tkey" + "github.com/tillitis/tkey-verification/internal/util" ) +type Message struct { + udi tkey.UDI + pubKey []byte + fw firmware.Firmware +} + func remoteSign(conf ProvConfig, dev Device, verbose bool) { + var firmwares firmware.Firmwares + + // Get our firmwares + firmwares.MustDecodeString(data.FirmwaresConf) + + // Find the app to use + var appHash [sha512.Size]byte + + if err := util.DecodeHex(appHash[:], conf.SigningAppHash); err != nil { + le.Printf("couldn't decode signingapphash from config") + os.Exit(1) + } + + bin, ok := appbins.MustAppBins().Bins[appHash] + if !ok { + le.Printf("Couldn't find configure app %v\n", conf.SigningAppHash) + os.Exit(1) + } + _, _, err := net.SplitHostPort(conf.ServerAddr) if err != nil { le.Printf("SplitHostPort failed: %s", err) os.Exit(1) } + // Authenticate the device + message, err := authDevice(dev, bin, firmwares, verbose) + if err != nil { + le.Printf("Couldn't authenticate device: %s\n", err) + os.Exit(1) + } + + // Sign this with our HSM server := Server{ Addr: conf.ServerAddr, TLSConfig: tls.Config{ @@ -33,13 +69,7 @@ func remoteSign(conf ProvConfig, dev Device, verbose bool) { }, } - appBin, udi, pubKey, fw, err := signChallenge(conf, dev, verbose) - if err != nil { - le.Printf("Couldn't sign challenge: %s\n", err) - os.Exit(1) - } - - err = vendorSign(&server, udi.Bytes, pubKey, fw, appBin) + err = vendorSign(&server, message.udi.Bytes, message.pubKey, message.fw, bin) if err != nil { le.Printf("Couldn't get a vendor signature: %s\n", err) os.Exit(1) @@ -48,79 +78,44 @@ func remoteSign(conf ProvConfig, dev Device, verbose bool) { le.Printf("Remote Sign was successful\n") } -// Returns the currently used device app, UDI, pubkey, expected -// firmware, and any error -func signChallenge(conf ProvConfig, dev Device, verbose bool) (AppBin, *tkey.UDI, []byte, Firmware, error) { - appBins, err := NewAppBins() - if err != nil { - fmt.Printf("Failed to init embedded device apps: %v\n", err) - os.Exit(1) - } - - // Do we have the configured device app to use for device signature? - var appBin AppBin - - if aBin, ok := appBins.Bins[conf.SigningAppHash]; ok { - appBin = aBin - } else { - fmt.Printf("Compiled in device signing app corresponding to hash %v (signingapphash) not found\n", conf.SigningAppHash) - os.Exit(1) - } - - firmwares, err := NewFirmwares() - if err != nil { - le.Printf("Found no usable firmwares\n") - os.Exit(1) - } +func authDevice(dev Device, appBin appbins.AppBin, firmwares firmware.Firmwares, verbose bool) (Message, error) { + var message Message - var fw Firmware + // Load the app tk, err := tkey.NewTKey(dev.Path, dev.Speed, verbose) if err != nil { - return appBin, nil, nil, fw, fmt.Errorf("%w", err) + return message, fmt.Errorf("%w", err) } defer tk.Close() - le.Printf("Loading device app built from %s ...\n", appBin.String()) pubKey, err := tk.LoadSigner(appBin.Bin) if err != nil { - return appBin, nil, nil, fw, fmt.Errorf("%w", err) + return message, fmt.Errorf("%w", err) } le.Printf("TKey UDI: %s\n", tk.Udi.String()) - expectfw, err := firmwares.GetFirmware(tk.Udi) + // Authenticate against pubkey + err = tk.Challenge(pubKey) if err != nil { - return appBin, nil, nil, fw, MissingError{what: "couldn't find firmware for UDI"} + return message, fmt.Errorf("challenge/response failed: %w", err) } - fw, err = verifyFirmwareHash(*expectfw, *tk) + // Verify the firmware + fw, err := verifyFirmwareHash(*tk, firmwares) if err != nil { - return appBin, nil, nil, fw, fmt.Errorf("%w", err) + return message, fmt.Errorf("%w", err) } le.Printf("TKey firmware with size:%d and verified hash:%0x…\n", fw.Size, fw.Hash[:16]) - // Locally generate a challenge and sign it - challenge := make([]byte, 32) - if _, err = rand.Read(challenge); err != nil { - return appBin, nil, nil, fw, fmt.Errorf("%w", err) - } - - signature, err := tk.Sign(challenge) - if err != nil { - return appBin, nil, nil, fw, fmt.Errorf("%w", err) - } - - fmt.Printf("signature: %x\n", signature) - - // Verify the signature against the extracted public key - if !ed25519.Verify(pubKey, challenge, signature) { - return appBin, nil, nil, fw, ErrVerificationFailed - } + message.udi = tk.Udi + message.pubKey = pubKey + message.fw = fw - return appBin, &tk.Udi, pubKey, fw, nil + return message, nil } -func vendorSign(server *Server, udi []byte, pubKey []byte, fw Firmware, appBin AppBin) error { +func vendorSign(server *Server, udi []byte, pubKey []byte, fw firmware.Firmware, appBin appbins.AppBin) error { conn, err := tls.Dial("tcp", server.Addr, &server.TLSConfig) if err != nil { return fmt.Errorf("%w", err) @@ -128,7 +123,7 @@ func vendorSign(server *Server, udi []byte, pubKey []byte, fw Firmware, appBin A client := rpc.NewClient(conn) - msg, err := buildMessage(udi, fw.Hash[:], pubKey) + msg, err := util.BuildMessage(udi, fw.Hash[:], pubKey) if err != nil { return fmt.Errorf("building message to sign failed: %w", err) } diff --git a/cmd/tkey-verification/servesigner.go b/cmd/tkey-verification/servesigner.go index 21bba3a..22a5b51 100644 --- a/cmd/tkey-verification/servesigner.go +++ b/cmd/tkey-verification/servesigner.go @@ -6,22 +6,16 @@ package main import ( "bytes" "crypto/tls" - "encoding/hex" "fmt" "net" "net/rpc" "os" + "github.com/tillitis/tkey-verification/internal/sigsum" + "github.com/tillitis/tkey-verification/internal/ssh" "github.com/tillitis/tkey-verification/internal/tkey" ) -type Verification struct { - Timestamp string `json:"timestamp"` - AppTag string `json:"apptag"` - AppHash string `json:"apphash"` - Signature string `json:"signature"` -} - func serveSigner(conf ServerConfig, dev Device, verbose bool, checkConfigOnly bool) { tlsConfig := tls.Config{ Certificates: []tls.Certificate{ @@ -38,25 +32,22 @@ func serveSigner(conf ServerConfig, dev Device, verbose bool, checkConfigOnly bo os.Exit(1) } - appBins, err := NewAppBins() - if err != nil { - fmt.Printf("Failed to init embedded device apps: %v\n", err) + var log sigsum.Log + if err = log.FromEmbedded(); err != nil { + le.Printf("Found no usable Sigsum configuration: %v\n", err) os.Exit(1) } - vendorKeys, err := NewVendorKeys(appBins) + // Do we have the configured pubkey to use for Sigsum submit key + activeKey, err := ssh.ParsePublicEd25519(conf.ActiveKey) if err != nil { - le.Printf("Found no usable embedded vendor signing public key: %v\n", err) + le.Printf("parse error in config: %v\n", err) os.Exit(1) } - // Do we have the configured pubkey to use for vendor signing? - var vendorPubKey PubKey - - if pubkey, ok := vendorKeys.Keys[conf.VendorSigningAppHash]; ok { - vendorPubKey = pubkey - } else { - fmt.Printf("Compiled in vendor key corresponding to signing app hash %v not found\n", conf.VendorSigningAppHash) + submitKey, ok := log.Keys[activeKey] + if !ok { + le.Printf("Compiled in submit key indexed by %x not found\n", activeKey) os.Exit(1) } @@ -75,15 +66,18 @@ func serveSigner(conf ServerConfig, dev Device, verbose bool, checkConfigOnly bo os.Exit(code) } - le.Printf("Vendor signing: %s\n", vendorPubKey.String()) - le.Printf("Loading device app built from %s ...\n", vendorPubKey.AppBin.String()) - foundPubKey, err := tk.LoadSigner(vendorPubKey.AppBin.Bin) + le.Printf("Sigsum signing: %s\n", submitKey.String()) + le.Printf("Loading device app built from %s ...\n", submitKey.AppBin.String()) + foundPubKey, err := tk.LoadSigner(submitKey.AppBin.Bin) if err != nil { fmt.Printf("Couldn't load device app: %v\n", err) exit(1) } - if bytes.Compare(vendorPubKey.PubKey[:], foundPubKey) != 0 { - le.Printf("The public key of the found TKey (\"%s\") does not match the embedded vendor signing public key in use\n", hex.EncodeToString(foundPubKey)) + + if !bytes.Equal(submitKey.Key[:], foundPubKey) { + le.Printf("TKey pubkey does not match active embedded pubkey\nExpected: %v\nReceived: %v\n", + ssh.FormatPublicEd25519(submitKey.Key), + ssh.FormatPublicEd25519(ssh.PublicKey(foundPubKey))) exit(1) } le.Printf("Found signing TKey with the expected public key and UDI: %s\n", tk.Udi.String()) @@ -93,7 +87,7 @@ func serveSigner(conf ServerConfig, dev Device, verbose bool, checkConfigOnly bo exit(1) } - if err = rpc.Register(NewAPI(vendorPubKey.PubKey[:], *tk)); err != nil { + if err = rpc.Register(NewAPI(submitKey.Key[:], *tk)); err != nil { le.Printf("Register failed: %s\n", err) exit(1) } diff --git a/cmd/tkey-verification/showpubkey.go b/cmd/tkey-verification/showpubkey.go index c914379..5add429 100644 --- a/cmd/tkey-verification/showpubkey.go +++ b/cmd/tkey-verification/showpubkey.go @@ -4,6 +4,7 @@ package main import ( + "crypto/ed25519" "crypto/sha512" "encoding/hex" "fmt" @@ -11,6 +12,7 @@ import ( "path/filepath" "strings" + "github.com/tillitis/tkey-verification/internal/ssh" "github.com/tillitis/tkey-verification/internal/tkey" ) @@ -42,8 +44,13 @@ func showPubkey(binPath string, dev Device, verbose bool) { tag := strings.TrimSuffix(filepath.Base(binPath), ".bin") - le.Printf("Public Key, app tag, and app hash for vendor-signing-pubkeys.txt follows on stdout:\n") + le.Printf("Public Key, app tag, and app hash for embedded vendor pubkeys follows on stdout:\n") fmt.Printf("%s %s %s\n", hex.EncodeToString(pubKey), tag, hex.EncodeToString(appHash[:])) + var sshPubKey [ed25519.PublicKeySize]byte + + copy(sshPubKey[:], pubKey) + fmt.Printf("SSH version: %v\n", ssh.FormatPublicEd25519(sshPubKey)) + exit(0) } diff --git a/cmd/tkey-verification/util.go b/cmd/tkey-verification/util.go index f33673f..48346f5 100644 --- a/cmd/tkey-verification/util.go +++ b/cmd/tkey-verification/util.go @@ -6,41 +6,35 @@ package main import ( "fmt" "os" - "runtime/debug" "strings" "github.com/spf13/pflag" + "github.com/tillitis/tkey-verification/internal/appbins" + "github.com/tillitis/tkey-verification/internal/firmware" + "github.com/tillitis/tkey-verification/internal/sigsum" + "github.com/tillitis/tkey-verification/internal/vendorkey" ) -func readBuildInfo() string { - version := "devel without BuildInfo" - if info, ok := debug.ReadBuildInfo(); ok { - sb := strings.Builder{} - sb.WriteString("devel") - for _, setting := range info.Settings { - if strings.HasPrefix(setting.Key, "vcs") { - sb.WriteString(fmt.Sprintf(" %s=%s", setting.Key, setting.Value)) - } - } - version = sb.String() - } - return version -} - func builtWith() { - appBins, err := NewAppBins() + appBins, err := appbins.NewAppBins() if err != nil { fmt.Printf("Failed to init embedded device apps: %v\n", err) os.Exit(1) } - vendorKeys, err := NewVendorKeys(appBins) - if err != nil { + var vendorKeys vendorkey.VendorKeys + if err = vendorKeys.FromEmbedded(appBins); err != nil { le.Printf("Found no usable embedded vendor signing public key\n") os.Exit(1) } - firmwares, err := NewFirmwares() + var log sigsum.Log + if err = log.FromEmbedded(); err != nil { + le.Printf("Sigsum configuration missing") + os.Exit(1) + } + + firmwares, err := firmware.NewFirmwares() if err != nil { le.Printf("Found no usable firmwares\n") os.Exit(1) @@ -51,11 +45,15 @@ Supported verisigner-app tags: %s Known vendor signing keys: %s + +Known Sigsum configuration: +%s Known firmwares: %s `, strings.Join(appBins.Tags(), " \n "), vendorKeys.String(), + log.String(), strings.Join(firmwares.List(), " \n ")) } @@ -69,16 +67,7 @@ Commands: remote-sign Call the remote signing server to sign for a local TKey. - verify Verify that a TKey is genuine by extracting the TKey UDI and using it - to fetch the verification data, including tag and signature from the - web. Then running the correct verisigner-app on the TKey, extracting - the public key and verifying it using the vendor's signing public key. - - The flags --show-url and --base-dir can be used to show the URL for - downloading the verification data on one machine, and verifying the - TKey on another machine that lacks network, see more below. - - show-pubkey Prints the info needed for the vendor-signing-pubkeys.txt to stdout. + show-pubkey Prints the info needed for the embedded vendor pubkeys to stdout. This includes public key, app tag, and app hash in the right format. Use the flag --app to specify the path o the desired app to use, i.e., diff --git a/cmd/tkey-verification/vendor-signing-pubkeys.txt b/cmd/tkey-verification/vendor-signing-pubkeys.txt deleted file mode 100644 index 22482a7..0000000 --- a/cmd/tkey-verification/vendor-signing-pubkeys.txt +++ /dev/null @@ -1 +0,0 @@ -14274d3570097aea209af1c23b64aa439a4d0d32c62735c5f6d6a29600c9a275 verisigner-v0.0.3 f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 diff --git a/cmd/tkey-verification/verify.go b/cmd/tkey-verification/verify.go deleted file mode 100644 index 153af8e..0000000 --- a/cmd/tkey-verification/verify.go +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Tillitis AB -// SPDX-License-Identifier: BSD-2-Clause - -package main - -import ( - "crypto/ed25519" - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path" - "time" - - "github.com/tillitis/tkey-verification/internal/tkey" -) - -const verifyInfoURL = "https://www.tillitis.se/verify" - -func verify(dev Device, verbose bool, showURLOnly bool, baseDir string, verifyBaseURL string) { - appBins, err := NewAppBins() - if err != nil { - missing(fmt.Sprintf("no embedded device apps: %v", err)) - os.Exit(1) - } - - vendorKeys, err := NewVendorKeys(appBins) - if err != nil { - missing(fmt.Sprintf("no vendor signing public key: %v", err)) - os.Exit(1) - } - - firmwares, err := NewFirmwares() - if err != nil { - missing("no firmware digests") - os.Exit(1) - } - - tk, err := tkey.NewTKey(dev.Path, dev.Speed, verbose) - if err != nil { - commFailed(err.Error()) - os.Exit(1) - } - - exit := func(code int) { - tk.Close() - os.Exit(code) - } - - le.Printf("TKey UDI: %s\n", tk.Udi.String()) - - verifyURL := fmt.Sprintf("%s/%s", verifyBaseURL, hex.EncodeToString(tk.Udi.Bytes)) - - if showURLOnly { - le.Printf("URL to verification data follows on stdout:\n") - fmt.Printf("%s\n", verifyURL) - exit(0) - } - - var verification Verification - - if baseDir != "" { - p := path.Join(baseDir, hex.EncodeToString(tk.Udi.Bytes)) - verification, err = verificationFromFile(p) - if err != nil { - commFailed(err.Error()) - exit(1) - } - } else { - if verbose { - le.Printf("Fetching verification data from %s ...\n", verifyURL) - } - - verification, err = verificationFromURL(verifyURL) - if err != nil { - commFailed(err.Error()) - exit(1) - } - } - - if verbose { - le.Printf("Verification data was created %s\n", verification.Timestamp) - } - - if verification.AppTag == "" { - parseFailure("app tag empty") - exit(1) - } - - _, err = hex.DecodeString(verification.AppHash) - if err != nil { - parseFailure("hex decode error") - exit(1) - } - - appBin, err := appBins.Get(verification.AppHash) - if err != nil { - notFound("upstream app digest unknown") - exit(1) - } - - pubKey, err := tk.LoadSigner(appBin.Bin) - if err != nil { - commFailed(err.Error()) - exit(1) - } - - expectedFw, err := firmwares.GetFirmware(tk.Udi) - if err != nil { - notFound("no known firmware for UDI") - exit(1) - } - - fw, err := verifyFirmwareHash(*expectedFw, *tk) - if err != nil { - verificationFailed("unexpected firmware") - exit(1) - } - if verbose { - le.Printf("TKey firmware was verified, size:%d hash:%0x…\n", fw.Size, fw.Hash[:16]) - } - - vSignature, err := hex.DecodeString(verification.Signature) - if err != nil { - parseFailure(err.Error()) - exit(1) - } - - // Verify vendor's signature over known message. - msg, err := buildMessage(tk.Udi.Bytes, fw.Hash[:], pubKey) - if err != nil { - parseFailure(err.Error()) - exit(1) - } - - // We allow for any of the known vendor keys. - var verified = false - - for _, vendorPubKey := range vendorKeys.Keys { - if ed25519.Verify(vendorPubKey.PubKey[:], msg, vSignature) { - le.Printf("Verified with vendor key %x\n", vendorPubKey.PubKey) - verified = true - break - } - } - - if !verified { - verificationFailed("vendor signature not verified") - exit(1) - } - - // Get a device signature over a random challenge - challenge := make([]byte, 32) - if _, err = rand.Read(challenge); err != nil { - le.Printf("rand.Read failed: %s", err) - exit(1) - } - - signature, err := tk.Sign(challenge) - if err != nil { - commFailed(err.Error()) - exit(1) - } - - // Verify device signature against device public key - if !ed25519.Verify(pubKey, challenge, signature) { - verificationFailed("challenge not verified") - exit(1) - } - - fmt.Printf("TKey is genuine!\n") - - exit(0) -} - -// commFailed describes an I/O failure of some kind, perhaps between -// the client and the TKey, an HTTP request that didn't succeed, or -// perhaps reading a file. -func commFailed(msg string) { - fmt.Printf("I/O FAILED: %s\n", msg) -} - -// parseFailure describes an error where we have tried to parse -// something from external sources but failed. -func parseFailure(msg string) { - fmt.Printf("PARSE ERROR: %s\n", msg) -} - -// missing describes an error where something is missing from the -// binary to even complete a verification. -func missing(msg string) { - fmt.Printf("MISSING IN PROGRAM: %s\n", msg) - fmt.Printf("It seems tkey-verification is not built correctly.\n") -} - -// notFound describes an error where we with data from external source -// can't find something, perhaps not finding something on a web -// server, or not finding the device app digest. -func notFound(msg string) { - fmt.Printf("NOT FOUND: %s\n", msg) -} - -// verificationFailed describes a real problem with a manipulated -// TKey. -func verificationFailed(msg string) { - fmt.Printf("VERIFICATION FAILED: %s\n", msg) - fmt.Printf("Please visit %s to understand what this might mean.\n", verifyInfoURL) -} - -func verificationFromURL(verifyURL string) (Verification, error) { - var verification Verification - - client := http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(verifyURL) // #nosec G107 - if err != nil { - return verification, IOError{path: verifyURL, err: err} - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - le.Printf("HTTP GET status code: %d (%s)", resp.StatusCode, resp.Status) - return verification, ErrIO - } - - verificationJSON, err := io.ReadAll(resp.Body) - if err != nil { - return verification, IOError{path: verifyURL, err: err} - } - - if err = json.Unmarshal(verificationJSON, &verification); err != nil { - return verification, ParseError{what: "JSON Unmarshal", err: err} - } - - return verification, nil -} - -func verificationFromFile(fn string) (Verification, error) { - var verification Verification - - le.Printf("Reading verification data from file %s ...\n", fn) - verificationJSON, err := os.ReadFile(fn) - if err != nil { - return verification, IOError{path: fn, err: err} - } - - if err = json.Unmarshal(verificationJSON, &verification); err != nil { - return verification, ParseError{what: "JSON unmarshal", err: err} - } - - return verification, nil -} diff --git a/cmd/tkey-verify/main.go b/cmd/tkey-verify/main.go new file mode 100644 index 0000000..90fb930 --- /dev/null +++ b/cmd/tkey-verify/main.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package main + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/pflag" + "github.com/tillitis/tkey-verification/internal/util" + "github.com/tillitis/tkeyclient" +) + +const ( + progname = "tkey-verify" + defaultBaseURL = "https://tkey.tillitis.se/verify" +) + +var version string + +// Use when printing err/diag msgs +var le = log.New(os.Stderr, "", 0) + +type Device struct { + Path string + Speed int +} + +func main() { + var dev Device + var baseURL, baseDir string + var sigsum, verbose, showURLOnly, versionOnly, helpOnly bool + + pflag.CommandLine.SetOutput(os.Stderr) + pflag.CommandLine.SortFlags = false + pflag.StringVar(&dev.Path, "port", "", + "Set serial port device `PATH`. If this is not passed, auto-detection will be attempted.") + pflag.IntVarP(&dev.Speed, "speed", "s", tkeyclient.SerialSpeed, + "Set serial port `SPEED` in bits per second.") + pflag.BoolVar(&verbose, "verbose", false, + "Enable verbose output.") + pflag.BoolVarP(&showURLOnly, "show-url", "u", false, + "Only output the URL to the verification data that should be downloaded.") + pflag.StringVarP(&baseDir, "base-dir", "d", "", + "Read verification data from a file located in `DIRECTORY` and named after the TKey UDI in hex, instead of from a URL. You can for example first use \"verify --show-url\" and download the verification file manually on some other computer, then transfer the file back and use \"verify --base-dir .\".") + pflag.StringVar(&baseURL, "base-url", defaultBaseURL, + "Set the base `URL` of verification server for fetching verification data.") + pflag.BoolVar(&sigsum, "sigsum", false, + "Demand a Sigsum proof in the verification file.") + pflag.BoolVar(&versionOnly, "version", false, "Output version information.") + pflag.BoolVar(&helpOnly, "help", false, "Output this help.") + pflag.Usage = usage + pflag.Parse() + + if helpOnly { + pflag.Usage() + os.Exit(0) + } + + if versionOnly { + fmt.Printf("%s %s\n", progname, util.Version(version)) + os.Exit(0) + } + + if baseDir != "" && (showURLOnly || pflag.CommandLine.Lookup("base-url").Changed) { + le.Printf("Cannot combine --base-dir and --show-url/--base-url\n") + os.Exit(2) + } + + if showURLOnly { + verifyShowURL(dev, baseURL) + } + + verify(dev, verbose, baseDir, baseURL, sigsum) +} + +func usage() { + desc := fmt.Sprintf(`Usage: %s [flags...] + +Verify that a TKey is genuine by extracting the TKey UDI and using it +to fetch the verification data, including tag and signature from the +web. Then running the correct verisigner-app on the TKey, extracting +the public key and verifying it using the vendor's signing public key. + +The flags --show-url and --base-dir can be used to show the URL for +downloading the verification data on one machine, and verifying the +TKey on another machine that lacks network, see more below. +`, progname) + + le.Printf("%s\n\nFlags:\n%s\n", desc, pflag.CommandLine.FlagUsagesWrapped(86)) +} diff --git a/cmd/tkey-verify/verify.go b/cmd/tkey-verify/verify.go new file mode 100644 index 0000000..04bcc98 --- /dev/null +++ b/cmd/tkey-verify/verify.go @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: 2022 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package main + +import ( + "bytes" + "encoding/hex" + "fmt" + "os" + "path" + + "github.com/tillitis/tkey-verification/internal/appbins" + "github.com/tillitis/tkey-verification/internal/data" + "github.com/tillitis/tkey-verification/internal/firmware" + "github.com/tillitis/tkey-verification/internal/sigsum" + "github.com/tillitis/tkey-verification/internal/ssh" + "github.com/tillitis/tkey-verification/internal/tkey" + "github.com/tillitis/tkey-verification/internal/util" + "github.com/tillitis/tkey-verification/internal/vendorkey" + "github.com/tillitis/tkey-verification/internal/verification" + "github.com/tillitis/tkeyclient" +) + +const verifyInfoURL = "https://www.tillitis.se/verify" +const vendorID = 0x1337 + +func verifyShowURL(dev Device, verifyBaseURL string) { + // Connect to a TKey + tk, err := tkey.NewTKey(dev.Path, dev.Speed, false) + if err != nil { + commFailed(err.Error()) + os.Exit(1) + } + + exit := func(code int) { + tk.Close() + os.Exit(code) + } + + le.Printf("TKey UDI: %s\n", tk.Udi.String()) + + verifyURL := fmt.Sprintf("%s/%s", verifyBaseURL, hex.EncodeToString(tk.Udi.Bytes)) + + le.Printf("URL to verification data follows on stdout:\n") + fmt.Printf("%s\n", verifyURL) + exit(0) +} + +// verify verifies a Tkey by: +// +// - Connecting to the device, retrieving the UDI +// +// - Fetching the verification file, indexed by the UDI. +// +// - Load the signer indicated in the verification file, which +// returns the public key. +// +// - Doing a challenge/response to prove the signer's identity +// against the public key we just got. +// +// - Verify that the device has the expected firmware. +// +// - Recreates the vendor signed message. +// +// - Verify the vendor signature over the message. +func verify(dev Device, verbose bool, baseDir string, verifyBaseURL string, useSigsum bool) { + var firmwares firmware.Firmwares + + firmwares.MustDecodeString(data.FirmwaresConf) + + appBins, err := appbins.NewAppBins() + if err != nil { + missing(fmt.Sprintf("no embedded device apps: %v", err)) + os.Exit(1) + } + + var vendorKeys vendorkey.VendorKeys + if err = vendorKeys.FromEmbedded(appBins); err != nil { + missing(fmt.Sprintf("no vendor signing public key: %v", err)) + os.Exit(1) + } + + var log sigsum.Log + if err = log.FromEmbedded(); err != nil { + missing("Sigsum configuration missing") + os.Exit(1) + } + + // Connect to a TKey + tk, err := tkey.NewTKey(dev.Path, dev.Speed, verbose) + if err != nil { + commFailed(err.Error()) + os.Exit(1) + } + + exit := func(code int) { + tk.Close() + os.Exit(code) + } + + le.Printf("TKey UDI: %s\n", tk.Udi.String()) + + if tk.Udi.VendorID != vendorID { + le.Printf("Unknown Vendor ID") + exit(1) + } + + // Castor means we demand a Sigsum proof. Bellatrix means + // vendor signature. Unless sigsum is forced. + if !useSigsum { + switch tk.Udi.ProductID { + case tkeyclient.UDIPIDBellatrix: + useSigsum = false + case tkeyclient.UDIPIDCastor: + useSigsum = true + default: + // Not sure! + le.Printf("Unknown Product ID: Don't know if we need signature or Sigsum proof.") + exit(1) + } + } + + var verification verification.Verification + + if baseDir != "" { + p := path.Join(baseDir, hex.EncodeToString(tk.Udi.Bytes)) + if err = verification.FromFile(p); err != nil { + commFailed(err.Error()) + exit(1) + } + } else { + // Verify from an URL + verifyURL := fmt.Sprintf("%s/%s", verifyBaseURL, hex.EncodeToString(tk.Udi.Bytes)) + + if verbose { + le.Printf("Fetching verification data from %s ...\n", verifyURL) + } + + if err = verification.FromURL(verifyURL); err != nil { + commFailed(err.Error()) + exit(1) + } + } + + if verbose { + le.Printf("Verification data was created %s\n", verification.Timestamp) + } + + // Find the right app to run + appBin, ok := appBins.Bins[verification.AppHash] + if !ok { + notFound("app digest") + exit(1) + } + + pubKey, err := tk.LoadSigner(appBin.Bin) + if err != nil { + commFailed(err.Error()) + exit(1) + } + + // Check device identity + if err = tk.Challenge(pubKey); err != nil { + verificationFailed("challenge/response failed") + exit(1) + } + + // Check we have the right firmware. + expectedFW, err := firmwares.GetFirmware(tk.Udi) + if err != nil { + verificationFailed("unexpected firmware") + exit(1) + } + + fwHash, err := tk.GetFirmwareHash(expectedFW.Size) + if err != nil { + commFailed("couldn't get firmware digest from TKey") + exit(1) + } + + if !bytes.Equal(expectedFW.Hash[:], fwHash) { + le.Printf("TKey does not have expected firmware hash %0x…, but instead %0x…", expectedFW.Hash[:16], fwHash[:16]) + verificationFailed("unexpected firmware") + exit(1) + } + + if verbose { + le.Printf("TKey firmware was verified, size:%d hash:%0x…\n", expectedFW.Size, expectedFW.Hash[:16]) + } + + // Recreate message the vendor signed + msg, err := util.BuildMessage(tk.Udi.Bytes, expectedFW.Hash[:], pubKey) + if err != nil { + parseFailure(err.Error()) + exit(1) + } + + // Verify the vendor signature or Sigsum proof over the + // recreated message. + if verification.IsProof() { + if useSigsum { + submitKey, err := verification.VerifyProof(msg, log) + if err != nil { + verificationFailed(err.Error()) + exit(1) + } + + le.Printf("Verified Sigsum proof. Submit key: %s\n", ssh.FormatPublicEd25519(submitKey.Key)) + } else { + // Strange. Exit. + verificationFailed("Expected vendor signature but got a Sigsum proof") + exit(1) + } + } else { + if useSigsum { + // Strange. Exit. + verificationFailed("Sigsum proof required but not available") + exit(1) + } + + verifiedWith, err := verification.VerifySig(msg, vendorKeys) + if err != nil { + verificationFailed(err.Error()) + exit(1) + } + + le.Printf("Verified with vendor key %x\n", verifiedWith.PubKey) + } + + fmt.Printf("TKey is genuine!\n") + + exit(0) +} + +// commFailed describes an I/O failure of some kind, perhaps between +// the client and the TKey, an HTTP request that didn't succeed, or +// perhaps reading a file. +func commFailed(msg string) { + fmt.Printf("I/O FAILED: %s\n", msg) +} + +// parseFailure describes an error where we have tried to parse +// something from external sources but failed. +func parseFailure(msg string) { + fmt.Printf("PARSE ERROR: %s\n", msg) +} + +// missing describes an error where something is missing from the +// binary to even complete a verification. +func missing(msg string) { + fmt.Printf("MISSING IN PROGRAM: %s\n", msg) + fmt.Printf("It seems tkey-verify is not built correctly.\n") +} + +// notFound describes an error where we with data from external source +// can't find something, perhaps not finding something on a web +// server, or not finding the device app digest. +func notFound(msg string) { + fmt.Printf("NOT FOUND: %s\n", msg) +} + +// verificationFailed describes a real problem with a manipulated +// TKey. +func verificationFailed(msg string) { + fmt.Printf("VERIFICATION FAILED: %s\n", msg) + fmt.Printf("Please visit %s to understand what this might mean.\n", verifyInfoURL) +} diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 0000000..fcdc412 --- /dev/null +++ b/doc/design.md @@ -0,0 +1,314 @@ +# Design of the TKey verification process + +## Introduction + +Think of a TKey identity as a message made up of: + +- Unique Device Identifier (UDI) +- the public key of a signing device app, typically + [signer](https://github.com/tillitis/tkey-device-signer), running on + the TKey +- digest of the firmware of this TKey + +This identity is what we want to prove is the same to the end user. We +do this by signing a digest of the identity, logging the digest into a +transparency log, and publishing a [verification +file](#verification-file) about it. + +The user can later download the verification file, recreate the +message, and verify that we signed a digest of it with the help of the +included Sigsum proof. + +## Sigsum transparency log + +We submit signed checksums of all identities in [a Sigsum transparency +log](https://sigsum.org/). We want to: + +- Be able to monitor when our private key is used and signal to us if + it's leaked. +- Increase trust in the identities by having our signatures logged and + witnessed. +- Control that the submission of our signature was done at a time when + our submit key was valid. + +We don't store or publish the TKey identities, so we can't actually +verify the identities ourselves. See "Why is the TKey identity not +published?" below. + +We plan to run a Sigsum monitor tailing the log to see when our key is +used. To make it easier to find illicit use of the key we will store +the *digest* of the TKey identity, but not the identity itself. + +If the monitor finds our key has been used, it uses the digest +reported from the log and checks if this is indeed a known digest we +have signed. If not, it alerts us. + +The timestamps by the witnesses should give us a hint when this was +first used. We can compare with the timestamps in our verification +files to see the last good verification file. + +## Provisioning + +Done by the vendor, maybe during provisioning of the FPGA bitstream, +but not necessarily. + +- Create and [authenticate](#authenticate) a TKey identity, sign a + hash digest of the identity and submit the signed digest to a Sigsum + log. + +- Publish the Sigsum proof and some metadata in a [verification + file](#verification-file). Publish this on a web server indexed by + the UDI, typically: https://tkey.tillitis.se/verify/UDI-in-hex + +## Verification + +Done by end user. + +- Recreate and [authenticate](#authenticate) the TKey identity by + loading the same app on the same TKey and doing a + challenge/response. + +- Verify that the TKey identity has been signed with a Sigsum proof. + +All data needed to recreate and verify the identity is provided in the +[verification file](#verification-file). + +## Verification file + +The verification file contains: + +- `timestamp`: RFC3339 timestamp when the signature was done. +- `apptag`: a human readable hint for anyone who wants to reproduce the + procedure manually. +- `apphash`: hash digest for the specific device app to run. +- `proof`: Sigsum proof that this TKey identity is signed and logged. + +In older versions of the verification file there is a vendor signature +instead of `proof`: + +- `signature`: Ed25519 vendor's signature. + +For compatibility with older TKeys we continue to support being able +to verify the vendor's signature. We identify what kind (proof or +signature) we need to use by the product ID of the TKey. Older TKeys: +require a signature, newer: require a Sigsum proof. + +The canonical URL for this file in a TKey provisioned by Tillitis is: + +https://tkey.tillitis.se/verify/UDI-in-hex + +like: + +https://tkey.tillitis.se/verify/0133704100000015 + +## Submit request file + +The submit request file contains: + +- `timestamp`: RFC3339 timestamp when the signature was done. +- `apptag`: a human readable hint for anyone who wants to reproduce the + procedure manually. +- `apphash`: hash digest for the specific device app to run. +- `request`: sigsum add-leaf-request data. + +## Authenticate + +Both during provisioning and verification we need to authenticate the +TKey identity, to see that the combination of device app and hardware +is the expected. + +- Load the signer device app. + +- Retrieve its public key. + +- Do a challenge/response, asking the running app to make a signature + over some random data. + +- Verify the signature with the already retrieved public key. + +## What is verified? + +What does verifying a TKey with `tkey-verify` prove? + +To explain the verification and what it proves, first we need a brief +explanation on how the TKey works. The TKey uses measured boot. The +measured boot guarantees that every app that starts on the TKey gets a +unique identity called the Compound Device Identity (CDI). + +The CDI is a combination of: + +- a per-device unique secret embedded in the hardware, the Unique + Device Secret (UDS), + +- the integrity of the software before it is started, as measured by + immutable and therefore trusted firmware, and, + +- an optional User Supplied Secret (not used in this case). + +The CDI is computed like this: + +``` +CDI = blake2s(UDS, blake2s(application), USS) +``` + +where `blake2s` is the hash function BLAKE2s from [RFC +7693](https://www.rfc-editor.org/info/rfc7693). + +There are two parts to verifying a TKey. First what the vendor does +during provisioning, then what the user does during verification: + +- Provisioning: The vendor signs a message containing the Unique + Device Identifier (UDI), a digest over the observed firmware, and + the signer's public key. + + The message itself is not published, but the signature and some + metadata is (see [Verification file](#verification-file) for + details). The message can later be recreated by the verification + process. + +- Verification: `tkey-verify` first recreates the message (UDI, + firmware digest, signer's public key), checks the vendor's signature + over the message, and finally does a challenge/response to prove + that the device has the corresponding private key. + +**Proven**: It's now proven that the currently used TKey device, +running this device app is the same as the TKey device (or at least a +device with the exact same UDS) that was running the same device app +during provisioning. + +Less strongly shown: + +- The firmware check assumes that a hash digest over the part of + memory where the firmware is supposed to be suffices. In a + manipulated TKey the real firmware might be somewhere else, with a + copy of the expected firmware in the right place in the memory map. +- The authenticity of the RISC-V softcore isn't proven, but it was at + least able to run the device app successfully. +- The rest of the FPGA design isn't proven either, except the UDS, but + at least it worked as expected with the loaded signer app. + +Not proved at all: + +- USB controller CH552 firmware and hardware. +- PCB design. + +## Why is the TKey identity not published? + +The TKey identity that is signed by the vendor's key is not published. +It is, instead, recreated when verifying a TKey. This makes it +impossible for anyone else, including the vendor, to verify the +message if they haven't stored the message somewhere else. + +When originally designing this system we were afraid that publishing, +or even keeping, the signer public key in a way that ties it very +strongly to a certain UDI would be bad for the user. + +Since we're currently using the ordinary +[signer](https://github.com/tillitis/tkey-device-signer) device app +(although compiled without touch requirement) publishing the signer +public key from this specific TKey would be equivalent of publishing +the user's public SSH key (if they choose not to use an USS and use +the signer without touch requirement) to the entire world. We were not +comfortable in doing that even if it would, in a way, be a way of +doing hardware attestation of the TKey. We might need to revisit this. + +Note that we always recommend using an USS for all your own use of the +TKey! We also make it slightly hard to use a signer without touch +requirement for security reasons, but we see the usefulness of +providing it as an option for server use. + +## Weaknesses + +- The entire device is not proven. +- The distribution of the vendor's public key is sensitive. Since + all trust is placed in the vendor's signature, all fails if the + end user is tricked to use the wrong public key. It's right now + embedded in tkey-verification. +- The distribution of the tkey-verification client app is sensitive, + since if it is malicious it can just say "TKey is genuine!" without + actually doing much else. + + However, the build of tkey-verification is reproducible if using + pinned versions of tools (but currently not the macOS binary). The + same verification can also be done independently by other tools. + +## Security Protocol + +Detailed step-by-step security protocol. + +### Terminology + +- "device under verification": The device the vendor is provisioning + or the user is verifying. +- "device signature": A signature made on the device under + verification with the signer device app. +- Unique Device Identifier (UDI): A unique identifier present in all + TKeys. The 1st half identifies the revision of the hardware, the 2nd + half is a serial number. +- "signing server": An HSM-like machine providing signatures over + messages and producing files to be uploaded to some database. +- "signer": A device app used for confirming the TKey's identity, + right now either verisigner (source in older versions in this + repository, look for verisigner tags) or + [tkey-device-signer](https://github.com/tillitis/tkey-device-signer). +- "signer public key": The public key of signer running on the device + under verification. +- "vendor's public key": The public key of the vendor, typically + Tillitis or an IT department, corresponding to a private key in a + TKey in the signing server. +- "vendor's Sigsum private key": The private key the vendor uses to + sign a digest for logging in the Sigsum transparency log. +- "vendor's Sigsum public key": The public key corresponding to the + vendor's Sigsum private key. +- "vendor's signature": A signature made by the signing server. + +### During provisioning + +1. Retrieve the UDI from the device under verification. +2. Run the signer with a specific tag on the device under + verification. This creates a unique key pair. +3. Retrieve signer public key from the device under verification. +4. Ask signer to sign a random challenge. +5. Verify the signature of the random challenge with the retrieved + public key to let the device prove that it has the corresponding + private key. +6. Ask signer for a digest of the firmware binary (in ROM). Consult + the internal firmware database to verify that the TKey, according + to its hardware revision, is running the expected firmware. +7. Sign a digest of a message consisting of the UDI, firmware digest, + and signer public key with vendor's private Sigsum key, creating a + Sigsum log [request file](#submit-request-file). +8. Transfer the request file to the Sigsum monitor. +9. Submit the sigsum request included in the request file to the + Sigsum log, collecting the proof. +10. Build the verification file, including the Sigsum proof. +11. Publish the [verification file](#verification-file), indexed by + the UDI. + +![Data flow during provisioning](../signing-procedure.svg) + +### Verifying + +1. Retrieve the UDI from the device under verification. +2. Get the [verification file](#verification-file) for this UDI. +3. Run the signer with the same tag and digest on the device under + verification. +4. Retrieve the signer public key. +5. Ask signer to sign a random challenge. +6. Verify the signature of the random challenge with the signer public + key thus proving that the device under verification has access to the + corresponding private key. +7. Ask signer for a digest of the firmware binary (in ROM). Consult + the internal firmware database to verify that the TKey, according + to its hardware revision, is running the expected firmware. +8. Recreate the message of UDI, firmware digest and signer public key. +9. Verify the Sigsum proof of the message, thus proving that the + UDI, the firmware, and this private/public key pair was the same + during vendor's signing. + +Note that the exact same signer binary that was used for producing the +signer signature during provisioning *must* be used when verifying it. +If a different signer is used then the device private/public key will +not match, even if the TKey is the same. A verifier must check the +"hash" field and complain if it does not have a signer binary with the +same digest. diff --git a/doc/implementation-notes.md b/doc/implementation-notes.md deleted file mode 100644 index 4d4c366..0000000 --- a/doc/implementation-notes.md +++ /dev/null @@ -1,152 +0,0 @@ -# Implementation notes - -## Assets - -There are some compiled-in assets: - -- application binaries: The device apps used for both vendor signing - and device signatures. -- vendor public keys: The public key extracted from a vendor TKey - during vendor signing and used for verification. Might be several, - including historical keys no longer being used for vendor signing. -- known firmwares: Expected firmwares for all provisioned TKeys. - -Commands are responsible for initializing their own assets. For -instance, `verify` needs all three, but `remote-sign` and -`serve-signer` need only two, and `show-pubkey` needs none. - -### Application binaries - -Initialized by calling `appbins.go:NewAppBins()`. - -Actual binaries are located in the `bins` directory stored like this: - -- `name-tag`, like `signer-v1.0.1.bin`: the actual device app binary. -- `name-tag.sha512`, like `signer-v1.0.1.bin.sha512`: the SHA-512 - digest of the above file to help ensure we don't make mistakes. - -### Vendor public keys - -Initialized by calling `vendorpubkeys.go:NewVendorKeys()`. Needs to -know the appbins (see above). - -The actual vendor keys are defined in the text file -`vendor-signing-pubkeys.txt`, which is embedded in the binary at build -time and parsed at start. It contains: - -- the public key -- the name and tag of the device app to use for vendor signing. -- the hash digest of that device app. - -Example content: - -``` -50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10 verisigner-v0.0.3 f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 -``` - -In use, it's initialized like this: - -```go -appBins, err := NewAppBins() -vendorKeys, err := NewVendorKeys() -``` - -### Known firmwares - -Initialized by calling `firmwares.go:NewFirmwares()`. - -It's used by probing a TKey for UDI, then looking up a firmware for a -specific UDI with `GetFirmware(udi)`. Now you know the size and the -SHA-512 digest of the expected firmware and call the device app to ask -it for a digest. - -## `serve-signer` command - -Defined in `servesigner.go`. - -- Initialises its assets using `NewAppBins()` and `NewVendorKeys()`. - -- Connects to a vendor TKey, loads the built-in vendor signing device - app indicated by the configuration in `vendorapphash`. - -- Sets up a HTTPS server exposing a simple API through "net/rpc". The - RPC commands are in `api.go`. - -- Waits for commands. - -- When a `Sign` command appears it requests the device app to sign the - message and returns an OK or error. It writes the signature over the - message and some metadata under the signatures directory. Note that - it doesn't store the message itself. A verifier needs to recreate - the message themselves from their TKey. - -- Goes back to wait for more commands. - -## `remote-sign` command - -Defined in `remotesign.go`. - -- Sets up its assets using `NewAppBins()` and `NewFirmwares()`. - -- Connects to a TKey during provisioning and loads built-in device app - indicated by the configuration in `signingapphash`. - -- Extracts the public key. - -- Verifies that firmware digest is as expected. - -- Verifies that the TKey can sign a random challenge, proving that it - has the corresponding private key. - -- Builds a message to be signed. - -- Asks the server (same program but started with `serve-signer`) to - sign the message. - -## `verify` command - -Defined in `verify.go`. - -- Initialises its assets by using `NewAppBins()`, `NewVendorKeys()`, - and `NewFirmwares()`. - -- Connects to a TKey, request UDI. - -- If this is `verify --show-url` we just construct the URL from the - UDI, outputs that and exits. - -- Otherwise, we fetch the verification data from something like - `https://tkey.tillitis.se/verify/0133708100000002`. - -- Look up the necessary device app to load from our embedded assets - and load it. - -- Extract the public key. - -- Look up the expected firmware hash digest from the UDI. - -- Compare the firmware digest of the TKey compared to the expected. - -- Recreate the message seen during provisioning. - -- Verify vendor's signature over the message against any of the - vendor's known public keys. - -- Sign and verify a random challenge, proving that the TKey knows the - corresponding private key. - -## `show-pubkey` command - -Defined in `showpubkey.go`. - -- Connects to a TKey. - -- Reads the device app file, on the path passed to it in `--app`. - -- Computes the SHA-512 digest of the file. - -- Loads the app device. - -- Extracts the public key. - -- Prints the public key, the name-tag, and the app digest. diff --git a/doc/tkey-sigsum-submit.1 b/doc/tkey-sigsum-submit.1 new file mode 100644 index 0000000..34a131d --- /dev/null +++ b/doc/tkey-sigsum-submit.1 @@ -0,0 +1,75 @@ +.\" Generated by scdoc 1.11.3 +.\" Complete documentation for this program is not available as a GNU info page +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.nh +.ad l +.\" Begin generated content: +.TH "tkey-sigsum-submit" "1" "2025-09-10" +.PP +.SH NAME +.PP +tkey-sigsum-submit - Process TKey submission files by submitting +requests to a Sigsum log.\& +.PP +.SH SYNOPSIS +.PP +\fBtkey-sigsum-submit\fR -h/--help +.PP +\fBtkey-sigsum-submit\fR -m directory -n directory -d directory +.PP +.SH DESCRIPTION +.PP +\fBtkey-sigsum-submit\fR processes TKey submissions files and generates +TKey verification files that can be used to verify a TKey identity.\& +.PP +Each submission file contains a Sigsum request.\& The request is used to +submit to a Sigsum log.\& The result is a verification file.\& +.PP +.SH OPTIONS +.PP +\fB-m\fR | \fB--submissions-dir\fR directory +.PP +.RS 4 +Set directory to read submission files from.\& +.PP +.RE +\fB-n\fR | \fB--processed-submissions-dir\fR directory +.PP +.RS 4 +Set directory to move submitted files to.\& +.PP +.RE +\fB-d\fR | \fB--verifications-dir\fR directory +.PP +.RS 4 +Set directory to write generated verification files in.\& +.PP +.RE +.SH EXAMPLES +.PP +.nf +.RS 4 +$ tkey-sigsum-submit -m signatures -n processed -d verifications +2025/09/10 10:23:23 [INFO] Attempting to submit checksum#1 to log: https://test\&.sigsum\&.org/barreleye +2025/09/10 10:23:23 [INFO] Attempting to retrieve proof for checksum#1 +.fi +.RE +.PP +.SH FILES +.PP +The files in the submission directory (set with \fB-m\fR) are produced by +\fBtkey-verification\fR(1) running the \fBserve-sign\fR command.\& +.PP +After a successful run of \fBtkey-sigsum-submit\fR the submitted submission +files are moved from the submissions directory to the processed +submissions directory (\fB-n\fR).\& The generated verification files are +placed in the verification directory (\fB-d\fR).\& +.PP +.SH SEE ALSO +.PP +\fBtkey-verification\fR(1) \fBtkey-verify\fR(1) +.PP +.SH AUTHORS +.PP +Tillitis AB, https://tillitis.\&se/ diff --git a/doc/tkey-sigsum-submit.scd b/doc/tkey-sigsum-submit.scd new file mode 100644 index 0000000..9c6e849 --- /dev/null +++ b/doc/tkey-sigsum-submit.scd @@ -0,0 +1,60 @@ +tkey-sigsum-submit(1) + +# NAME + +tkey-sigsum-submit - Process TKey submission files by submitting +requests to a Sigsum log. + +# SYNOPSIS + +*tkey-sigsum-submit* -h/--help + +*tkey-sigsum-submit* -m directory -n directory -d directory + +# DESCRIPTION + +*tkey-sigsum-submit* processes TKey submissions files and generates +TKey verification files that can be used to verify a TKey identity. + +Each submission file contains a Sigsum request. The request is used to +submit to a Sigsum log. The result is a verification file. + +# OPTIONS + +*-m* | *--submissions-dir* directory + + Set directory to read submission files from. + +*-n* | *--processed-submissions-dir* directory + + Set directory to move submitted files to. + +*-d* | *--verifications-dir* directory + + Set directory to write generated verification files in. + +# EXAMPLES + +``` +$ tkey-sigsum-submit -m signatures -n processed -d verifications +2025/09/10 10:23:23 [INFO] Attempting to submit checksum#1 to log: https://test.sigsum.org/barreleye +2025/09/10 10:23:23 [INFO] Attempting to retrieve proof for checksum#1 +``` + +# FILES + +The files in the submission directory (set with *-m*) are produced by +*tkey-verification*(1) running the *serve-sign* command. + +After a successful run of *tkey-sigsum-submit* the submitted submission +files are moved from the submissions directory to the processed +submissions directory (*-n*). The generated verification files are +placed in the verification directory (*-d*). + +# SEE ALSO + +*tkey-verification*(1) *tkey-verify*(1) + +# AUTHORS + +Tillitis AB, https://tillitis.se/ diff --git a/doc/tkey-verification.1 b/doc/tkey-verification.1 index 3089bbe..221a5bd 100644 --- a/doc/tkey-verification.1 +++ b/doc/tkey-verification.1 @@ -5,11 +5,11 @@ .nh .ad l .\" Begin generated content: -.TH "tkey-verification" "1" "2024-07-03" +.TH "tkey-verification" "1" "2025-09-22" .PP .SH NAME .PP -A program to sign or verify the identity of a Tillitis TKey.\& +tkey-verification - sign the identity of a Tillitis TKey.\& .PP .SH SYNOPSIS .PP @@ -20,29 +20,24 @@ A program to sign or verify the identity of a Tillitis TKey.\& \fBtkey-verification\fR serve-signer [--config path] [--check-config] [--port port] [--speed speed] .PP -\fBtkey-verification\fR show-pubkey [--port port] [--speed speed] -.PP -\fBtkey-verification\fR verify [--base-url url] [-d | --base-dir] [--port -port] [-u | --show-url] [--speed speed] +\fBtkey-verification\fR show-pubkey [--port port] [--speed speed] --app path .PP .SH DESCRIPTION .PP -\fBtkey-verification\fR is a program to sign or verify the identity of a -Tillitis TKey.\& +\fBtkey-verification\fR signs the identity of a Tillitis TKey.\& .PP -A typical end user will only be interested in the \fBverify\fR command.\& +A typical end user will only be interested in the \fBtkey-verify\fR(1) +command.\& \fBtkey-verification\fR is for the vendor.\& .PP The commands are as follows: .PP \fBremote-sign\fR .PP .RS 4 -Request that the tkey-verification serve-signer sign the identity +Request that the \fBtkey-verification\fR serve-signer sign the identity of a TKey.\& .PP -To use, first insert a TKey and then run the command.\& If one is -already running a TKey program, remove it and re-insert before -running the command.\& +To use, first insert a TKey and then run the command.\& .PP Options: .PP @@ -65,8 +60,9 @@ Speed in bit/s of the TKey device port.\& .RS 4 Provide a signing server with its own TKey, the vendor key.\& .PP -When it receives a signing request it signs the data and creates a new -file with metadata and a signature.\& See FILES.\& +When it receives a signing request from \fBremote-sign\fR it signs the +message and creates a new file with metadata and a Sigsum log +request.\& See FILES.\& .PP Options: .PP @@ -99,7 +95,7 @@ Speed in bit/s of the TKey device port.\& \fBshow-pubkey\fR .PP .RS 4 -Output public key data to populate "vendor-signing-pubkeys.\&txt" +Output public key data to populate the embedded vendor pubkeys for building into \fBtkey-verification\fR to be able to use some other commands.\& This is necessary in order to bootstrap \fBtkey-verification\fR from scratch or to add another vendor signing key.\& @@ -131,81 +127,45 @@ Speed in bit/s of the TKey device port.\& .PP .RE .RE -\fBverify\fR -.PP -.RS 4 -Verify a TKey identity.\& -.PP -To use, first insert a TKey and then run the command.\& If one is -already running a TKey program, remove it and re-insert before running -the command.\& -.PP -Options: -.PP -\fB--base-url\fR url -.PP -.RS 4 -Set the base URL of verification server for fetching verification -data.\& Default is "https://example.\&com/verify".\& -.PP -.RE -\fB-d\fR | \fB--base-dir\fR directory -.PP -.RS 4 -Read verification data from a file located in directory -and named after the TKey Unique Device Identifier in hex, instead of -from a URL.\& -.PP -.RE -\fB--port\fR port -.PP -.RS 4 -Path to the TKey device port.\& If not given, autodetection will be -attempted.\& -.PP -.RE -\fB-u\fR | \fB--show-url\fR -.PP -.RS 4 -Only output the URL to the verification data that should be -downloaded, then exit.\& -.PP -.RE -\fB--speed\fR speed -.PP -.RS 4 -Speed in bit/s of the TKey device port.\& +.SH FILES .PP -.RE -.RE -.SS Verification on a machine without network +Both \fBremote-sign\fR and \fBserve-signer\fR has YAML configuration files.\& .PP -If you'\&re on a machine without network and need to verify a TKey you -can run +In the \fBremote-sign\fR configuration file you need to specify: .PP .nf .RS 4 -$ tkey-verification verify ---show-url +--- +cacert: "path to the CA certificate" +clientcert: "path to the client certificate used by remote-sign" +clientkey: "path to the corresponding private key" +server: "name:port of the server to contact" + +# Hash digest of active app to authenticate a TKey - has to be present +# in internal/data/data\&.go +signingapphash: "3493a20868b99897f0caff2f7b57b35ea3565c59f5ba353439d444cf8aac07e4899f260b98cb13f2fbb1bf6416ba8e2083d537391c1495ed1362c9008b7f8dff" .fi .RE .PP -which will output the URL to the verification file.\& Download the file -using another, networked, computer and somehow bring the file or type -it in again on your airgapped computer.\& Then run: +In the \fBserve-signer\fR configuration file you need to specify: .PP .nf .RS 4 -tkey-verification verify -d=\&. +--- +cacert: "path to the CA certificate" +servercert: "path to the server certificate used by serve-signer" +serverkey: "path to the corresponding private key" +listen: "name:port to listen to for requests" + +# Current active Sigsum submit key - needs to occur in SigsumConf in internal/data/data\&.go +activekey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sQ sigsum key" .fi .RE .PP -to read from the current directory.\& -.PP -.SH FILES -.PP -\fBtkey-verification\fR serve-signer produces a file which is named after -the Unique Device Identifier (in hexadecimal) for every signature -made.\& An example filename would be "signatures/0133704100000015".\& +The \fBserve-signer\fR command produces a submission file which is named +after the Unique Device Identifier (in hexadecimal) for every +signature made.\& An example filename would be +"signatures/0133704100000015".\& .PP The file contains: .PP @@ -216,78 +176,44 @@ timestamp: RFC3339 UTC timestamp when the signature was done.\& tag: The Git tag of the signer program used on the device under verification, .IP \(bu 4 -signature: Vendor ed25519 signature of the device public key.\& Stored -in hexadecimal.\& +request: A Sigsum request to log a digest of our message.\& .PD .PP -The files generated will later be published on a public web server.\& -The publication is out of scope for the current program.\& -.PP -\fBtkey-verification\fR show-pubkey generates output for a file called -"vendor-signing-pubkeys.\&txt" to be included in the build of a new -\fBtkey-verification\fR.\& It'\&s output is: -.PP -.PD 0 -.IP \(bu 4 -Ed25519 public key -.IP \(bu 4 -app name and tag.\& -.IP \(bu 4 -digest of hash -.PD -.PP -Example file: -.PP -.nf -.RS 4 -038dd0b898c601517a09cd249d3c4f2de8e9aab38c5fa02701ae29bb41a6d863 verisigner-v0\&.0\&.1 9598910ec9ebe2504a5f894de6f8e0677dc94c156c7bd6f7e805a35354b3c85daa4ca66ab93f4d75221b501def457b4cafc933c6cdcf16d1eb8ccba6cccf6630 -.fi -.RE +The files generated can be submitted to a Sigsum log with +\fBtkey-sigsum-submit\fR(1).\& .PP .SH EXAMPLES .PP -Verifying the identity of a Tillitis TKey using a networked computer.\& +Run the signing server with a vendor key TKey inserted: .PP .nf .RS 4 -$ tkey-verification verify -TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 -TKey is genuine! +$ tkey-verification serve-signer --config tkey-verification-server\&.yaml .fi .RE .PP -Verifying the identity with a non-networked computer: First +On the provisioning computer, insert the new TKey and run: .PP .nf .RS 4 -$ tkey-verification --show-url +$ tkey-verification remote-sign --config tkey-verification-client\&.yaml .fi .RE .PP -Then download the file and move it to your current working directory.\& -Keep the name of the file intact since it'\&s named after the TKey -Unique Device Identifier.\& and run: +In order to include a new vendor signing key and Sigsum submit key, use: .PP .nf .RS 4 -$ tkey-verification verify -d=\&. -TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 -Reading verification data from file \&./0133708100000002 \&.\&.\&. -TKey is genuine! +$ tkey-verification show-pubkey --app internal/appbins/bins/signer-v1\&.0\&.1\&.bin +Public Key, app tag, and app hash for embedded vendor pubkeys follows on stdout: +03a7bd3be67cb466869904ec14b9974ebcc6e593abdc4151315ace2511b9c94d signer-v1\&.0\&.1 cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4 +SSH version: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOnvTvmfLRmhpkE7BS5l068xuWTq9xBUTFaziURuclN sigsum key .fi .RE .PP -In order to include a new vendor signing key, use: +.SS SEE ALSO .PP -.nf -.RS 4 -% \&./tkey-verification show-pubkey --port /dev/pts/12 --app cmd/tkey-verification/bins/signer-v1\&.0\&.0\&.bin -Connecting to device on serial port /dev/pts/12 \&.\&.\&. -Firmware name0:\&'tk1 \&' name1:\&'mkdf\&' version:5 -Public Key, app tag, and app hash for vendor-signing-pubkeys\&.txt follows on stdout: -038dd0b898c601517a09cd249d3c4f2de8e9aab38c5fa02701ae29bb41a6d863 verisigner-v0\&.0\&.1 9598910ec9ebe2504a5f894de6f8e0677dc94c156c7bd6f7e805a35354b3c85daa4ca66ab93f4d75221b501def457b4cafc933c6cdcf16d1eb8ccba6cccf6630 -.fi -.RE +\fBtkey-sigsum-submit\fR(1) \fBtkey-verify\fR(1) .PP .SH AUTHORS .PP @@ -297,17 +223,9 @@ Tillitis AB, https://tillitis.\&se/ .PP You can currently not use several TKeys on the same computer at the same time, which means you can'\&t use \fBserve-signer\fR and the other -commands on the same computer.\& +commands on the same computer, unless you start them with \fB--port\fR.\& .PP .SH SECURITY CONSIDERATIONS .PP -\fBtkey-verification\fR only verifies the /identity/ of the TKey hasn'\&t -changed since signing by the vendor.\& It might have been manipulated in -other ways.\& -.PP -The device public key isn'\&t published in the files generated by the -\fBserve-signer\fR but is retrievable by anyone with access to the device -under verification.\& -.PP You probably shouldn'\&t expose the computers running \fBserve-signer\fR or \fBremote-sign\fR on the Internet.\& diff --git a/doc/tkey-verification.scd b/doc/tkey-verification.scd index c54b8d5..81588b2 100644 --- a/doc/tkey-verification.scd +++ b/doc/tkey-verification.scd @@ -2,7 +2,7 @@ tkey-verification(1) # NAME -A program to sign or verify the identity of a Tillitis TKey. +tkey-verification - sign the identity of a Tillitis TKey. # SYNOPSIS @@ -13,28 +13,23 @@ A program to sign or verify the identity of a Tillitis TKey. *tkey-verification* serve-signer [--config path] [--check-config] [--port port] [--speed speed] -*tkey-verification* show-pubkey [--port port] [--speed speed] - -*tkey-verification* verify [--base-url url] [-d | --base-dir] [--port -port] [-u | --show-url] [--speed speed] +*tkey-verification* show-pubkey [--port port] [--speed speed] --app path # DESCRIPTION -*tkey-verification* is a program to sign or verify the identity of a -Tillitis TKey. +*tkey-verification* signs the identity of a Tillitis TKey. -A typical end user will only be interested in the *verify* command. +A typical end user will only be interested in the *tkey-verify*(1) +command. *tkey-verification* is for the vendor. The commands are as follows: *remote-sign* - Request that the tkey-verification serve-signer sign the identity + Request that the *tkey-verification* serve-signer sign the identity of a TKey. - To use, first insert a TKey and then run the command. If one is - already running a TKey program, remove it and re-insert before - running the command. + To use, first insert a TKey and then run the command. Options: @@ -51,8 +46,9 @@ The commands are as follows: Provide a signing server with its own TKey, the vendor key. - When it receives a signing request it signs the data and creates a new - file with metadata and a signature. See FILES. + When it receives a signing request from *remote-sign* it signs the + message and creates a new file with metadata and a Sigsum log + request. See FILES. Options: @@ -75,7 +71,7 @@ The commands are as follows: *show-pubkey* - Output public key data to populate "vendor-signing-pubkeys.txt" + Output public key data to populate the embedded vendor pubkeys for building into *tkey-verification* to be able to use some other commands. This is necessary in order to bootstrap *tkey-verification* from scratch or to add another vendor signing key. @@ -100,127 +96,78 @@ The commands are as follows: Speed in bit/s of the TKey device port. -*verify* - - Verify a TKey identity. - - To use, first insert a TKey and then run the command. If one is - already running a TKey program, remove it and re-insert before running - the command. - - Options: - - *--base-url* url - - Set the base URL of verification server for fetching verification - data. Default is "https://example.com/verify". - - *-d* | *--base-dir* directory - - Read verification data from a file located in directory - and named after the TKey Unique Device Identifier in hex, instead of - from a URL. - - *--port* port - - Path to the TKey device port. If not given, autodetection will be - attempted. - - *-u* | *--show-url* - - Only output the URL to the verification data that should be - downloaded, then exit. - - *--speed* speed - - Speed in bit/s of the TKey device port. +# FILES -## Verification on a machine without network +Both *remote-sign* and *serve-signer* has YAML configuration files. -If you're on a machine without network and need to verify a TKey you -can run +In the *remote-sign* configuration file you need to specify: ``` -$ tkey-verification verify ---show-url +--- +cacert: "path to the CA certificate" +clientcert: "path to the client certificate used by remote-sign" +clientkey: "path to the corresponding private key" +server: "name:port of the server to contact" + +# Hash digest of active app to authenticate a TKey - has to be present +# in internal/data/data.go +signingapphash: "3493a20868b99897f0caff2f7b57b35ea3565c59f5ba353439d444cf8aac07e4899f260b98cb13f2fbb1bf6416ba8e2083d537391c1495ed1362c9008b7f8dff" ``` -which will output the URL to the verification file. Download the file -using another, networked, computer and somehow bring the file or type -it in again on your airgapped computer. Then run: +In the *serve-signer* configuration file you need to specify: ``` -tkey-verification verify -d=. +--- +cacert: "path to the CA certificate" +servercert: "path to the server certificate used by serve-signer" +serverkey: "path to the corresponding private key" +listen: "name:port to listen to for requests" + +# Current active Sigsum submit key - needs to occur in SigsumConf in internal/data/data.go +activekey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sQ sigsum key" ``` -to read from the current directory. - -# FILES - -*tkey-verification* serve-signer produces a file which is named after -the Unique Device Identifier (in hexadecimal) for every signature -made. An example filename would be "signatures/0133704100000015". +The *serve-signer* command produces a submission file which is named +after the Unique Device Identifier (in hexadecimal) for every +signature made. An example filename would be +"signatures/0133704100000015". The file contains: - timestamp: RFC3339 UTC timestamp when the signature was done. - tag: The Git tag of the signer program used on the device under verification, -- signature: Vendor ed25519 signature of the device public key. Stored - in hexadecimal. - -The files generated will later be published on a public web server. -The publication is out of scope for the current program. - -*tkey-verification* show-pubkey generates output for a file called -"vendor-signing-pubkeys.txt" to be included in the build of a new -*tkey-verification*. It's output is: +- request: A Sigsum request to log a digest of our message. -- Ed25519 public key -- app name and tag. -- digest of hash - -Example file: - -``` -038dd0b898c601517a09cd249d3c4f2de8e9aab38c5fa02701ae29bb41a6d863 verisigner-v0.0.1 9598910ec9ebe2504a5f894de6f8e0677dc94c156c7bd6f7e805a35354b3c85daa4ca66ab93f4d75221b501def457b4cafc933c6cdcf16d1eb8ccba6cccf6630 -``` +The files generated can be submitted to a Sigsum log with +*tkey-sigsum-submit*(1). # EXAMPLES -Verifying the identity of a Tillitis TKey using a networked computer. +Run the signing server with a vendor key TKey inserted: ``` -$ tkey-verification verify -TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 -TKey is genuine! +$ tkey-verification serve-signer --config tkey-verification-server.yaml ``` -Verifying the identity with a non-networked computer: First +On the provisioning computer, insert the new TKey and run: ``` -$ tkey-verification --show-url +$ tkey-verification remote-sign --config tkey-verification-client.yaml ``` -Then download the file and move it to your current working directory. -Keep the name of the file intact since it's named after the TKey -Unique Device Identifier. and run: +In order to include a new vendor signing key and Sigsum submit key, use: ``` -$ tkey-verification verify -d=. -TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 -Reading verification data from file ./0133708100000002 ... -TKey is genuine! +$ tkey-verification show-pubkey --app internal/appbins/bins/signer-v1.0.1.bin +Public Key, app tag, and app hash for embedded vendor pubkeys follows on stdout: +03a7bd3be67cb466869904ec14b9974ebcc6e593abdc4151315ace2511b9c94d signer-v1.0.1 cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4 +SSH version: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOnvTvmfLRmhpkE7BS5l068xuWTq9xBUTFaziURuclN sigsum key ``` -In order to include a new vendor signing key, use: +## SEE ALSO -``` -% ./tkey-verification show-pubkey --port /dev/pts/12 --app cmd/tkey-verification/bins/signer-v1.0.0.bin -Connecting to device on serial port /dev/pts/12 ... -Firmware name0:'tk1 ' name1:'mkdf' version:5 -Public Key, app tag, and app hash for vendor-signing-pubkeys.txt follows on stdout: -038dd0b898c601517a09cd249d3c4f2de8e9aab38c5fa02701ae29bb41a6d863 verisigner-v0.0.1 9598910ec9ebe2504a5f894de6f8e0677dc94c156c7bd6f7e805a35354b3c85daa4ca66ab93f4d75221b501def457b4cafc933c6cdcf16d1eb8ccba6cccf6630 -``` +*tkey-sigsum-submit*(1) *tkey-verify*(1) # AUTHORS @@ -230,17 +177,9 @@ Tillitis AB, https://tillitis.se/ You can currently not use several TKeys on the same computer at the same time, which means you can't use *serve-signer* and the other -commands on the same computer. +commands on the same computer, unless you start them with *--port*. # SECURITY CONSIDERATIONS -*tkey-verification* only verifies the /identity/ of the TKey hasn't -changed since signing by the vendor. It might have been manipulated in -other ways. - -The device public key isn't published in the files generated by the -*serve-signer* but is retrievable by anyone with access to the device -under verification. - You probably shouldn't expose the computers running *serve-signer* or *remote-sign* on the Internet. diff --git a/doc/tkey-verify.1 b/doc/tkey-verify.1 new file mode 100644 index 0000000..66156aa --- /dev/null +++ b/doc/tkey-verify.1 @@ -0,0 +1,134 @@ +.\" Generated by scdoc 1.11.3 +.\" Complete documentation for this program is not available as a GNU info page +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.nh +.ad l +.\" Begin generated content: +.TH "tkey-verify" "1" "2025-09-11" +.PP +.SH NAME +.PP +tkey-verify - Verify the identity of a Tillitis TKey.\& +.PP +.SH SYNOPSIS +.PP +\fBtkey-verify\fR -h/--help +.PP +\fBtkey-verify\fR [--base-url url] [-d | --base-dir] [--port port] [-u | --show-url] [--speed speed] +.PP +.SH DESCRIPTION +.PP +\fBtkey-verify\fR verifies the identity of a Tillitis TKey.\& +.PP +To use, first insert a TKey, then run the command.\& If a TKey is +already running a device app, remove the TKey and insert again before +running the command.\& +.PP +.SH OPTIONS +.PP +\fB--base-url\fR url +.PP +.RS 4 +Set the base URL of verification server for fetching verification +data.\& Default is "https://example.\&com/verify".\& +.PP +.RE +\fB-d\fR | \fB--base-dir\fR directory +.PP +.RS 4 +Read verification data from a file located in directory +and named after the TKey Unique Device Identifier in hex, instead of +from a URL.\& +.PP +.RE +\fB--port\fR port +.PP +.RS 4 +Path to the TKey device port.\& If not given, autodetection will be +attempted.\& +.PP +.RE +\fB-u\fR | \fB--show-url\fR +.PP +.RS 4 +Only output the URL to the verification data that should be +downloaded, then exit.\& +.PP +.RE +\fB--speed\fR speed +.PP +.RS 4 +Speed in bit/s of the TKey device port.\& +.PP +.RE +.SS Verification on a machine without network +.PP +On a machine without network you can run +.PP +.nf +.RS 4 +$ tkey-verify --show-url +.fi +.RE +.PP +which will output the URL to the verification file.\& Download the file +using another, networked, computer and bring the file or type it in +again on your airgapped computer.\& Then run: +.PP +.nf +.RS 4 +tkey-verify -d=\&. +.fi +.RE +.PP +to read from the current directory.\& +.PP +.SH EXAMPLES +.PP +Verifying the identity of a Tillitis TKey using a networked computer.\& +.PP +.nf +.RS 4 +$ tkey-verify +TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 +TKey is genuine! +.fi +.RE +.PP +Verifying the identity on an airgapped computer: First +.PP +.nf +.RS 4 +$ tkey-verify --show-url +.fi +.RE +.PP +Then download the file and move it to your current working directory.\& +Keep the name of the file intact since it'\&s named after the TKey +Unique Device Identifier.\& Then run: +.PP +.nf +.RS 4 +$ tkey-verify -d=\&. +TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 +Reading verification data from file \&./0133708100000002 \&.\&.\&. +TKey is genuine! +.fi +.RE +.PP +.SH SEE ALSO +.PP +\fBtkey-verification\fR(1) \fBtkey-sigsum-submit\fR(1) +.PP +.SH AUTHORS +.PP +Tillitis AB, https://tillitis.\&se/ +.PP +.SH SECURITY CONSIDERATIONS +.PP +\fBtkey-verify\fR only verifies that the \fBidentity\fR of the TKey hasn'\&t changed since +signing by the vendor.\& It might have been manipulated in other ways.\& +.PP +The device public key isn'\&t published in the verification files but is +retrievable by anyone with access to the device under verification.\& diff --git a/doc/tkey-verify.scd b/doc/tkey-verify.scd new file mode 100644 index 0000000..205c719 --- /dev/null +++ b/doc/tkey-verify.scd @@ -0,0 +1,107 @@ +tkey-verify(1) + +# NAME + +tkey-verify - Verify the identity of a Tillitis TKey. + +# SYNOPSIS + +*tkey-verify* -h/--help + +*tkey-verify* [--base-url url] [-d | --base-dir] [--port port] [-u | --show-url] [--speed speed] + +# DESCRIPTION + +*tkey-verify* verifies the identity of a Tillitis TKey. + +To use, first insert a TKey, then run the command. If a TKey is +already running a device app, remove the TKey and insert again before +running the command. + +# OPTIONS + +*--base-url* url + + Set the base URL of verification server for fetching verification + data. Default is "https://example.com/verify". + +*-d* | *--base-dir* directory + + Read verification data from a file located in directory + and named after the TKey Unique Device Identifier in hex, instead of + from a URL. + +*--port* port + + Path to the TKey device port. If not given, autodetection will be + attempted. + +*-u* | *--show-url* + + Only output the URL to the verification data that should be + downloaded, then exit. + +*--speed* speed + + Speed in bit/s of the TKey device port. + +## Verification on a machine without network + +On a machine without network you can run + +``` +$ tkey-verify --show-url +``` + +which will output the URL to the verification file. Download the file +using another, networked, computer and bring the file or type it in +again on your airgapped computer. Then run: + +``` +tkey-verify -d=. +``` + +to read from the current directory. + +# EXAMPLES + +Verifying the identity of a Tillitis TKey using a networked computer. + +``` +$ tkey-verify +TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 +TKey is genuine! +``` + +Verifying the identity on an airgapped computer: First + +``` +$ tkey-verify --show-url +``` + +Then download the file and move it to your current working directory. +Keep the name of the file intact since it's named after the TKey +Unique Device Identifier. Then run: + +``` +$ tkey-verify -d=. +TKey UDI: 0x0133708100000002(BE) VendorID: 0x1337 ProductID: 2 ProductRev: 1 +Reading verification data from file ./0133708100000002 ... +TKey is genuine! +``` + +# SEE ALSO + +*tkey-verification*(1) *tkey-sigsum-submit*(1) + +# AUTHORS + +Tillitis AB, https://tillitis.se/ + +# SECURITY CONSIDERATIONS + +*tkey-verify* only verifies that the *identity* of the TKey hasn't changed since +signing by the vendor. It might have been manipulated in other ways. + +The device public key isn't published in the verification files but is +retrievable by anyone with access to the device under verification. diff --git a/go.mod b/go.mod index 84acf08..24d3452 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,21 @@ module github.com/tillitis/tkey-verification -go 1.21 +go 1.23.0 require ( github.com/spf13/pflag v1.0.5 - github.com/tillitis/tkeyclient v1.1.0 + github.com/tillitis/tkeyclient v1.2.0 github.com/tillitis/tkeysign v1.0.1 gopkg.in/yaml.v2 v2.4.0 + sigsum.org/sigsum-go v0.11.2 ) require ( github.com/ccoveille/go-safecast v1.1.0 // indirect github.com/creack/goselect v0.1.2 // indirect go.bug.st/serial v1.6.2 // indirect - golang.org/x/crypto v0.28.0 // indirect - golang.org/x/sys v0.26.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect ) diff --git a/go.sum b/go.sum index 528fead..3216c0a 100644 --- a/go.sum +++ b/go.sum @@ -4,25 +4,33 @@ github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0 github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/tillitis/tkeyclient v1.1.0 h1:/RTIGMZGR8cZ6bUkgZLUmDvTNH5C7qSU2qaSDl3nxhA= -github.com/tillitis/tkeyclient v1.1.0/go.mod h1:K3b/zU58CLtP5iaOMypc4c7PAAmNqyw7F3bs5iNYtG0= +github.com/tillitis/tkeyclient v1.2.0 h1:X/PjUPK061EFNmf3V4knb20wTfcIuUH13VSC/0VFaYo= +github.com/tillitis/tkeyclient v1.2.0/go.mod h1:4TiEj0qUwaffmOODPZro6cUBaw+Gb6F5EFd6tcH7UXE= github.com/tillitis/tkeysign v1.0.1 h1:E+nycj/iv9247MOBval1PmWcZ68J7LUXhXdlW49+N08= github.com/tillitis/tkeysign v1.0.1/go.mod h1:C4gYuqI2HHJ86S0PoWlk30eFsKDnMvdEgIYIVyNfzvY= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigsum.org/sigsum-go v0.11.2 h1:7HhDPC8gVJzl3wB3gAg3j6gTpO2t0UPHC0ogwhKuNRc= +sigsum.org/sigsum-go v0.11.2/go.mod h1:pGa/r4QsNYom+RqRMkdhcG5E00ty1nlTmALEizdRWPk= diff --git a/cmd/tkey-verification/appbins.go b/internal/appbins/appbins.go similarity index 56% rename from cmd/tkey-verification/appbins.go rename to internal/appbins/appbins.go index 84f5fe3..8c1d69e 100644 --- a/cmd/tkey-verification/appbins.go +++ b/internal/appbins/appbins.go @@ -1,18 +1,18 @@ // SPDX-FileCopyrightText: 2023 Tillitis AB // SPDX-License-Identifier: BSD-2-Clause -package main +package appbins import ( - "bytes" "crypto/sha512" "embed" - "encoding/hex" "fmt" "io/fs" "path" "sort" "strings" + + "github.com/tillitis/tkey-verification/internal/util" ) type AppBin struct { @@ -21,25 +21,15 @@ type AppBin struct { } func (a *AppBin) String() string { - return fmt.Sprintf("tag:%s hash:%0x…", a.Tag, a.Hash()[:16]) + return fmt.Sprintf("tag:%s hash:%0x…", a.Tag, a.Hash()) } -func (a *AppBin) Hash() []byte { - hash := sha512.Sum512(a.Bin) - return hash[:] +func (a *AppBin) Hash() [sha512.Size]byte { + return sha512.Sum512(a.Bin) } type AppBins struct { - Bins map[string]AppBin -} - -// Get returns an AppBin indexed by the app hash digest. -func (a AppBins) Get(hash string) (AppBin, error) { - if val, ok := a.Bins[hash]; ok { - return val, nil - } - - return AppBin{}, ErrNotFound + Bins map[[sha512.Size]byte]AppBin } //go:embed bins/*.bin bins/*.bin.sha512 @@ -62,12 +52,12 @@ func (a AppBins) Tags() []string { // NewAppBins initializes the embedded device apps. func NewAppBins() (AppBins, error) { var appBins = AppBins{ - Bins: map[string]AppBin{}, + Bins: map[[sha512.Size]byte]AppBin{}, } entries, err := binsFS.ReadDir(binsDir) if err != nil { - return AppBins{}, IOError{path: binsDir, err: err} + return appBins, fmt.Errorf("error when reading %v: %w", binsDir, err) } for _, entry := range entries { @@ -85,24 +75,27 @@ func NewAppBins() (AppBins, error) { var info fs.FileInfo if info, err = entry.Info(); err != nil { - return AppBins{}, IOError{path: binFn, err: err} + return appBins, fmt.Errorf("couldn't stat %v: %w", binFn, err) } else if info.Size() == 0 { - return AppBins{}, MissingError{what: binFn} + return appBins, fmt.Errorf("missing file %v", binFn) } var bin []byte if bin, err = binsFS.ReadFile(path.Join(binsDir, binFn)); err != nil { - return AppBins{}, IOError{path: binFn, err: err} + return appBins, fmt.Errorf("couldn't read %v: %w", binFn, err) } // Require accompanying sha512 file with matching hash hashFn := binFn + ".sha512" - var hash []byte - if hash, err = binsFS.ReadFile(path.Join(binsDir, hashFn)); err != nil { - return AppBins{}, IOError{path: binFn, err: err} + var hashHex []byte + if hashHex, err = binsFS.ReadFile(path.Join(binsDir, hashFn)); err != nil { + return appBins, fmt.Errorf("couldn't read %v: %w", path.Join(binsDir, hashFn), err) } - if hash, err = hex.DecodeString(string(hash[:sha512.Size*2])); err != nil { - return AppBins{}, IOError{path: hashFn, err: err} + + var hash [sha512.Size]byte + + if err := util.DecodeHex(hash[:], string(hashHex[:sha512.Size*2])); err != nil { + return appBins, fmt.Errorf("%w", err) } appBin := AppBin{ @@ -110,12 +103,21 @@ func NewAppBins() (AppBins, error) { Bin: bin, } - if !bytes.Equal(appBin.Hash(), hash) { - return AppBins{}, EqualError{one: binFn, two: hashFn} + if appBin.Hash() != hash { + return appBins, fmt.Errorf("digests of %v != %v", binFn, hashFn) } - appBins.Bins[hex.EncodeToString(hash)] = appBin + appBins.Bins[hash] = appBin } return appBins, nil } + +func MustAppBins() AppBins { + bins, err := NewAppBins() + if err != nil { + panic(err) + } + + return bins +} diff --git a/cmd/tkey-verification/bins/.gitkeep b/internal/appbins/bins/.gitkeep similarity index 100% rename from cmd/tkey-verification/bins/.gitkeep rename to internal/appbins/bins/.gitkeep diff --git a/cmd/tkey-verification/bins/README.md b/internal/appbins/bins/README.md similarity index 71% rename from cmd/tkey-verification/bins/README.md rename to internal/appbins/bins/README.md index df1d0b6..0a21fe7 100644 --- a/cmd/tkey-verification/bins/README.md +++ b/internal/appbins/bins/README.md @@ -3,6 +3,11 @@ This file consists of a list of embedded binaries, the location of the source and the license. +File signer-tkey-verification-1.bin +Source: https://github.com/tillitis/tkey-device-signer +License: BSD-2 +Tag: tkey-verification1 + File: signer-v1.0.1.bin Source: https://github.com/tillitis/tkey-device-signer License: GPL-2.0 Only diff --git a/internal/appbins/bins/signer-tkey-verification-1.bin b/internal/appbins/bins/signer-tkey-verification-1.bin new file mode 100644 index 0000000..807b3f3 Binary files /dev/null and b/internal/appbins/bins/signer-tkey-verification-1.bin differ diff --git a/internal/appbins/bins/signer-tkey-verification-1.bin.sha512 b/internal/appbins/bins/signer-tkey-verification-1.bin.sha512 new file mode 100644 index 0000000..846d50c --- /dev/null +++ b/internal/appbins/bins/signer-tkey-verification-1.bin.sha512 @@ -0,0 +1 @@ +3493a20868b99897f0caff2f7b57b35ea3565c59f5ba353439d444cf8aac07e4899f260b98cb13f2fbb1bf6416ba8e2083d537391c1495ed1362c9008b7f8dff signer/app.bin diff --git a/cmd/tkey-verification/bins/signer-v1.0.1.bin b/internal/appbins/bins/signer-v1.0.1.bin similarity index 100% rename from cmd/tkey-verification/bins/signer-v1.0.1.bin rename to internal/appbins/bins/signer-v1.0.1.bin diff --git a/cmd/tkey-verification/bins/signer-v1.0.1.bin.sha512 b/internal/appbins/bins/signer-v1.0.1.bin.sha512 similarity index 100% rename from cmd/tkey-verification/bins/signer-v1.0.1.bin.sha512 rename to internal/appbins/bins/signer-v1.0.1.bin.sha512 diff --git a/cmd/tkey-verification/bins/verisigner-v0.0.3.bin b/internal/appbins/bins/verisigner-v0.0.3.bin similarity index 100% rename from cmd/tkey-verification/bins/verisigner-v0.0.3.bin rename to internal/appbins/bins/verisigner-v0.0.3.bin diff --git a/cmd/tkey-verification/bins/verisigner-v0.0.3.bin.deps b/internal/appbins/bins/verisigner-v0.0.3.bin.deps similarity index 100% rename from cmd/tkey-verification/bins/verisigner-v0.0.3.bin.deps rename to internal/appbins/bins/verisigner-v0.0.3.bin.deps diff --git a/cmd/tkey-verification/bins/verisigner-v0.0.3.bin.sha512 b/internal/appbins/bins/verisigner-v0.0.3.bin.sha512 similarity index 100% rename from cmd/tkey-verification/bins/verisigner-v0.0.3.bin.sha512 rename to internal/appbins/bins/verisigner-v0.0.3.bin.sha512 diff --git a/internal/data/data.go b/internal/data/data.go new file mode 100644 index 0000000..42947a4 --- /dev/null +++ b/internal/data/data.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package data + +////////////////////////////////////////////////////////////////////// +/// Vendor key +////////////////////////////////////////////////////////////////////// + +const VendorPubKeys = `14274d3570097aea209af1c23b64aa439a4d0d32c62735c5f6d6a29600c9a275 verisigner-v0.0.3 f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142` + +// Test vendor key. +// const VendorPubKeys = `50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10 verisigner-v0.0.3 f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142` + +////////////////////////////////////////////////////////////////////// +/// Sigsum +////////////////////////////////////////////////////////////////////// + +// const SigsumConf = ` +// # This Sigsum submit key is the exact same key as the first vendor pubkey above +// tillitis-sigsum1 +// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIONFrsjCVeDB3KwJVsfr/kphaZZZ9Sypuu42ahZBjeya sigsum key +// verisigner-v0.0.3 +// f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 +// ` + +// Test Sigsum submit key corresponding to verisigner-0.3 running on QEMU with test UDS. +const SigsumConf = ` +tillitis-sigsum-test +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sQ sigsum key +verisigner-v0.0.3 +f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 +2025-09-16T08:10:33+02:00 +2125-09-16T08:11:33+02:00 +` + +// Sigsum policy +const PolicyStr = `log 4644af2abd40f4895a003bca350f9d5912ab301a49c77f13e5b6d905c20a5fe6 https://test.sigsum.org/barreleye + +witness poc.sigsum.org/nisse 1c25f8a44c635457e2e391d1efbca7d4c2951a0aef06225a881e46b98962ac6c + +group demo-quorum-rule any poc.sigsum.org/nisse +quorum demo-quorum-rule +` + +////////////////////////////////////////////////////////////////////// +/// Firmwares +////////////////////////////////////////////////////////////////////// + +const FirmwaresConf = ` +# The default/qemu UDI0, with firmware from main at +# TK1-24.03 (1c90b1aa3dbfb4e62039683ee6049ae8af608498) +# UDI 00010203 +00010203 0010 8 3 4192 3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623 + +# Firmware from main at +# c126199a41149f6284aa9533e72395c978733b44 +01337080 1337 2 0 4192 3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623 + +# First Bellatrix release +01337081 1337 2 1 4192 3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623 + +# TK1-24.03 (1c90b1aa3dbfb4e62039683ee6049ae8af608498) +01337082 1337 2 2 4160 06d0aafcc763307420380a8c5a324f3fccfbba6af7ff6fe0facad684ebd69dd43234c8531a096c77c2dc3543f8b8b629c94136ca7e257ca560da882e4dbbb025 +` diff --git a/cmd/tkey-verification/firmwares.go b/internal/firmware/firmware.go similarity index 57% rename from cmd/tkey-verification/firmwares.go rename to internal/firmware/firmware.go index dbcc07c..6cd5664 100644 --- a/cmd/tkey-verification/firmwares.go +++ b/internal/firmware/firmware.go @@ -1,15 +1,20 @@ // SPDX-FileCopyrightText: 2023 Tillitis AB // SPDX-License-Identifier: BSD-2-Clause -package main +package firmware import ( "crypto/sha512" "encoding/binary" "encoding/hex" + "errors" "fmt" + "strconv" + "strings" + "github.com/tillitis/tkey-verification/internal/data" "github.com/tillitis/tkey-verification/internal/tkey" + "github.com/tillitis/tkey-verification/internal/util" ) const ( @@ -17,6 +22,15 @@ const ( fwSizeMax int = 8192 ) +type Hardware struct { + Udi string + VendorID uint16 + ProductID uint8 + ProductRev uint8 + FwSize int + FwHash [sha512.Size]byte +} + type Firmware struct { Hash [sha512.Size]byte Size int @@ -30,17 +44,22 @@ type Firmwares struct { // GetFirmware returns what we know about a firmware indexed by a UDI, // or an error if no firmare is found. -func (f Firmwares) GetFirmware(udi tkey.UDI) (*Firmware, error) { +func (f Firmwares) GetFirmware(udi tkey.UDI) (Firmware, error) { + var fw Firmware + hw, err := newHardware(udi.VendorID, udi.ProductID, udi.ProductRev) if err != nil { - return nil, err + return fw, err } - fw, ok := f.firmwares[*hw] + + var ok bool + + fw, ok = f.firmwares[*hw] if !ok { - return nil, ErrUDI + return fw, errors.New("firmware not found") } - return &fw, nil + return fw, nil } func (f Firmwares) List() []string { @@ -53,45 +72,90 @@ func (f Firmwares) List() []string { return list } -// NewFirmwares initialises all known firmwares. It returns a map of -// all firmwares or an error. -// -// To add a new firmware to the database, use addFirmware() in this -// function. -func NewFirmwares() (Firmwares, error) { - fws := Firmwares{ - firmwares: make(map[hardware]Firmware), - } +func (f *Firmwares) FromString(fwStr string) error { + lines := strings.Split(strings.Trim(strings.ReplaceAll(fwStr, "\r\n", "\n"), "\n"), "\n") - var err error + // Put everything in the map, indexed by the hardware description + f.firmwares = make(map[hardware]Firmware) - // This is the default/qemu UDI0, with firmware from main at - // c126199a41149f6284aa9533e72395c978733b44 - err = fws.addFirmware("00010203", 0x0010, 8, 3, 4192, "3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623") - if err != nil { - return fws, err - } + for _, line := range lines { + fields := strings.Fields(line) - err = fws.addFirmware("01337080", 0x1337, 2, 0, 4192, "3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623") - if err != nil { - return fws, err + if len(fields) == 0 || strings.HasPrefix(fields[0], "#") { + // ignoring empty/spaces-only lines and comments + continue + } + + if len(fields) != 6 { + return errors.New("expected 6 fields: UDI0 vendor product rev size hash") + } + + udi0Str, vendorStr, productStr, revStr, sizeStr, hashStr := fields[0], fields[1], fields[2], fields[3], fields[4], fields[5] + + var hw Hardware + + hw.Udi = udi0Str + + var err error + var vid uint64 + + vid, err = strconv.ParseUint(vendorStr, 16, 16) + if err != nil { + return fmt.Errorf("%w", err) + } + + hw.VendorID = uint16(vid) + + var prod uint64 + prod, err = strconv.ParseUint(productStr, 10, 8) + if err != nil { + return fmt.Errorf("%w", err) + } + + hw.ProductID = uint8(prod) + + var rev uint64 + rev, err = strconv.ParseUint(revStr, 10, 8) + if err != nil { + return fmt.Errorf("%w", err) + } + + hw.ProductRev = uint8(rev) + + hw.FwSize, err = strconv.Atoi(sizeStr) + if err != nil { + return fmt.Errorf("%w", err) + } + + if err := f.addFirmware(hw.Udi, hw.VendorID, hw.ProductID, hw.ProductRev, hw.FwSize, hashStr); err != nil { + return err + } } - err = fws.addFirmware("01337081", 0x1337, 2, 1, 4192, "3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623") - if err != nil { - return fws, err + return nil +} + +func (f *Firmwares) MustDecodeString(s string) { + if err := f.FromString(s); err != nil { + panic(err) } +} - err = fws.addFirmware("01337082", 0x1337, 2, 2, 4160, "06d0aafcc763307420380a8c5a324f3fccfbba6af7ff6fe0facad684ebd69dd43234c8531a096c77c2dc3543f8b8b629c94136ca7e257ca560da882e4dbbb025") - if err != nil { - return fws, err +// NewFirmwares initialises all known firmwares. It returns a map of +// all firmwares or an error. +// +// To add a new firmware to the database, use addFirmware() in this +// function. +func NewFirmwares() (Firmwares, error) { + f := Firmwares{ + firmwares: make(map[hardware]Firmware), } - if len(fws.firmwares) == 0 { - return fws, MissingError{what: "no firmware"} + if err := f.FromString(data.FirmwaresConf); err != nil { + return f, err } - return fws, nil + return f, nil } // addFirmware adds a new known hardware identified by the triple @@ -104,10 +168,10 @@ func NewFirmwares() (Firmwares, error) { func (f *Firmwares) addFirmware(udi0BEhex string, vendorID uint16, productID uint8, productRev uint8, fwSize int, fwHashHex string) error { udi0BE, err := hex.DecodeString(udi0BEhex) if err != nil { - return ParseError{what: "UDI", err: err} + return fmt.Errorf("couldn't decode UDI: %w", err) } if l := len(udi0BE); l != tkey.UDISize/2 { - return ErrWrongLen + return errors.New("wrong length of UDI0") } hw, err := newHardware(vendorID, productID, productRev) @@ -116,33 +180,31 @@ func (f *Firmwares) addFirmware(udi0BEhex string, vendorID uint16, productID uin } if fwSize < fwSizeMin { - return ErrWrongLen + return errors.New("too small firmware size") } if fwSize > fwSizeMax { - return ErrWrongLen + return errors.New("too large firmware size") } - fwHash, err := hex.DecodeString(fwHashHex) - if err != nil { - return ParseError{what: "firmware hash", err: err} - } - if l := len(fwHash); l != sha512.Size { - return ErrWrongLen + var fwHash [sha512.Size]byte + + if err := util.DecodeHex(fwHash[:], fwHashHex); err != nil { + return fmt.Errorf("%w", err) } // Safety check. We compare the passed UDI0 argument to what // we computed from vendor ID, product ID, and product // revision. If it's not the same, we bail. if udi0BEhex != hw.toUDI0BEhex() { - return EqualError{one: "udi0BEhex arg", two: "calculated"} + return errors.New("udi0BEhex arg != calculated") } if _, ok := f.firmwares[*hw]; ok { - return ExistError{what: "hardware with same UDI0"} + return errors.New("hardware with same UDI0") } f.firmwares[*hw] = Firmware{ - Hash: *(*[64]byte)(fwHash), + Hash: fwHash, Size: fwSize, } @@ -161,10 +223,10 @@ func newHardware(vendorID uint16, productID uint8, productRev uint8) (*hardware, const sixBitsMax = 2*2*2*2*2*2 - 1 if productID > sixBitsMax { - return nil, RangeError{what: "product ID"} + return nil, errors.New("product ID out of range") } if productRev > sixBitsMax { - return nil, RangeError{what: "product revision"} + return nil, errors.New("product revision out of range") } return &hardware{ diff --git a/internal/firmware/firmware_test.go b/internal/firmware/firmware_test.go new file mode 100644 index 0000000..96b91e7 --- /dev/null +++ b/internal/firmware/firmware_test.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package firmware + +import ( + "strings" + "testing" + + "github.com/tillitis/tkey-verification/internal/data" +) + +func TestParseEmbeddedFirmwares(t *testing.T) { + var f Firmwares + + if err := f.FromString(data.FirmwaresConf); err != nil { + t.Fatal(err) + } +} + +const validFwHashHex = "06d0aafcc763307420380a8c5a324f3fccfbba6af7ff6fe0facad684ebd69dd43234c8531a096c77c2dc3543f8b8b629c94136ca7e257ca560da882e4dbbb025" + +func TestWrongFirmware(t *testing.T) { + var f = Firmwares{ + firmwares: make(map[hardware]Firmware), + } + + // Not hex. Err should be filled + err := f.addFirmware("oo", 01, 02, 03, 4711, validFwHashHex) + assertErrorMsgStartsWith(t, err, "couldn't decode UDI: ") + + // Wrong UDI length + err = f.addFirmware("000102", 16, 8, 3, 4711, validFwHashHex) + assertErrorMsgStartsWith(t, err, "wrong length of UDI0") + + // firmware too small + err = f.addFirmware("00010203", 16, 8, 3, 1999, validFwHashHex) + assertErrorMsgStartsWith(t, err, "too small firmware size") + + // firmware too big + err = f.addFirmware("00010203", 16, 8, 3, 8193, validFwHashHex) + assertErrorMsgStartsWith(t, err, "too large firmware size") + + // Broken firmware digest hex + err = f.addFirmware("00010203", 16, 8, 3, 8192, "oo") + assertErrorMsgStartsWith(t, err, "encoding/hex: invalid byte: U+006F 'o'") + + // Wrong length of firmware digest hex + err = f.addFirmware("00010203", 16, 8, 3, 8192, "ffff") + assertErrorMsgStartsWith(t, err, "unexpected length of hex data, expected 64, got 2") + + // Wrong UDI0 compared to calculated UDI0 + err = f.addFirmware("00010203", 01, 02, 03, 8192, validFwHashHex) + assertErrorMsgStartsWith(t, err, "udi0BEhex arg != calculated") + + // Add same hardware twice + err = f.addFirmware("00010203", 16, 8, 3, 4711, validFwHashHex) + assertNoError(t, err) + err = f.addFirmware("00010203", 16, 8, 3, 4711, validFwHashHex) + assertErrorMsgStartsWith(t, err, "hardware with same UDI") +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + + if err != nil { + t.Log("Expected error") + t.Fail() + } +} + +func assertErrorMsgStartsWith(t *testing.T, err error, want string) { + t.Helper() + + if err == nil { + t.Log("Expected error") + t.Fail() + return + } + + if !strings.HasPrefix(err.Error(), want) { + t.Logf("Unexpected error '%v', should start with '%v'", err, want) + t.Fail() + } +} diff --git a/internal/sigsum/sigsum.go b/internal/sigsum/sigsum.go new file mode 100644 index 0000000..e7ece6e --- /dev/null +++ b/internal/sigsum/sigsum.go @@ -0,0 +1,190 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package sigsum + +import ( + "bufio" + "bytes" + "crypto/ed25519" + "crypto/sha512" + "errors" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/tillitis/tkey-verification/internal/appbins" + "github.com/tillitis/tkey-verification/internal/data" + "github.com/tillitis/tkey-verification/internal/util" + sumcrypto "sigsum.org/sigsum-go/pkg/crypto" + "sigsum.org/sigsum-go/pkg/key" + "sigsum.org/sigsum-go/pkg/policy" +) + +type PubKey struct { + Name string + Key [ed25519.PublicKeySize]byte // Vendor public key + Tag string // Name and tag of the device app + AppHash [sha512.Size]byte // Hash of app binary used for signing with this key + AppBin appbins.AppBin // The actual device app binary + Start time.Time + End time.Time +} + +func (p PubKey) String() string { + return fmt.Sprintf("%v using app %v: %x\n Valid: %v - %v\n", p.Name, p.Tag, p.Key, p.Start.Format(time.RFC3339), p.End.Format(time.RFC3339)) +} + +type Log struct { + Keys map[[ed25519.PublicKeySize]byte]PubKey // key -> PubKey + SubmitKeys map[sumcrypto.Hash]sumcrypto.PublicKey + Policy *policy.Policy +} + +func (s *Log) String() string { + var output string + + output = "Logs:\n" + for _, log := range s.Policy.GetLogsWithUrl() { + output += fmt.Sprintf(" - Key: %x\n", log.PublicKey) + output += fmt.Sprintf(" URL: %v\n\n", log.URL) + } + + output += "Submit keys:\n" + for _, key := range s.Keys { + output += fmt.Sprintf(" - %s", key) + } + + return output +} + +type State int + +const ( + sName = iota + sKey + sTag + sAppHash + sStart + sEnd +) + +func ParseKeys(r io.Reader, appBins appbins.AppBins) (map[[ed25519.PublicKeySize]byte]PubKey, error) { + var pubkey PubKey + var state State + + pubKeys := map[[ed25519.PublicKeySize]byte]PubKey{} + + state = sName + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + line := scanner.Text() + + // Skip comments or empty lines + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + switch state { + case sName: + pubkey.Name = line + state = sKey + + case sKey: + pkey, err := key.ParsePublicKey(line) + if err != nil { + return nil, fmt.Errorf("%w", err) + } + + pubkey.Key = pkey + state = sTag + + case sTag: + pubkey.Tag = line + state = sAppHash + + case sAppHash: + if err := util.DecodeHex(pubkey.AppHash[:], line); err != nil { + return nil, fmt.Errorf("couldn't decode pubkey app hash: %w", err) + } + + // Do we have the app? + app, ok := appBins.Bins[pubkey.AppHash] + if !ok { + // App not found. + return nil, errors.New("app used for key not found") + } + + pubkey.AppBin = app + + state = sStart + + case sStart: + startTime, err := time.Parse(time.RFC3339, line) + if err != nil { + return nil, fmt.Errorf("couldn't parse key start time: %w", err) + } + + pubkey.Start = startTime + state = sEnd + + case sEnd: + endTime, err := time.Parse(time.RFC3339, line) + if err != nil { + return nil, fmt.Errorf("couldn't parse key end time: %w", err) + } + + pubkey.End = endTime + + // This is the last, store away and reset state to begin again + pubKeys[pubkey.Key] = pubkey + + state = sName + default: + return nil, errors.New("unknown state when parsing keys") + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to parse sigsum keys: %w", err) + } + + return pubKeys, nil +} + +func (s *Log) FromEmbedded() error { + return s.FromString(data.SigsumConf, data.PolicyStr) +} + +func (s *Log) FromString(sigsumConf string, policyStr string) error { + // Get all our embedded device apps used for vendor signing + appBins, err := appbins.NewAppBins() + if err != nil { + fmt.Printf("Failed to init embedded device apps: %v\n", err) + os.Exit(1) + } + + keys, err := ParseKeys(bytes.NewBufferString(sigsumConf), appBins) + if err != nil { + return fmt.Errorf("%w", err) + } + + s.Keys = keys + + // Transform to Sigsum submitkeys + s.SubmitKeys = make(map[sumcrypto.Hash]sumcrypto.PublicKey) + for _, key := range keys { + s.SubmitKeys[sumcrypto.HashBytes(key.Key[:])] = key.Key + } + + // Parse policy + s.Policy, err = policy.ParseConfig(bytes.NewBufferString(policyStr)) + if err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/internal/sigsum/sigsum_test.go b/internal/sigsum/sigsum_test.go new file mode 100644 index 0000000..988273d --- /dev/null +++ b/internal/sigsum/sigsum_test.go @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package sigsum + +import ( + "testing" +) + +func TestParseEmbedded(t *testing.T) { + var s Log + + if err := s.FromEmbedded(); err != nil { + t.Fatal(err) + } +} + +const sigsumConf = `tillitis-sigsum-test +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sQ sigsum key +verisigner-v0.0.3 +f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 +2025-09-16T08:10:33+02:00 +2025-09-16T08:11:33+02:00 + +tillitis-sigsum-test2 +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sA sigsum key +signer-v1.0.1 +cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4 +2025-09-16T08:10:33+02:00 +2025-09-16T08:11:33+02:00 +` + +// Sigsum policy +const policyStr = `log 4644af2abd40f4895a003bca350f9d5912ab301a49c77f13e5b6d905c20a5fe6 https://test.sigsum.org/barreleye + +witness poc.sigsum.org/nisse 1c25f8a44c635457e2e391d1efbca7d4c2951a0aef06225a881e46b98962ac6c + +group demo-quorum-rule any poc.sigsum.org/nisse +quorum demo-quorum-rule +` + +func TestParse(t *testing.T) { + var s Log + + if err := s.FromString(sigsumConf, policyStr); err != nil { + t.Fatal(err) + } + + if len(s.Keys) != 2 { + t.Fatal("wrong number of submit keys parsed") + } +} diff --git a/internal/ssh/README.md b/internal/ssh/README.md new file mode 100644 index 0000000..ba77c95 --- /dev/null +++ b/internal/ssh/README.md @@ -0,0 +1,4 @@ +Taken from sigsum-go/internal/ssh and slightly altered + +Project URL: +https://git.glasklar.is/sigsum/core/sigsum-go diff --git a/internal/ssh/public.go b/internal/ssh/public.go new file mode 100644 index 0000000..5aa6937 --- /dev/null +++ b/internal/ssh/public.go @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2025 Glasklar Teknik +// SPDX-License-Identifier: BSD-2-Clause + +package ssh + +import ( + "bytes" + "crypto/ed25519" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "log" + "math" + "strings" + + "sigsum.org/sigsum-go/pkg/crypto" +) + +type ( + PublicKey [ed25519.PublicKeySize]byte + bytesOrString interface{ []byte | string } +) + +func serializeUint32(x uint32) []byte { + buffer := make([]byte, 4) + binary.BigEndian.PutUint32(buffer, x) + return buffer +} + +func serializeString[T bytesOrString](s T) []byte { + slen := len(s) + if slen > math.MaxInt32 { + log.Panicf("string too large for ssh, length %d", slen) + } + buffer := make([]byte, 4+slen) + binary.BigEndian.PutUint32(buffer, uint32(slen)) + copy(buffer[4:], s) + return buffer +} + +// Skips prefix, if present, otherwise return nil. +func skipPrefix(buffer []byte, prefix []byte) []byte { + if !bytes.HasPrefix(buffer, prefix) { + return nil + } + return buffer[len(prefix):] +} + +func serializePublicEd25519(pub [ed25519.PublicKeySize]byte) []byte { + return bytes.Join([][]byte{ + serializeString("ssh-ed25519"), + serializeString(pub[:])}, + nil) +} + +func parsePublicEd25519(blob []byte) (PublicKey, error) { + pub := skipPrefix(blob, bytes.Join([][]byte{ + serializeString("ssh-ed25519"), + serializeUint32(crypto.PublicKeySize), + }, nil)) + + if pub == nil { + return PublicKey{}, errors.New("invalid public key blob prefix") + } + if len(pub) != crypto.PublicKeySize { + return PublicKey{}, fmt.Errorf("invalid public key length: %v", len(blob)) + } + var ret PublicKey + copy(ret[:], pub) + return ret, nil +} + +func ParsePublicEd25519(asciiKey string) (PublicKey, error) { + // Split into fields, recognizing exclusively ascii space and TAB + fields := strings.FieldsFunc(asciiKey, func(c rune) bool { + return c == ' ' || c == '\t' + }) + if len(fields) < 2 { + return PublicKey{}, errors.New("invalid public key, splitting line failed") + } + if fields[0] != "ssh-ed25519" { + return PublicKey{}, fmt.Errorf("unsupported public key type: %v", fields[0]) + } + blob, err := base64.StdEncoding.DecodeString(fields[1]) + if err != nil { + return PublicKey{}, fmt.Errorf("%w", err) + } + return parsePublicEd25519(blob) +} + +func FormatPublicEd25519(pub [ed25519.PublicKeySize]byte) string { + return "ssh-ed25519 " + + base64.StdEncoding.EncodeToString(serializePublicEd25519(pub)) + + " sigsum key\n" +} diff --git a/internal/submission/submission.go b/internal/submission/submission.go new file mode 100644 index 0000000..1aae9cf --- /dev/null +++ b/internal/submission/submission.go @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package submission + +import ( + "bytes" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/tillitis/tkey-verification/internal/util" + "sigsum.org/sigsum-go/pkg/requests" +) + +type submissionJSON struct { + Timestamp string `json:"timestamp"` + AppTag string `json:"apptag"` + AppHash string `json:"apphash"` + Request string `json:"request"` +} + +type Submission struct { + Timestamp time.Time + AppTag string + AppHash [sha512.Size]byte + Request requests.Leaf +} + +func (s *Submission) FromJSON(b []byte) error { + var sJ submissionJSON + + if err := json.Unmarshal(b, &sJ); err != nil { + return fmt.Errorf("couldn't unmarshal JSON: %w", err) + } + + var err error + + s.Timestamp, err = time.Parse(time.RFC3339, sJ.Timestamp) + if err != nil { + return fmt.Errorf("%w", err) + } + + if sJ.AppTag == "" { + return errors.New("app-tag empty") + } + s.AppTag = sJ.AppTag + + if err = util.DecodeHex(s.AppHash[:], sJ.AppHash); err != nil { + return errors.New("couldn't decode app digest") + } + + err = s.Request.FromASCII(bytes.NewBufferString(sJ.Request)) + if err != nil { + return fmt.Errorf("couldn't decode request: %w", err) + } + + _, err = s.Request.Verify() + if err != nil { + return fmt.Errorf("invalid request: %w", err) + } + + return nil +} + +func (s *Submission) ToJSON() ([]byte, error) { + var sJ submissionJSON + + sJ.Timestamp = s.Timestamp.UTC().Format(time.RFC3339) + sJ.AppTag = s.AppTag + sJ.AppHash = hex.EncodeToString(s.AppHash[:]) + + reqTextBuilder := strings.Builder{} + err := s.Request.ToASCII(&reqTextBuilder) + if err != nil { + return nil, fmt.Errorf("couldn't convert request to ASCII: %w", err) + } + sJ.Request = reqTextBuilder.String() + + json, err := json.Marshal(sJ) + if err != nil { + return nil, fmt.Errorf("couldn't marshal JSON: %w", err) + } + + return json, nil +} + +func (s *Submission) FromFile(fn string) error { + submissionJSON, err := os.ReadFile(fn) + if err != nil { + return fmt.Errorf("%w", err) + } + + return s.FromJSON(submissionJSON) +} + +func (s *Submission) ToFile(fn string) error { + sJ, err := s.ToJSON() + if err != nil { + return err + } + + err = os.WriteFile(fn, append(sJ, '\n'), 0o600) + if err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/internal/submission/submission_test.go b/internal/submission/submission_test.go new file mode 100644 index 0000000..0d5fb6e --- /dev/null +++ b/internal/submission/submission_test.go @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package submission + +import ( + "bytes" + "crypto/sha512" + "encoding/hex" + "strings" + "testing" + "time" + + "github.com/tillitis/tkey-verification/internal/util" + sumcrypto "sigsum.org/sigsum-go/pkg/crypto" + "sigsum.org/sigsum-go/pkg/requests" +) + +const submJSON = ` +{ + "timestamp":"2025-09-02T10:56:48Z", + "apptag":"signer-v1.0.1", + "apphash":"cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4", + "request":"message=f23e454ee9c9627dd1a80f6ab2e1565fa0cda3a7c91f853eb8099ff645674719\nsignature=b4f9eabdcb6b05d259e964ba6fa427c178b5586d30e6b4026287656c8a7ee2674af33d2c05701ea8f98458fe7c54b787c7a73c0fda6f09046bcf7604cea86c00\npublic_key=50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10\n" +} +` + +func TestJSONDecodeSubmission(t *testing.T) { + var s Submission + + if err := s.FromJSON([]byte(submJSON)); err != nil { + t.Fatal(err) + } + + wantTime := time.Date(2025, 9, 2, 10, 56, 48, 0, time.UTC) + if s.Timestamp != wantTime { + t.Fatalf("Incorrect timestamp. Got: %v, want: %v", s.Timestamp, wantTime) + } + + wantAppTag := "signer-v1.0.1" + if s.AppTag != wantAppTag { + t.Fatalf("Incorrect apptag. Got: %v, want: %v", s.AppTag, wantAppTag) + } + + wantAppHash := mustDecodeHash("cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4") + if s.AppHash != wantAppHash { + t.Logf("Incorrect apphash. Got: %v, want: %v", s.AppHash, wantAppHash) + } + + rMessage := mustDecodeHexString("f23e454ee9c9627dd1a80f6ab2e1565fa0cda3a7c91f853eb8099ff645674719") + rSignature := mustDecodeHexString("b4f9eabdcb6b05d259e964ba6fa427c178b5586d30e6b4026287656c8a7ee2674af33d2c05701ea8f98458fe7c54b787c7a73c0fda6f09046bcf7604cea86c00") + rPublicKey := mustDecodeHexString("50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10") + wantRequest := requests.Leaf{ + Message: sumcrypto.Hash(rMessage), + Signature: sumcrypto.Signature(rSignature), + PublicKey: sumcrypto.PublicKey(rPublicKey), + } + if s.Request != wantRequest { + t.Fatalf("Incorrect request. Got: %+v, want: %+v", s.Request, wantRequest) + } + + _, err := s.Request.Verify() + if err != nil { + t.Fatal("Not a valid sigsum leaf request") + } +} + +func TestJSONEncodeSubmission(t *testing.T) { + appHash := mustDecodeHash("cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4") + rMessage := mustDecodeHexString("f23e454ee9c9627dd1a80f6ab2e1565fa0cda3a7c91f853eb8099ff645674719") + rSignature := mustDecodeHexString("b4f9eabdcb6b05d259e964ba6fa427c178b5586d30e6b4026287656c8a7ee2674af33d2c05701ea8f98458fe7c54b787c7a73c0fda6f09046bcf7604cea86c00") + rPublicKey := mustDecodeHexString("50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10") + s := Submission{ + Timestamp: time.Date(2025, 9, 2, 10, 56, 48, 0, time.UTC), + AppTag: "signer-v1.0.1", + AppHash: appHash, + Request: requests.Leaf{ + Message: sumcrypto.Hash(rMessage), + Signature: sumcrypto.Signature(rSignature), + PublicKey: sumcrypto.PublicKey(rPublicKey), + }, + } + + wantJSONStr := submJSON + wantJSONStr = strings.ReplaceAll(wantJSONStr, "\n", "") + wantJSONStr = strings.ReplaceAll(wantJSONStr, " ", "") + wantJSON := []byte(wantJSONStr) + + js, err := s.ToJSON() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(js, wantJSON) { + t.Fatalf("Incorrect JSON. Got: %s, want: %s", js, wantJSON) + } +} + +func mustDecodeHexString(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + + return b +} + +func mustDecodeHash(s string) [sha512.Size]byte { + var hash [sha512.Size]byte + + if err := util.DecodeHex(hash[:], s); err != nil { + panic(err) + } + + return hash +} diff --git a/internal/tkey/tkey.go b/internal/tkey/tkey.go index 9672165..27c3be8 100644 --- a/internal/tkey/tkey.go +++ b/internal/tkey/tkey.go @@ -20,6 +20,8 @@ package tkey import ( + "crypto/ed25519" + "crypto/rand" "fmt" "log" "os" @@ -168,6 +170,17 @@ func (t TKey) GetFirmwareHash(firmwareSize int) ([]byte, error) { return fwHash, nil } +// GetPubkey connects to a TKey and asks an already running +// verisigner-app for its public key. +func (t TKey) GetPubkey() ([]byte, error) { + pubkey, err := t.signer.GetPubkey() + if err != nil { + return nil, fmt.Errorf("%w", err) + } + + return pubkey, nil +} + func handleSignals(action func(), sig ...os.Signal) chan<- os.Signal { ch := make(chan os.Signal, 1) signal.Notify(ch, sig...) @@ -179,3 +192,25 @@ func handleSignals(action func(), sig ...os.Signal) chan<- os.Signal { }() return ch } + +// Challenge gets a device signature over a random challenge. +// +// It returns true if verification was a success. False otherwise. +func (t *TKey) Challenge(pubKey []byte) error { + challenge := make([]byte, 32) + if _, err := rand.Read(challenge); err != nil { + return fmt.Errorf("rand.Read failed: %w", err) + } + + signature, err := t.Sign(challenge) + if err != nil { + return fmt.Errorf("%w", err) + } + + // Verify device signature against device public key + if !ed25519.Verify(pubKey, challenge, signature) { + return fmt.Errorf("%w", err) + } + + return nil +} diff --git a/internal/tkey/udi.go b/internal/tkey/udi.go index 13c0a74..48d9e14 100644 --- a/internal/tkey/udi.go +++ b/internal/tkey/udi.go @@ -31,12 +31,13 @@ func (u *UDI) fromRawLE(udiLE []byte) error { } vpr := binary.LittleEndian.Uint32(udiLE[0:4]) - if reserved := uint8((vpr >> 28) & 0xf); reserved != 0 { + if reserved := uint8((vpr >> 28) & 0xf); reserved != 0 { // nolint:gosec + return ErrWrongUDIData } - u.VendorID = uint16((vpr >> 12) & 0xffff) - u.ProductID = uint8((vpr >> 6) & 0x3f) - u.ProductRev = uint8(vpr & 0x3f) + u.VendorID = uint16((vpr >> 12) & 0xffff) // nolint:gosec + u.ProductID = uint8((vpr >> 6) & 0x3f) // nolint:gosec + u.ProductRev = uint8(vpr & 0x3f) // nolint:gosec // u.Serial = binary.LittleEndian.Uint32(udiLE[4:8]) u.Bytes = make([]byte, 8) diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..54c3ab7 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package util + +import ( + "bytes" + "crypto/ed25519" + "crypto/sha512" + "encoding/hex" + "errors" + "fmt" + "runtime/debug" + + "github.com/tillitis/tkey-verification/internal/tkey" +) + +func DecodeHex(out []byte, s string) error { + b, err := hex.DecodeString(s) + if err != nil { + return fmt.Errorf("%w", err) + } + if len(b) != len(out) { + return fmt.Errorf("unexpected length of hex data, expected %d, got %d", len(out), len(b)) + } + copy(out, b) + + return nil +} + +func BuildMessage(udiBE, fwHash, pubKey []byte) ([]byte, error) { + var buf bytes.Buffer + + if l := len(udiBE); l != tkey.UDISize { + return nil, errors.New("wrong length of UDI") + } + buf.Write(udiBE) + + if l := len(fwHash); l != sha512.Size { + return nil, errors.New("wrong length of digest") + } + buf.Write(fwHash) + + if l := len(pubKey); l != ed25519.PublicKeySize { + return nil, errors.New("wrong length of pubkey") + } + buf.Write(pubKey) + + return buf.Bytes(), nil +} + +func Version(version string) string { + if version == "" { + if info, ok := debug.ReadBuildInfo(); ok { + // When built with go install ...@version + version = info.Main.Version + if version != "(devel)" { + return version + } + } + } + + return version +} diff --git a/cmd/tkey-verification/vendorpubkeys.go b/internal/vendorkey/vendorkey.go similarity index 50% rename from cmd/tkey-verification/vendorpubkeys.go rename to internal/vendorkey/vendorkey.go index 48a2001..60b5cae 100644 --- a/cmd/tkey-verification/vendorpubkeys.go +++ b/internal/vendorkey/vendorkey.go @@ -1,27 +1,26 @@ // SPDX-FileCopyrightText: 2024 Tillitis AB // SPDX-License-Identifier: BSD-2-Clause -package main +package vendorkey import ( "bytes" "crypto/ed25519" "crypto/sha512" - _ "embed" "encoding/hex" + "errors" "fmt" "strings" -) -// nolint:typecheck // Avoid lint error when the embedding file is missing. -// -//go:embed vendor-signing-pubkeys.txt -var pubKeysData []byte + "github.com/tillitis/tkey-verification/internal/appbins" + "github.com/tillitis/tkey-verification/internal/data" + "github.com/tillitis/tkey-verification/internal/util" +) type PubKey struct { PubKey [ed25519.PublicKeySize]byte // Vendor public key Tag string // Name and tag of the device app - AppBin AppBin // The actual device app binary + AppBin appbins.AppBin // The actual device app binary } func (p *PubKey) String() string { @@ -43,17 +42,15 @@ func (v *VendorKeys) String() string { return sb.String() } -// NewVendorKeys initializes all the known vendor public keys. It +// FromString initializes all the known vendor public keys. It // needs to know the existing device applications (get them with // NewAppBins()) // // It returns the vendor public keys and any error. -func NewVendorKeys(appBins AppBins) (VendorKeys, error) { - lines := strings.Split(strings.Trim(strings.ReplaceAll(string(pubKeysData), "\r\n", "\n"), "\n"), "\n") +func (v *VendorKeys) FromString(pubkeys string, appBins appbins.AppBins) error { + lines := strings.Split(strings.Trim(strings.ReplaceAll(pubkeys, "\r\n", "\n"), "\n"), "\n") - var vendorKeys = VendorKeys{ - map[string]PubKey{}, - } + v.Keys = make(map[string]PubKey) for _, line := range lines { fields := strings.Fields(line) @@ -64,50 +61,52 @@ func NewVendorKeys(appBins AppBins) (VendorKeys, error) { } if len(fields) != 3 { - return vendorKeys, SimpleParseError{msg: "Expected 3 space-separated fields: pubkey in hex, signer-app tag, and its hash in hex"} + return errors.New("expected 3 space-separated fields: pubkey in hex, signer-app tag, and its hash in hex") } pubKeyHex, tag, appHashHex := fields[0], fields[1], fields[2] pubKey, err := hex.DecodeString(pubKeyHex) if err != nil { - return vendorKeys, ParseError{what: "public key hex", err: err} + return fmt.Errorf("couldn't decode public key: %w", err) } if l := len(pubKey); l != ed25519.PublicKeySize { - return vendorKeys, ErrWrongLen + return errors.New("public key has wrong length") } - appHash, err := hex.DecodeString(appHashHex) - if err != nil { - return vendorKeys, ParseError{what: "app digest hex", err: err} - } - if l := len(appHash); l != sha512.Size { - return vendorKeys, ErrWrongLen + var appHash [sha512.Size]byte + + if err := util.DecodeHex(appHash[:], appHashHex); err != nil { + return fmt.Errorf("%w", err) } - var appBin AppBin - if _, ok := appBins.Bins[appHashHex]; ok { - appBin = appBins.Bins[appHashHex] + var appBin appbins.AppBin + if _, ok := appBins.Bins[appHash]; ok { + appBin = appBins.Bins[appHash] if appBin.Tag != tag { - return vendorKeys, EqualError{"embedded app tag", "vendor signing app tag"} + return errors.New("embedded app tag != vendor signing app tag") } } else { - return vendorKeys, ErrNotFound + return fmt.Errorf("couldn't find device app for digest %v", appHashHex) } - for _, pk := range vendorKeys.Keys { - if bytes.Compare(pubKey, pk.PubKey[:]) == 0 { - return vendorKeys, ExistError{what: "public key"} + for _, pk := range v.Keys { + if bytes.Equal(pubKey, pk.PubKey[:]) { + return errors.New("public key already exists") } } - vendorKeys.Keys[appHashHex] = PubKey{ + v.Keys[appHashHex] = PubKey{ PubKey: *(*[ed25519.PublicKeySize]byte)(pubKey), Tag: tag, AppBin: appBin, } } - return vendorKeys, nil + return nil +} + +func (v *VendorKeys) FromEmbedded(appBins appbins.AppBins) error { + return v.FromString(string(data.VendorPubKeys), appBins) } diff --git a/internal/verification/verification.go b/internal/verification/verification.go new file mode 100644 index 0000000..865f493 --- /dev/null +++ b/internal/verification/verification.go @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package verification + +import ( + "bytes" + "crypto/ed25519" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "net/http" + "os" + "strings" + "time" + + "github.com/tillitis/tkey-verification/internal/sigsum" + "github.com/tillitis/tkey-verification/internal/util" + "github.com/tillitis/tkey-verification/internal/vendorkey" + sumcrypto "sigsum.org/sigsum-go/pkg/crypto" + "sigsum.org/sigsum-go/pkg/proof" +) + +type Type int + +const ( + VerSig Type = iota + VerProof +) + +type verificationJSON struct { + Timestamp string `json:"timestamp"` + AppTag string `json:"apptag"` + AppHash string `json:"apphash"` + Signature string `json:"signature,omitempty"` + Proof string `json:"proof"` +} + +type Verification struct { + Type Type + Timestamp time.Time + AppTag string + AppHash [sha512.Size]byte + Signature []byte + Proof proof.SigsumProof +} + +func (v *Verification) FromJSON(b []byte) error { + var vJ verificationJSON + + if err := json.Unmarshal(b, &vJ); err != nil { + return fmt.Errorf("couldn't unmarshal JSON: %w", err) + } + + var err error + + v.Timestamp, err = time.Parse(time.RFC3339, vJ.Timestamp) + if err != nil { + return fmt.Errorf("%w", err) + } + + if vJ.AppTag == "" { + return errors.New("app-tag empty") + } + v.AppTag = vJ.AppTag + + if err = util.DecodeHex(v.AppHash[:], vJ.AppHash); err != nil { + return errors.New("couldn't decode app digest") + } + + if vJ.Proof != "" && vJ.Signature != "" { + return errors.New("verification file contains both Sigsum proof and vendor signature") + } + + if vJ.Proof != "" { + // This contains a Sigsum proof + v.Type = VerProof + + if err = v.Proof.FromASCII(bytes.NewBufferString(vJ.Proof)); err != nil { + return fmt.Errorf("couldn't parse proof: %w", err) + } + } else { + // Old type vendor signature + v.Type = VerSig + + v.Signature, err = hex.DecodeString(vJ.Signature) + if err != nil { + return errors.New("couldn't decode vendor signature") + } + } + + return nil +} + +func (v *Verification) ToJSON() ([]byte, error) { + var vJ verificationJSON + + vJ.Timestamp = v.Timestamp.UTC().Format(time.RFC3339) + vJ.AppTag = v.AppTag + vJ.AppHash = hex.EncodeToString(v.AppHash[:]) + + if v.Type == VerSig || len(v.Signature) != 0 { + return nil, errors.New("vendor signature not supported") + } + + if v.Type == VerProof { + proofTextBuilder := strings.Builder{} + err := v.Proof.ToASCII(&proofTextBuilder) + if err != nil { + return nil, fmt.Errorf("couldn't convert proof to ASCII: %w", err) + } + vJ.Proof = proofTextBuilder.String() + } else { + return nil, errors.New("unknown verification type") + } + + json, err := json.Marshal(vJ) + if err != nil { + return nil, fmt.Errorf("couldn't marshal JSON: %w", err) + } + + return json, nil +} + +func (v *Verification) FromFile(fn string) error { + verificationJSON, err := os.ReadFile(fn) + if err != nil { + return fmt.Errorf("%w", err) + } + + return v.FromJSON(verificationJSON) +} + +func (v *Verification) ToFile(fn string) error { + vJ, err := v.ToJSON() + if err != nil { + return err + } + + err = os.WriteFile(fn, append(vJ, '\n'), 0o600) + if err != nil { + return fmt.Errorf("%w", err) + } + + return nil +} + +func (v *Verification) FromURL(verifyURL string) error { + client := http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(verifyURL) // #nosec G107 + if err != nil { + return fmt.Errorf("error accessing %v: %v", verifyURL, resp.Status) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("error accessing %v: %v", verifyURL, resp.Status) + } + + verificationJSON, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("couldn't ready body: %w", err) + } + + return v.FromJSON(verificationJSON) +} + +func (v *Verification) IsProof() bool { + return v.Type == VerProof +} + +func (v *Verification) IsSig() bool { + return v.Type == VerSig +} + +// VerifySig verifies a signature (Signature in the struct) over +// message 'msg' against a number of public keys in 'vendorKeys'. +// +// It returns the matching public key that successfully verified the +// signature, if any, and any error. +func (v *Verification) VerifySig(msg []byte, vendorKeys vendorkey.VendorKeys) (vendorkey.PubKey, error) { + // We allow for any of the known vendor keys and return on the + // first which verifies. + for _, vendorPubKey := range vendorKeys.Keys { + if ed25519.Verify(vendorPubKey.PubKey[:], msg, v.Signature) { + return vendorPubKey, nil + } + } + + return vendorkey.PubKey{}, errors.New("vendor signature not verified") +} + +func (v *Verification) VerifyProofDigest(digest sumcrypto.Hash, log sigsum.Log) (sigsum.PubKey, error) { + var ourPubkey sigsum.PubKey + + if err := v.Proof.Verify(&digest, log.SubmitKeys, log.Policy); err != nil { + return ourPubkey, fmt.Errorf("%w", err) + } + + keyFound := false + for keyindex, key := range log.Keys { + if sumcrypto.HashBytes(keyindex[:]) == v.Proof.Leaf.KeyHash { + keyFound = true + ourPubkey = key + break + } + } + + if !keyFound { + return ourPubkey, errors.New("couldn't find submit key") + } + + // Also check that all cosigning witness signatures timestamps are + // within the submit key lifetime + for _, c := range v.Proof.TreeHead.Cosignatures { + if c.Timestamp > math.MaxInt64 { + return ourPubkey, fmt.Errorf("invalid timestamp: %d", c.Timestamp) + } + + ts := time.Unix(int64(c.Timestamp), 0) + + if !ts.After(ourPubkey.Start) { + return ourPubkey, fmt.Errorf("witness cosignature outside of lifetime, %v not in %v - %v", + ts.Format(time.RFC3339), + ourPubkey.Start.Format(time.RFC3339), + ourPubkey.End.Format(time.RFC3339)) + } + + if !ts.Before(ourPubkey.End) { + return ourPubkey, fmt.Errorf("witness cosignature outside of lifetime, %v not in %v - %v", + ts.Format(time.RFC3339), + ourPubkey.Start.Format(time.RFC3339), + ourPubkey.End.Format(time.RFC3339)) + } + } + + return ourPubkey, nil +} + +func (v *Verification) VerifyProof(msg []byte, log sigsum.Log) (sigsum.PubKey, error) { + digest := sumcrypto.HashBytes(msg) + + return v.VerifyProofDigest(digest, log) +} diff --git a/internal/verification/verification_test.go b/internal/verification/verification_test.go new file mode 100644 index 0000000..5ea9ae8 --- /dev/null +++ b/internal/verification/verification_test.go @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2025 Tillitis AB +// SPDX-License-Identifier: BSD-2-Clause + +package verification + +import ( + "crypto/ed25519" + "encoding/hex" + "fmt" + "strings" + "testing" + + "github.com/tillitis/tkey-verification/internal/sigsum" + "github.com/tillitis/tkey-verification/internal/util" + "github.com/tillitis/tkey-verification/internal/vendorkey" + sumcrypto "sigsum.org/sigsum-go/pkg/crypto" +) + +const submitKey = `tillitis-sigsum-test +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIONFrsjCVeDB3KwJVsfr/kphaZZZ9Sypuu42ahZBjeya sigsum key +verisigner-v0.0.3 +f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 +2025-08-16T08:10:33+02:00 +2125-09-16T08:11:33+02:00 +` + +const oldSubmitKey = `tillitis-sigsum-test +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIONFrsjCVeDB3KwJVsfr/kphaZZZ9Sypuu42ahZBjeya sigsum key +verisigner-v0.0.3 +f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 +2023-09-16T08:10:33+02:00 +2023-09-16T08:11:33+02:00 +` + +const policyStr = `log 4644af2abd40f4895a003bca350f9d5912ab301a49c77f13e5b6d905c20a5fe6 https://test.sigsum.org/barreleye + +witness poc.sigsum.org/nisse 1c25f8a44c635457e2e391d1efbca7d4c2951a0aef06225a881e46b98962ac6c + +group demo-quorum-rule any poc.sigsum.org/nisse +quorum demo-quorum-rule +` + +const verificationSigJSON = ` +{ + "timestamp": "2023-03-03T09:31:51Z", + "apptag":"verisigner-v0.0.3", + "apphash":"f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142", + "signature":"e30f47287220a6aa0553cb93ca38f60eb0601fa5f802dc15d7be13447ea4f57308a94137543c5492606cb4b69eaa6f9618e7806d588c391fdd83cd920dcf230c" +} +` + +const verificationProofJSON = ` +{ + "timestamp": "2023-03-03T09:31:51Z", + "apptag":"verisigner-v0.0.3", + "apphash":"f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142", + "proof": "version=1\nlog=4e89cc51651f0d95f3c6127c15e1a42e3ddf7046c5b17b752689c402e773bb4d\nleaf=ca78 f3744a4d05231ceed5d704e7fcdd8ee436f2253d980cf5716ae34cc16c06f439 acd7edc4e9483b4bc3aee2ada2c7b08ac5868e762a024420e94e82ef0ab0dc3f480bf89bde6a88906bacd183b79a8803e72ab507fd3c40d60a2200ca912a1208\n\nsize=4473\nroot_hash=b09a7824a111f11e1ea00b1e76735eba200284830e3fa6fee84d8d556bacb1b8\nsignature=6414fa4fb92d42c067675299c7a3fb2ad4e926e0960f29dd7efca63df2602975b580873f49baa6fb5477f8ec0db26a2b8eac9787d18c025643ea45b89060ac02\ncosignature=70b861a010f25030de6ff6a5267e0b951e70c04b20ba4a3ce41e7fba7b9b7dfc 1756222257 08a623f83c5dfc33cac5c76370f8436920dd9a9428c52c9958075fc6ec54c11295acbc648c580a88d9a9c8cbc10f9075f055ba39848e75734a04429e80ccdf09\ncosignature=b95ef35a9ffb3cf516f423a04128d37d3bffc74d4096bd5e967990c53d09678a 1756222257 7f6ca5349f054d7a375054c5718dcfa864f245d45d273085ff939aeaed2391ed072fae13d92f8374afb0ae48b6f3b2d787a928518330fee71de0450e7ad39906\ncosignature=0d4f46a219ab309cea48cde9712e7d8486fc99802f873175eeab70fb84b4f5a4 1756222257 5685321ea3776687d12efeb398d09723f00092ed007a482f2a13a66270442b2e639a6b9faa2d7a6481be5dc6927447244fd04b959cc468cb4c774b8022e74604\ncosignature=49c4cd6124b7c572f3354d854d50b2a4b057a750f786cf03103c09de339c4ea3 1756222257 03bc716a896f16a97f3328e2efb340eaf13d224c42d51825c0acffda9080cd388e0b304d55f55205d01ebfb3f334ae2bf11494c83aff481a4519e7790c7b150a\ncosignature=1c997261f16e6e81d13f420900a2542a4b6a049c2d996324ee5d82a90ca3360c 1756222257 83d5f8bcdb33e82082c7f8969f7a798687838074ac086fd99bdb677df0275363fc957d6cd63d3462ac88392b3a5d03e0d7eead34ba28d64bf030ac9354b53a0c\ncosignature=42351ad474b29c04187fd0c8c7670656386f323f02e9a4ef0a0055ec061ecac8 1756222257 25791a84085b014e0357acaf68425abf8a8c0f33f754d521b935972ae2a7fdbe8f939e9ce173145aefc3b8d814162dbd627f0a72edb4655b13ac9864879c6808\n\nleaf_index=4472\nnode_hash=c52527e8f58d5e11428c85c8308892cb22383437d2a8428a37335c4b461b640e\nnode_hash=730ccb8fa755d9bbcb3282b9dc8f7e0900c21d0110287efcc0580f5b6cd298ec\nnode_hash=454c7b728002a36a48db60d03a2e3c0eccf043b8e3179ef1aebcc97baf8bde62\nnode_hash=0103bb70827f682b599c65fb22ef2b53ab26ec590c6b2aae920ded4065514f89\nnode_hash=aa56901f083cdabbaaa1ac1066b2b7d2910b9cff7803991a6a18e080e3c71f7a\nnode_hash=9ca6b461d616cf790a32a967574087298abb4cd0c3da938b7fed143b7d92b5ec\n" +} +` + +func TestParseVerification(t *testing.T) { + var v Verification + + if err := v.FromJSON([]byte(verificationProofJSON)); err != nil { + t.Fatal(err) + } +} + +func TestVerifySignature(t *testing.T) { + var v Verification + + udi := []byte{0, 1, 2, 3, 4, 5, 6, 7} + + // Create test vendor keys + var vendorKeys vendorkey.VendorKeys + + seed := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + + privKey := ed25519.NewKeyFromSeed(seed) + + var pubKey vendorkey.PubKey + copy(pubKey.PubKey[:], privKey[32:]) + + vendorKeys.Keys = map[string]vendorkey.PubKey{ + "key": pubKey, + } + + fwDigest, err := hex.DecodeString("3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623") + if err != nil { + t.Fatal(err) + } + + // Build a message. + // Let's just reuse the test vendor pubkey as the signer's pubkey + msg, err := util.BuildMessage(udi, fwDigest, pubKey.PubKey[:]) + if err != nil { + t.Fatal(err) + } + + // Keep to create test data. Sign and output both key pair and signature: + // sig := ed25519.Sign(privKey, msg) + // fmt.Printf("privkey: %x\npubkey: %x\nsig: %x\ndigest: %x\n", privKey, pubKey.PubKey, sig, sha256.Sum256(msg)) + + if err = v.FromJSON([]byte(verificationSigJSON)); err != nil { + t.Fatal(err) + } + + _, err = v.VerifySig(msg, vendorKeys) + if err != nil { + t.Fatal("vendor signature verification failed") + } +} + +func TestVerifyProofRawHash(t *testing.T) { + var v Verification + var log sigsum.Log + + if err := log.FromString(submitKey, policyStr); err != nil { + t.Fatal(err) + } + + digest, err := sumcrypto.HashFromHex("2291327fbadd2b3b8f8c0c005426700ad9139425a52a9140679e89b3b65c359b") + if err != nil { + t.Fatal(err) + } + + if err := v.FromJSON([]byte(verificationProofJSON)); err != nil { + t.Fatal(err) + } + + if _, err := v.VerifyProofDigest(digest, log); err != nil { + fmt.Printf("err %v\n", err) + t.Fatal("vendor signature not verified") + } +} + +func TestVerifyProof(t *testing.T) { + var v Verification + var log sigsum.Log + + if err := log.FromString(submitKey, policyStr); err != nil { + t.Fatal(err) + } + + if err := v.FromJSON([]byte(verificationProofJSON)); err != nil { + t.Fatal(err) + } + + // Build a message + + udi := []byte{0, 1, 2, 3, 4, 5, 6, 7} + + fwDigest, err := hex.DecodeString("3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623") + if err != nil { + t.Fatal(err) + } + + signerPubKey, err := hex.DecodeString("3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29") + if err != nil { + t.Fatal(err) + } + + msg, err := util.BuildMessage(udi, fwDigest, signerPubKey) + if err != nil { + t.Fatal(err) + } + + if _, err := v.VerifyProof(msg, log); err != nil { + t.Fatal("vendor signature not verified") + } +} + +// TestVerifyProofWrongTime verifies a proof with a pubkey that is end +// of life. Verification should fail. +func TestVerifyProofKeyLifetime(t *testing.T) { + var v Verification + var log sigsum.Log + + if err := log.FromString(oldSubmitKey, policyStr); err != nil { + t.Fatal(err) + } + + if err := v.FromJSON([]byte(verificationProofJSON)); err != nil { + t.Fatal(err) + } + + // Build a message + + udi := []byte{0, 1, 2, 3, 4, 5, 6, 7} + + fwDigest, err := hex.DecodeString("3769540390ee3d990ea3f9e4cc9a0d1af5bcaebb82218185a78c39c6bf01d9cdc305ba253a1fb9f3f9fcc63d97c8e5f34bbb1f7bec56a8f246f1d2239867b623") + if err != nil { + t.Fatal(err) + } + + signerPubKey, err := hex.DecodeString("3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29") + if err != nil { + t.Fatal(err) + } + + msg, err := util.BuildMessage(udi, fwDigest, signerPubKey) + if err != nil { + t.Fatal(err) + } + + _, err = v.VerifyProof(msg, log) + assertErrorMsgStartsWith(t, err, "witness cosignature outside of lifetime") +} + +func assertErrorMsgStartsWith(t *testing.T, err error, want string) { + t.Helper() + + if err == nil { + t.Log("Expected error") + t.Fail() + return + } + + if !strings.HasPrefix(err.Error(), want) { + t.Logf("Unexpected error '%v', should start with '%v'", err, want) + t.Fail() + } +} diff --git a/signing-procedure.svg b/signing-procedure.svg index 2e90689..8256bb4 100644 --- a/signing-procedure.svg +++ b/signing-procedure.svg @@ -1,3 +1,3 @@ -UDI,Firmware HashPublic Key,Signer App Tag,Signer App HashProvisioning StationVerification File =UDI,Timestamp,Signer App Tag,Signer App Hash,Vendor Signaturemsg =UDI |Firmware Hash |Public KeySigning ServerVendor Signature =sign(msg)Vendor Signing TKeyUSB DriveVerification FileUSB DriveVerification FileComputer withInternet Accesstkey.tillitis.se/verify/<UDI>UDI,Firmware Hash,Public KeyProvisioned TKeyPublish Verification FileGenerate Verification File \ No newline at end of file +UDI,Firmware HashPublic Key,Signer App Tag,Signer App HashProvisioning StationSubmit Request File =UDI,Timestamp,Signer App Tag,Signer App Hash,Sigsum 'add-leaf' Requestmsg =UDI |Firmware Hash |Public KeySigner ServerVendor Signature =sign(msg)Vendor Signing TKeyUSB DriveGenerate Submit Request FileSubmit Request FileUSB DriveVerification File =UDI,Timestamp,Signer App Tag,Signer App Hash,Sigsum ProofSigsum'add-leaf'RequestComputer withInternet Accesstkey.tillitis.se/verify/<UDI>UDI,Firmware HashPublic KeyProvisioned TKeyPublish Submit Request File and Verification FileSigsumProofSigsum LogMonitor \ No newline at end of file diff --git a/test-vendor-signing-pubkeys.txt b/test-vendor-signing-pubkeys.txt deleted file mode 100644 index 6ec09d0..0000000 --- a/test-vendor-signing-pubkeys.txt +++ /dev/null @@ -1 +0,0 @@ -50d9a125f51d85ffa1fb12011bdae05d39e03cda2a35d0daf3077072daabbb10 verisigner-v0.0.3 f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142 diff --git a/tkey-verification.yaml.example-remote-sign b/tkey-verification.yaml.example-remote-sign index 1c78e92..74b1a32 100644 --- a/tkey-verification.yaml.example-remote-sign +++ b/tkey-verification.yaml.example-remote-sign @@ -5,4 +5,4 @@ clientkey: "certs/client.key" server: "localhost:1337" # Hash digest of signing device app for device signature during provisioning. -signingapphash: "cd3c4f433f84648428113bd0a0cc407b2150e925a51b478006321e5a903c1638ce807138d1cc1f8f03cfb6236a87de0febde3ce0ddf177208e5483d1c169bac4" +signingapphash: "3493a20868b99897f0caff2f7b57b35ea3565c59f5ba353439d444cf8aac07e4899f260b98cb13f2fbb1bf6416ba8e2083d537391c1495ed1362c9008b7f8dff" diff --git a/tkey-verification.yaml.example-serve-signer b/tkey-verification.yaml.example-serve-signer index e91059d..a318add 100644 --- a/tkey-verification.yaml.example-serve-signer +++ b/tkey-verification.yaml.example-serve-signer @@ -4,5 +4,4 @@ servercert: "certs/server.crt" serverkey: "certs/server.key" listen: "localhost:1337" -# SHA-512 digest of the app used for vendor signing -vendorapphash: "f8ecdcda53a296636a0297c250b27fb649860645626cc8ad935eabb4c43ea3e1841c40300544fade4189aa4143c1ca8fe82361e3d874b42b0e2404793a170142" +activekey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDZoSX1HYX/ofsSARva4F054DzaKjXQ2vMHcHLaq7sQ sigsum key" diff --git a/tools/spdx-ensure b/tools/spdx-ensure deleted file mode 100755 index 5b0598b..0000000 --- a/tools/spdx-ensure +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: 2022 Tillitis AB -# SPDX-License-Identifier: BSD-2-Clause - -set -eu - -# Check for the SPDX tag in all files in the repo. Exit with a non-zero code if -# some is missing. The missingok arrays below contain files and directories -# with files where the the tag is not required. - -cd "${0%/*}" -cd .. - -tag="SPDX-License-Identifier:" - -missingok_dirs=( - certs - LICENSES -) - -missingok_files=( -.editorconfig -.gitignore -.github/workflows/ci.yaml -.github/workflows/golangci-lint.yml -.golangci.yml -.goreleaser.yaml -LICENSE -Makefile -README.md -RELEASE.md -REUSE.toml -go.mod -go.sum -gon.hcl -.clang-format -cmd/tkey-verification/bins/README.md -cmd/tkey-verification/bins/signer-v1.0.1.bin -cmd/tkey-verification/bins/signer-v1.0.1.bin.sha512 -cmd/tkey-verification/bins/verisigner-v0.0.3.bin -cmd/tkey-verification/bins/verisigner-v0.0.3.bin.deps -cmd/tkey-verification/bins/verisigner-v0.0.3.bin.sha512 -cmd/tkey-verification/vendor-signing-pubkeys.txt -release-builds/tkey-verification_0.0.2_linux-amd64.sha512 -release-builds/tkey-verification_0.0.2_macos-amd64.sha512 -release-builds/tkey-verification_0.0.2_macos-arm64.sha512 -release-builds/tkey-verification_0.0.2_macos-universal.sha512 -release-builds/tkey-verification_0.0.2_windows-amd64.exe.sha512 -release-builds/tkey-verification_0.0.3_linux-amd64.sha512 -release-builds/tkey-verification_0.0.3_macos-amd64.sha512 -release-builds/tkey-verification_0.0.3_macos-arm64.sha512 -release-builds/tkey-verification_0.0.3_macos-universal.sha512 -release-builds/tkey-verification_0.0.3_windows-amd64.exe.sha512 -doc/implementation-notes.md -doc/tkey-verification.1 -doc/tkey-verification.scd -test-vendor-signing-pubkeys.txt -tkey-verification.yaml.example-remote-sign -tkey-verification.yaml.example-serve-signer -signing-procedure.svg -) - -is_missingok() { - item="$1" - # ok for empty files - [[ -f "$item" ]] && [[ ! -s "$item" ]] && return 0 - for fileok in "${missingok_files[@]}"; do - [[ "$item" = "$fileok" ]] && return 0 - done - for dirok in "${missingok_dirs[@]}"; do - [[ "$item" =~ ^$dirok ]] && return 0 - done - return 1 -} - -printf "* Checking for SPDX tags in %s\n" "$PWD" - -mapfile -t repofiles < <(git ls-files || true) -if [[ -z "${repofiles[*]}" ]]; then - printf "* No files in the repo?!\n" - exit 1 -fi - -failed=0 - -printed=0 -for fileok in "${missingok_files[@]}"; do - [[ -f "$fileok" ]] && continue - if (( !printed )); then - printf "* Some files in missingok_files are themselves missing:\n" - printed=1 - failed=1 - fi - printf "%s\n" "$fileok" -done - -printed=0 -for dirok in "${missingok_dirs[@]}"; do - [[ -d "$dirok" ]] && continue - if (( !printed )); then - printf "* Some dirs in missingok_dirs are themselves missing:\n" - printed=1 - failed=1 - fi - printf "%s\n" "$dirok" -done - -printed=0 -for file in "${repofiles[@]}"; do - is_missingok "$file" && continue - if ! grep -q "$tag" "$file"; then - if (( !printed )); then - printf "* Files missing the SPDX tag:\n" - printed=1 - failed=1 - fi - printf "%s\n" "$file" - fi -done - -exit "$failed"