more robust - find USB hub on own, build docker image at runtime
This commit is contained in:
parent
83e6f9074c
commit
fd265f0023
7 changed files with 778 additions and 88 deletions
|
|
@ -5,8 +5,10 @@ FROM docker:28.3.1-dind-alpine3.22
|
|||
|
||||
# This is an alpine-based image
|
||||
|
||||
# The HSM's power is switched through the kernel's sysfs port interface (see
|
||||
# entrypoint.sh), so uhubctl is not needed
|
||||
RUN true && \
|
||||
apk add --no-cache curl uhubctl && \
|
||||
apk add --no-cache curl && \
|
||||
apkArch="$(arch)" && \
|
||||
if [ $apkArch = "x86_64" ]; then apkArch=amd64; fi && \
|
||||
curl -sLO https://github.com/sigstore/rekor/releases/download/v1.0.1/rekor-cli-linux-${apkArch} && \
|
||||
|
|
@ -15,5 +17,8 @@ RUN true && \
|
|||
true
|
||||
|
||||
COPY entrypoint.sh /
|
||||
# The signer's build context, built on the host daemon at run time so its
|
||||
# layers are cached there between runs
|
||||
COPY signer /signer
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
|
|
|||
209
README.md
209
README.md
|
|
@ -1,42 +1,149 @@
|
|||
Signs files using an HSM
|
||||
========================
|
||||
Signs with an HSM
|
||||
================
|
||||
|
||||
Basic Usage
|
||||
A docker action that signs with a key that never leaves an HSM attached to the
|
||||
runner host, reached through the host's pcscd. It can:
|
||||
|
||||
* make **detached signatures** of files (RSA PKCS#1 v1.5 over SHA-256),
|
||||
optionally logged to the [sigstore public transparency log](https://sigstore.dev)
|
||||
* **sign an APK** (APK signature schemes v2 and v3) with `apksigner`
|
||||
|
||||
or both in one step: the APK is signed first, so the files to sign can include
|
||||
the signed APK.
|
||||
|
||||
Detached signatures
|
||||
-------------------
|
||||
|
||||
```yaml
|
||||
- name: Sign
|
||||
id: sign
|
||||
uses: https://git.lerch.org/lobo/action-hsm-sign@v3
|
||||
with:
|
||||
pin: ${{ secrets.HSM_USER_PIN }}
|
||||
files: dist/*
|
||||
public_key: 'https://emil.lerch.org/serverpublic.pem'
|
||||
```
|
||||
|
||||
`files` is a glob of files in one directory, or `dir/**` for every file under
|
||||
`dir`. Each signature is written beside its file, as `FILE.sig`.
|
||||
|
||||
If a public key is specified, [rekor](https://github.com/sigstore/rekor) will
|
||||
be invoked, sending the signature to the sigstore public transparency log. The
|
||||
signatures are deterministic, so signing the same file again produces the same
|
||||
signature, and rekor reports the existing entry.
|
||||
|
||||
The action provides the following outputs:
|
||||
|
||||
* `SOURCE_n`: Source file used for the signature
|
||||
* `SIG_n`: Signature
|
||||
* `URL_n`: If a public key is specified, the sigstore log url
|
||||
|
||||
Because multiple files can be signed, these outputs have numerical suffixes,
|
||||
starting at 0, in the order of the sorted file names. In the above example, the
|
||||
output `${{ steps.sign.outputs.URL_0 }}` would be the url for the first file
|
||||
signed with this action
|
||||
|
||||
APK signing
|
||||
-----------
|
||||
|
||||
```yaml
|
||||
- name: Sign
|
||||
id: sign
|
||||
uses: https://git.lerch.org/lobo/action-hsm-sign@v1
|
||||
uses: https://git.lerch.org/lobo/action-hsm-sign@v3
|
||||
with:
|
||||
pin: ${{ secrets.HSM_USER_PIN }}
|
||||
files: ???
|
||||
apk: unsigned/app-release-unsigned.apk
|
||||
apk_output: dist/app.apk
|
||||
# Optional: detached signatures of everything in dist/, the signed APK
|
||||
# included
|
||||
files: dist/*
|
||||
public_key: 'https://emil.lerch.org/serverpublic.pem'
|
||||
```
|
||||
|
||||
If a public key is specified, [rekor](https://github.com/sigstore/rekor) will
|
||||
be invoked, sending the signature to the [sigstore public transparency
|
||||
log](https://sigstore.dev).
|
||||
The APK must be zipaligned (AGP's release output already is).
|
||||
|
||||
The action provides the following outputs:
|
||||
An APK signature carries a certificate, and Java (which `apksigner` runs on)
|
||||
only offers a PKCS#11 key that has a certificate on the token. **Once, before
|
||||
the first APK is signed**, make a self-signed certificate for the key and store
|
||||
it beside the key. On the HSM host, with the HSM powered, from a clone of this
|
||||
repository:
|
||||
|
||||
* Source: Source file used for the signature
|
||||
* Signature: Signature
|
||||
* URL: If a public key is specified, the URL output provides the sigstore log url
|
||||
```sh
|
||||
docker build -t hsm-signer signer
|
||||
read -rs PKCS11_PIN && export PKCS11_PIN
|
||||
docker run --rm -v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm:ro \
|
||||
-e PKCS11_PIN hsm-signer make-cert '/CN=My App/O=example.org'
|
||||
```
|
||||
|
||||
Because multiple files can be signed, these outputs have numerical suffixes.
|
||||
In the above example, the output `${{ steps.sign.outputs.URL_1 }}` would be the
|
||||
url for the first file signed with this action
|
||||
It prints the certificate; keep a copy. The certificate is the app's identity
|
||||
on every device from then on: replacing it means users uninstall and reinstall.
|
||||
Its 30-year validity does not matter for APKs, whose certificate dates Android
|
||||
does not check. Running the same container with `list` instead of `make-cert`
|
||||
shows what Java sees on the token.
|
||||
|
||||
Inputs
|
||||
------
|
||||
|
||||
| input | default | |
|
||||
|----------------|---------|--------------------------------------------------------------------------|
|
||||
| `files` | | files to make detached signatures of |
|
||||
| `apk` | | unsigned APK to sign |
|
||||
| `apk_output` | | where to write the signed APK; required with `apk` |
|
||||
| `pin` | | the HSM's user PIN (required) |
|
||||
| `key_id` | `03` | the key's CKA_ID, in hex |
|
||||
| `slot_index` | `0` | PKCS#11 slot, by position in the slot list |
|
||||
| `key_alias` | | APK only: which key, when the token has more than one with a certificate |
|
||||
| `public_key` | | URL of the PEM public key; set it to log detached signatures to sigstore |
|
||||
| `uhub_control` | `false` | power cycle the HSM on a smart USB hub (below) |
|
||||
|
||||
At least one of `files` and `apk` is required.
|
||||
|
||||
How it works
|
||||
------------
|
||||
|
||||
The HSM is only reachable from the host, through pcscd's socket, and this
|
||||
action's container is not the host. So the action builds the signer image
|
||||
([signer/](signer)) on the host's docker daemon, and starts a signer container
|
||||
beside itself for each signature, with `/run/pcscd/pcscd.comm` mounted. A
|
||||
volume mount would name a host path, so inputs and results travel by
|
||||
`docker cp`.
|
||||
|
||||
The signer image is tagged `action-hsm-sign-signer:<hash of signer/>` and only
|
||||
built when that tag is missing, so the first run with a new version of
|
||||
[signer/](signer) builds it (a few minutes, from the Debian mirrors) and later
|
||||
runs reuse it, without needing a build at all. It is not rebuilt for updates to
|
||||
its Debian base: remove the image (`docker image rm`), and the next run builds
|
||||
it afresh. Old versions' images stay on the host until removed.
|
||||
|
||||
The signer is Debian with opensc, which reaches the HSM through pcscd, plus
|
||||
Debian's `apksigner` and OpenSSL's PKCS#11 engine. Its opensc and pcsc-lite
|
||||
have to speak the host pcscd's protocol, so the signer image should follow the
|
||||
host's Debian release (`BASE` in [signer/Dockerfile](signer/Dockerfile)).
|
||||
|
||||
[signer/test.sh](signer/test.sh) runs the signer end to end against SoftHSM,
|
||||
with docker or podman: a token with a bare key, a detached signature checked
|
||||
with openssl against the token's public key and, given an APK, `make-cert`,
|
||||
APK signing, `apksigner verify`, and a check that the APK's signing key is the
|
||||
token's:
|
||||
|
||||
```sh
|
||||
signer/test.sh # detached only
|
||||
signer/test.sh app-release-unsigned.apk [signed.apk]
|
||||
```
|
||||
|
||||
What it cannot cover is the HSM itself and the host's pcscd.
|
||||
|
||||
Usage with Smart USB Hubs
|
||||
-------------------------
|
||||
|
||||
Many consumer HSMs will "hang" after prolonged usage. To alleviate problems
|
||||
associated with this, this action can integrate with smart USB hubs to turn
|
||||
on the hub's port and wait for the OS to recognize the attached HSM before
|
||||
performing the signing action.
|
||||
associated with this, this action can integrate with smart USB hubs (hubs with
|
||||
per-port power switching) to power cycle the hub's port and wait for the OS to
|
||||
recognize the attached HSM before performing the signing action. Power cycling
|
||||
also drops any other process on the host that has the HSM open exclusively.
|
||||
|
||||
**NOTE: The action will turn off the port on the USB hub when it is done processing**
|
||||
**NOTE: The action will turn off the port on the USB hub when it is done
|
||||
processing, including when signing fails or the job is cancelled**
|
||||
|
||||
To enable this feature, set `uhub_control` to `true`. As this is controlling
|
||||
physical hardware, you will also need a runner set with a max concurrency of 1
|
||||
|
|
@ -53,19 +160,69 @@ jobs:
|
|||
runs-on: ubuntu-latest-with-hsm
|
||||
```
|
||||
|
||||
The runner will also need to set environment variables `UHUB_PORT` and
|
||||
`UHUB_LOCATION` as appropriate. To determine the proper values for these, it is
|
||||
best to consult [uhubctl
|
||||
documentation](https://github.com/mvp/uhubctl?tab=readme-ov-file#usage) and run
|
||||
some command line tests. Updating the previous example:
|
||||
The port is switched through the kernel's sysfs interface
|
||||
(`/sys/bus/usb/devices/<hub>:1.0/<hub>-port<N>/disable`), so the runner host
|
||||
needs Linux 6.0 or later, and the runner must run containers privileged
|
||||
(`container.privileged: true` in the runner config).
|
||||
|
||||
The runner also needs environment variables (`runner.envs` in the runner
|
||||
config) that say where the HSM is:
|
||||
|
||||
* `UHUB_PORT` (required): the hub port the HSM is plugged into
|
||||
* `UHUB_ID` (recommended): the hub's `vid:pid`, as shown by `lsusb`. The hub's
|
||||
location is looked up at run time, so the hub can be moved to another USB
|
||||
port on the host without a config change
|
||||
* `UHUB_LOCATION` (optional): the hub's location (e.g. `1-1.4`, as shown by
|
||||
`lsusb -t` or `ls /sys/bus/usb/devices`). Takes precedence over `UHUB_ID`
|
||||
|
||||
If neither `UHUB_ID` nor `UHUB_LOCATION` is set, the action uses the one hub
|
||||
with a smartcard device on `UHUB_PORT` or, if there is none, the one hub whose
|
||||
port `UHUB_PORT` is powered off. When the hub cannot be found, the action fails
|
||||
and lists every hub it can see. For example:
|
||||
|
||||
```yaml
|
||||
runner:
|
||||
capacity: 1
|
||||
envs:
|
||||
UHUB_PORT: 4
|
||||
UHUB_ID: '366b:0004'
|
||||
```
|
||||
|
||||
Updating the first example:
|
||||
|
||||
```yaml
|
||||
- name: Sign
|
||||
id: sign
|
||||
uses: https://git.lerch.org/lobo/action-hsm-sign@v1
|
||||
uses: https://git.lerch.org/lobo/action-hsm-sign@v3
|
||||
with:
|
||||
pin: ${{ secrets.HSM_USER_PIN }}
|
||||
files: ???
|
||||
files: dist/*
|
||||
public_key: 'https://emil.lerch.org/serverpublic.pem'
|
||||
uhub_control: 'true'
|
||||
```
|
||||
|
||||
If you need to use the HSM by hand while its port is powered off, a privileged
|
||||
container can switch it on (adjust hub location and port):
|
||||
|
||||
```sh
|
||||
docker run --rm --privileged alpine sh -c \
|
||||
'echo 0 > /sys/bus/usb/devices/1-1.4:1.0/1-1.4-port4/disable'
|
||||
```
|
||||
|
||||
Other users of the HSM on the runner host must not hold it open exclusively. In
|
||||
particular, GnuPG's `scdaemon` opens smartcard readers exclusively and keeps
|
||||
them open, so on the runner host put `disable-scdaemon` in
|
||||
`~/.gnupg/gpg-agent.conf` (or `pcsc-shared` in `~/.gnupg/scdaemon.conf`).
|
||||
|
||||
Upgrading from v2
|
||||
-----------------
|
||||
|
||||
Detached signing is unchanged for callers: `files`, `pin`, `public_key` and
|
||||
`uhub_control` work as before, and the signatures are the same. Changes:
|
||||
|
||||
* `slot` (which was the key's id) is now `key_id`, default `03`
|
||||
* `files` is no longer required, as long as `apk` is set
|
||||
* the signer is built from [signer/](signer) instead of pulling
|
||||
`git.lerch.org/lobo/pkcs11:1`
|
||||
* hub control finds the hub at run time (`UHUB_ID`), and needs Linux 6.0 or
|
||||
later
|
||||
|
|
|
|||
32
action.yml
32
action.yml
|
|
@ -1,22 +1,36 @@
|
|||
name: 'HSM Signing'
|
||||
description: 'Signs using HSM'
|
||||
description: 'Signs APKs and makes detached signatures of files, with a key held in an HSM'
|
||||
author: 'lobo'
|
||||
inputs:
|
||||
files:
|
||||
description: 'Files to sign'
|
||||
required: true
|
||||
user_pin:
|
||||
description: 'Files to make detached signatures of (a glob in one directory, or dir/** for all files under dir). Signatures are written beside them as FILE.sig'
|
||||
required: false
|
||||
apk:
|
||||
description: 'An unsigned, zipaligned APK to sign (signature schemes v2 and v3). Signed before files, so files can include apk_output'
|
||||
required: false
|
||||
apk_output:
|
||||
description: 'Where to write the signed APK. Required with apk'
|
||||
required: false
|
||||
pin:
|
||||
description: 'User pin for HSM on build server'
|
||||
required: true
|
||||
slot:
|
||||
description: 'HSM slot used for signing'
|
||||
key_id:
|
||||
description: 'CKA_ID of the key, in hex'
|
||||
required: true
|
||||
default: 3
|
||||
default: '03'
|
||||
slot_index:
|
||||
description: 'PKCS#11 slot, by position in the slot list'
|
||||
required: true
|
||||
default: '0'
|
||||
key_alias:
|
||||
description: 'APK only: the key alias, when the token holds more than one key with a certificate'
|
||||
required: false
|
||||
default: ''
|
||||
public_key:
|
||||
description: 'URL to PEM format public key. Specify only if uploading to sigstore'
|
||||
description: 'URL to PEM format public key. Specify only if uploading detached signatures to sigstore'
|
||||
required: false
|
||||
uhub_control:
|
||||
description: 'If HSM is attached to software controlled power hub, setting this to "true" will power on the HSM during operation'
|
||||
description: 'If HSM is attached to software controlled power hub, setting this to "true" will power cycle the HSM before signing and power it off afterwards. The runner needs UHUB_PORT set (see README)'
|
||||
required: true
|
||||
default: "false"
|
||||
runs:
|
||||
|
|
|
|||
363
entrypoint.sh
363
entrypoint.sh
|
|
@ -1,64 +1,328 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Sign with the HSM: an APK (input apk), detached signatures of files (input files),
|
||||
# or both, APK first, so a files glob can include the signed APK. Builds the signer
|
||||
# image (./signer) on the host's docker daemon, powers the HSM on if asked, and for
|
||||
# each signature starts a signer container beside this one with the host's pcscd
|
||||
# socket, copying the input in and the result out. Detached signatures can also be
|
||||
# logged to sigstore.
|
||||
#
|
||||
# There is no concurrency control here. We are relying on the fact that
|
||||
# the runner on the host is set to a max capacity of 1
|
||||
if [ "${INPUT_UHUB_CONTROL}" != "false" ]; then
|
||||
if [ -z "${UHUB_LOCATION}" ] || [ -z "${UHUB_PORT}" ]; then
|
||||
echo "error: UHUB control requested, but runner has not been configured with UHUB_LOCATION and UHUB_PORT environment variables"
|
||||
exit 255
|
||||
fi
|
||||
uhubctl -a off -p "${UHUB_PORT}" -l "${UHUB_LOCATION}" # Off seems to be reflected immediately
|
||||
# Capture the number of hidraw devices with the port off
|
||||
# The way docker works, we can't seem to monitor /dev directory
|
||||
# But a USB device should show up in dmesg log when this happens
|
||||
#devs="$(find /dev -maxdepth 1 -name 'hi*' |wc -l)"
|
||||
devs=$(dmesg |grep "usb ${UHUB_LOCATION}.${UHUB_PORT}" |grep -c "New USB device found")
|
||||
uhubctl -a on -p "${UHUB_PORT}" -l "${UHUB_LOCATION}"
|
||||
retries=0
|
||||
while [ "$(dmesg |grep "usb ${UHUB_LOCATION}.${UHUB_PORT}" |grep -c "New USB device found")" = "$devs" ] && [ $retries -lt 10 ]; do
|
||||
# Generally takes a few seconds to settle in
|
||||
echo "waiting for device connection ($((retries+1)) / 10)"
|
||||
sleep 1
|
||||
retries=$((retries+1))
|
||||
|
||||
# --- HSM power control -------------------------------------------------------
|
||||
#
|
||||
# The HSM is on port UHUB_PORT of a hub that can switch each port's power. The
|
||||
# kernel names the port by the hub's location (1-1.4 and the like), which
|
||||
# changes when the hub is plugged in somewhere else, so it is found at run time:
|
||||
#
|
||||
# UHUB_LOCATION if set, used as given (checked to be a hub with that port)
|
||||
# UHUB_ID else, if set: the hub with this vid:pid (as lsusb shows it)
|
||||
# neither the one hub with a smartcard on port UHUB_PORT or, failing
|
||||
# that, the one hub whose port UHUB_PORT has no power
|
||||
#
|
||||
# Docker gives a container a copy of /dev made when it starts, so devices coming
|
||||
# and going cannot be seen there. /sys/bus/usb is live, so that is where this
|
||||
# watches the HSM go away and come back. Switching the power needs Linux 6.0 or
|
||||
# later and a privileged container, for /sys to be writable.
|
||||
|
||||
uhub_sys=/sys/bus/usb/devices
|
||||
uhub_location=""
|
||||
uhub_port=""
|
||||
uhub_switched=false
|
||||
|
||||
uhub_die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The sysfs directory of port $2 of the hub at location $1. A root hub's
|
||||
# location is its bus number: location 1 is device usb1, ports under 1-0:1.0.
|
||||
uhub_port_dir() {
|
||||
case "$1" in
|
||||
*-*) echo "${uhub_sys}/$1:1.0/$1-port$2" ;;
|
||||
*) echo "${uhub_sys}/$1-0:1.0/usb$1-port$2" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The location of a hub, from its device directory (1-1.4, usb1).
|
||||
uhub_location_of_hub() {
|
||||
_ul="$(basename "$1")"
|
||||
echo "${_ul#usb}"
|
||||
}
|
||||
|
||||
# The location of the hub a port directory belongs to.
|
||||
uhub_location_of_port() {
|
||||
_ul="$(basename "$1")"
|
||||
_ul="${_ul%-port*}"
|
||||
echo "${_ul#usb}"
|
||||
}
|
||||
|
||||
uhub_is_hub() {
|
||||
[ "$(cat "$1/bDeviceClass" 2>/dev/null)" = 09 ]
|
||||
}
|
||||
|
||||
uhub_attached() {
|
||||
[ -e "$1/device" ]
|
||||
}
|
||||
|
||||
uhub_detached() {
|
||||
! uhub_attached "$1"
|
||||
}
|
||||
|
||||
# Whether the device on a port has a smartcard (CCID, class 0b) interface.
|
||||
uhub_smartcard() {
|
||||
for _uc in "$1"/device/*:*/bInterfaceClass; do
|
||||
if [ "$(cat "$_uc" 2>/dev/null)" = 0b ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
if [ $retries -ge 10 ]; then
|
||||
echo "device is not available. Aborting"
|
||||
exit 1
|
||||
return 1
|
||||
}
|
||||
|
||||
# The kernel's view of the port's power (Linux 6.0 and later).
|
||||
uhub_unpowered() {
|
||||
[ "$(cat "$1/disable" 2>/dev/null)" = 1 ]
|
||||
}
|
||||
|
||||
# Every hub, and what is on its port UHUB_PORT. For error messages.
|
||||
uhub_list() {
|
||||
echo "hubs (location vid:pid product: port ${UHUB_PORT}):"
|
||||
for _ud in "${uhub_sys}"/*; do
|
||||
if uhub_is_hub "$_ud"; then
|
||||
_ul="$(uhub_location_of_hub "$_ud")"
|
||||
_up="$(uhub_port_dir "$_ul" "${UHUB_PORT}")"
|
||||
if [ ! -d "$_up" ]; then
|
||||
_us="no such port"
|
||||
elif uhub_smartcard "$_up"; then
|
||||
_us="smartcard"
|
||||
elif uhub_attached "$_up"; then
|
||||
_us="another device"
|
||||
elif uhub_unpowered "$_up"; then
|
||||
_us="no power"
|
||||
else
|
||||
_us="nothing attached"
|
||||
fi
|
||||
echo " ${_ul} $(cat "$_ud/idVendor"):$(cat "$_ud/idProduct") $(cat "$_ud/product" 2>/dev/null): ${_us}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Sets uhub_location and uhub_port, as described at the top of this block.
|
||||
uhub_find() {
|
||||
_uhubs=""
|
||||
if [ -n "${UHUB_LOCATION:-}" ]; then
|
||||
_uby="UHUB_LOCATION=${UHUB_LOCATION}"
|
||||
_uhubs="${UHUB_LOCATION}"
|
||||
elif [ -n "${UHUB_ID:-}" ]; then
|
||||
_uby="UHUB_ID=${UHUB_ID}"
|
||||
_uid="$(echo "${UHUB_ID}" | tr 'A-F' 'a-f')"
|
||||
for _ud in "${uhub_sys}"/*; do
|
||||
if uhub_is_hub "$_ud" && [ "$(cat "$_ud/idVendor"):$(cat "$_ud/idProduct")" = "${_uid}" ]; then
|
||||
_uhubs="${_uhubs} $(uhub_location_of_hub "$_ud")"
|
||||
fi
|
||||
done
|
||||
else
|
||||
_uby="a smartcard on port ${UHUB_PORT}"
|
||||
for _up in "${uhub_sys}"/*/*-port"${UHUB_PORT}"; do
|
||||
if uhub_smartcard "$_up"; then
|
||||
_uhubs="${_uhubs} $(uhub_location_of_port "$_up")"
|
||||
fi
|
||||
done
|
||||
if [ -z "${_uhubs}" ]; then
|
||||
_uby="port ${UHUB_PORT} having no power"
|
||||
for _up in "${uhub_sys}"/*/*-port"${UHUB_PORT}"; do
|
||||
if uhub_detached "$_up" && uhub_unpowered "$_up"; then
|
||||
_uhubs="${_uhubs} $(uhub_location_of_port "$_up")"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
# shellcheck disable=SC2086 # one word per hub
|
||||
set -- ${_uhubs}
|
||||
if [ $# -eq 0 ]; then
|
||||
uhub_list >&2
|
||||
uhub_die "no hub found by ${_uby}"
|
||||
fi
|
||||
if [ $# -gt 1 ]; then
|
||||
uhub_list >&2
|
||||
uhub_die "more than one hub found by ${_uby} ($*): set UHUB_ID or UHUB_LOCATION on the runner"
|
||||
fi
|
||||
if [ ! -d "$(uhub_port_dir "$1" "${UHUB_PORT}")" ]; then
|
||||
uhub_list >&2
|
||||
uhub_die "$1 (from ${_uby}) is not a hub with a port ${UHUB_PORT}"
|
||||
fi
|
||||
uhub_location="$1"
|
||||
uhub_port="$(uhub_port_dir "${uhub_location}" "${UHUB_PORT}")"
|
||||
echo "HSM: port ${UHUB_PORT} of hub ${uhub_location}, found by ${_uby}"
|
||||
}
|
||||
|
||||
# Runs "$@" once a second until it succeeds, giving up after 10 tries.
|
||||
uhub_wait() {
|
||||
_uw="$1"
|
||||
shift
|
||||
_ui=0
|
||||
until "$@"; do
|
||||
_ui=$((_ui + 1))
|
||||
if [ "${_ui}" -gt 10 ]; then
|
||||
return 1
|
||||
fi
|
||||
echo "waiting for ${_uw} (${_ui} / 10)"
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# Switches the port off (1) or on (0) through the kernel's port "disable" file,
|
||||
# which disconnects the HSM or has it enumerated as it does so. Not uhubctl:
|
||||
# Alpine's is built without its sysfs support, so it only sends the hub the
|
||||
# request, and the kernel never hears that the HSM went away.
|
||||
uhub_power() {
|
||||
echo "$1" >"${uhub_port}/disable"
|
||||
}
|
||||
|
||||
uhub_is_off() {
|
||||
uhub_detached "${uhub_port}" && uhub_unpowered "${uhub_port}"
|
||||
}
|
||||
|
||||
# Power the HSM off and on again, and wait for it to connect.
|
||||
uhub_on() {
|
||||
if [ -z "${UHUB_PORT:-}" ]; then
|
||||
uhub_die "UHUB control requested, but the runner has no UHUB_PORT environment variable"
|
||||
fi
|
||||
uhub_find
|
||||
# From here on the port is switched off again when the script exits.
|
||||
uhub_switched=true
|
||||
echo "HSM: switching the port off"
|
||||
uhub_power 1 ||
|
||||
uhub_die "could not write ${uhub_port}/disable: this needs Linux 6.0 or later and a privileged container"
|
||||
# The kernel reads the power state back from the hub, so this also catches
|
||||
# hubs that take the request and leave the power on.
|
||||
uhub_wait "the port to switch off" uhub_is_off ||
|
||||
uhub_die "port ${UHUB_PORT} of hub ${uhub_location} still has power or a device after switching it off"
|
||||
# Long enough off for the HSM to reset.
|
||||
sleep 1
|
||||
echo "HSM: switching the port on"
|
||||
uhub_power 0 ||
|
||||
uhub_die "could not write ${uhub_port}/disable"
|
||||
uhub_wait "the HSM to connect" uhub_smartcard "${uhub_port}" ||
|
||||
uhub_die "no smartcard on port ${UHUB_PORT} of hub ${uhub_location} after switching it on"
|
||||
echo "HSM: connected"
|
||||
}
|
||||
|
||||
# Power the HSM off, if uhub_on switched it. For the exit trap.
|
||||
uhub_off() {
|
||||
if [ "${uhub_switched}" = true ]; then
|
||||
echo "HSM: switching the port off"
|
||||
uhub_power 1 ||
|
||||
echo "warning: could not switch off port ${UHUB_PORT} of hub ${uhub_location}" >&2
|
||||
fi
|
||||
}
|
||||
# --- end HSM power control ---------------------------------------------------
|
||||
|
||||
container=""
|
||||
cleanup() {
|
||||
if [ -n "${container}" ]; then
|
||||
docker rm -f "${container}" >/dev/null 2>&1
|
||||
fi
|
||||
# Whatever happened, so a failed run does not leave the HSM powered
|
||||
uhub_off
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Runs `hsm-sign COMMAND` in a signer container. We can't use a volume mount
|
||||
# because it will use the host volume, and we're not on the host, but in a
|
||||
# container. So we create the container, copy the input in as /home/user/NAME_IN,
|
||||
# run it, and copy /home/user/NAME_OUT back.
|
||||
# usage: run_signer COMMAND IN OUT NAME_IN NAME_OUT
|
||||
run_signer() {
|
||||
container="$(docker create \
|
||||
-v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm:ro \
|
||||
-e PKCS11_PIN -e PKCS11_SLOT_INDEX -e KEY_ID -e KEY_ALIAS \
|
||||
"${signer}" "$1" "/home/user/$4" "/home/user/$5")" ||
|
||||
die "could not create the signer container"
|
||||
docker cp "$2" "${container}:/home/user/$4" || die "could not copy $2 into the signer"
|
||||
# let container run, pick up the exit code. The exit trap removes the container
|
||||
docker start -a "${container}" || exit $?
|
||||
docker cp "${container}:/home/user/$5" "$3" || die "could not copy the result to $3"
|
||||
docker rm "${container}" >/dev/null
|
||||
container=""
|
||||
}
|
||||
|
||||
# Pass these through sort so we can have deterministic output indexing
|
||||
list_files() {
|
||||
dir="$(dirname "${INPUT_FILES}")"
|
||||
glob="$(basename "${INPUT_FILES}")"
|
||||
if [ "${glob}" = "**" ]; then
|
||||
find "$dir" -type f |sort
|
||||
else
|
||||
find "$dir" -maxdepth 1 -name "${glob}" -type f |sort
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "${INPUT_FILES:-}" ] && [ -z "${INPUT_APK:-}" ]; then
|
||||
die "nothing to sign: set files, apk, or both"
|
||||
fi
|
||||
if [ -n "${INPUT_APK:-}" ]; then
|
||||
[ -n "${INPUT_APK_OUTPUT:-}" ] || die "apk is set, but apk_output is not"
|
||||
[ -f "${INPUT_APK}" ] || die "no APK at ${INPUT_APK}"
|
||||
elif [ -n "${INPUT_FILES:-}" ]; then
|
||||
# With an APK to sign first, the glob is expanded after, to include it
|
||||
all_files="$(list_files)"
|
||||
[ -n "${all_files}" ] || die "no files match ${INPUT_FILES}"
|
||||
fi
|
||||
|
||||
# Before the HSM is powered, so it is not on through a cold build, and a
|
||||
# failed build does not cycle it. The image is tagged with a hash of its build
|
||||
# context and only built when that tag is missing: a build needs a buildkit
|
||||
# session with the host daemon, which times out when the host is busy, so a
|
||||
# run with an unchanged signer should not need one. (So the image is also not
|
||||
# rebuilt for base image updates: remove it, and the next run builds afresh.)
|
||||
signer="action-hsm-sign-signer:$(cd /signer && find . -type f -exec sha256sum {} + | sort -k 2 | sha256sum | cut -c1-12)"
|
||||
if docker image inspect "${signer}" >/dev/null 2>&1; then
|
||||
echo "Signer image ${signer} is already built"
|
||||
else
|
||||
echo "Building signer image ${signer}"
|
||||
docker build -q -t "${signer}" /signer || die "could not build the signer image"
|
||||
fi
|
||||
|
||||
# The PIN reaches the signer as an environment variable, by name: never an
|
||||
# argument
|
||||
export PKCS11_PIN="${INPUT_PIN:-}"
|
||||
export PKCS11_SLOT_INDEX="${INPUT_SLOT_INDEX:-0}"
|
||||
export KEY_ID="${INPUT_KEY_ID:-03}"
|
||||
export KEY_ALIAS="${INPUT_KEY_ALIAS:-}"
|
||||
|
||||
if [ "${INPUT_UHUB_CONTROL:-false}" != "false" ]; then
|
||||
uhub_on
|
||||
fi
|
||||
|
||||
if [ -n "${INPUT_APK:-}" ]; then
|
||||
echo "Signing APK ${INPUT_APK}. Signed APK destination: ${INPUT_APK_OUTPUT}"
|
||||
mkdir -p "$(dirname "${INPUT_APK_OUTPUT}")"
|
||||
run_signer apk "${INPUT_APK}" "${INPUT_APK_OUTPUT}" in.apk out.apk
|
||||
if [ -n "${INPUT_FILES:-}" ]; then
|
||||
all_files="$(list_files)"
|
||||
[ -n "${all_files}" ] || die "no files match ${INPUT_FILES}"
|
||||
fi
|
||||
fi
|
||||
|
||||
dir="$(dirname "${INPUT_FILES}")"
|
||||
glob="$(basename "${INPUT_FILES}")"
|
||||
# Pass these through sort so we can have deterministic output indexing
|
||||
if [ "${glob}" = "**" ]; then
|
||||
all_files="$(find "$dir" -type f |sort)"
|
||||
else
|
||||
all_files="$(find "$dir" -maxdepth 1 -name "${glob}" -type f |sort)"
|
||||
if [ -z "${INPUT_FILES:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
i=0
|
||||
while IFS= read -r f; do
|
||||
sign_dir="$(dirname "$f")"
|
||||
sign_file="$(basename "$f")"
|
||||
dest_sig="${sign_dir}/${sign_file}.sig"
|
||||
echo "Signing file $f. Signature file destination: ${dest_sig}"
|
||||
# We can't use a volume mount because it will use the host volume, and we're
|
||||
# not on the host, but in a container. So we'll create a container, copy
|
||||
# the file to sign in place, get the signature and copy that back
|
||||
container="$(docker create \
|
||||
-v /run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm:ro \
|
||||
-e INPUT_PIN \
|
||||
git.lerch.org/lobo/pkcs11:1 \
|
||||
-s --id "${INPUT_SLOT}" -m SHA256-RSA-PKCS -i artifact -o signature --pin env:INPUT_PIN)"
|
||||
docker cp "$f" "${container}":/home/user/artifact
|
||||
docker start -a "$container" # let container run, pick up the exit code
|
||||
ec=$?
|
||||
if [ $ec -ne 0 ]; then
|
||||
docker rm "$container"
|
||||
exit $ec
|
||||
fi
|
||||
# We are clear. Copy signature back into the workspace and remove the container
|
||||
docker cp "${container}":/home/user/signature "${dest_sig}"
|
||||
docker rm "${container}"
|
||||
run_signer detached "$f" "${dest_sig}" artifact signature
|
||||
if [ -n "${INPUT_PUBLIC_KEY}" ]; then
|
||||
echo "Public key url specified. Uploading to sigstore public transparency log"
|
||||
echo "Fetching key from ${INPUT_PUBLIC_KEY}"
|
||||
|
|
@ -77,8 +341,3 @@ while IFS= read -r f; do
|
|||
done <<ALLFILES_INPUT
|
||||
$all_files
|
||||
ALLFILES_INPUT
|
||||
|
||||
if [ "${INPUT_UHUB_CONTROL}" != "false" ]; then
|
||||
# Turn off the port when we're done
|
||||
uhubctl -a off -p "${UHUB_PORT}" -l "${UHUB_LOCATION}"
|
||||
fi
|
||||
|
|
|
|||
37
signer/Dockerfile
Normal file
37
signer/Dockerfile
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# The signer: the container that talks to the HSM, started beside the action's own
|
||||
# container by entrypoint.sh. See hsm-sign.sh for what it does.
|
||||
#
|
||||
# Debian with opensc, reaching the HSM through the host's pcscd socket (mounted at run
|
||||
# time) as an unprivileged uid 1000. Plus Debian's apksigner, which signs APKs through
|
||||
# Java's SunPKCS11 provider and so needs nothing but opensc's PKCS#11 module, and
|
||||
# OpenSSL's PKCS#11 engine, used once to make the certificate an APK signature has to
|
||||
# carry (hsm-sign.sh, make-cert).
|
||||
#
|
||||
# The action builds this on the host's docker daemon at run time, so the image always
|
||||
# matches the action's version, and the layers are cached there between runs. opensc
|
||||
# and pcsc-lite have to speak the host pcscd's socket protocol, so this follows the
|
||||
# host's Debian release (nas2: bookworm). That is the one thing here test.sh cannot show:
|
||||
# it uses SoftHSM, with no pcscd.
|
||||
#
|
||||
# BASE is an argument so the image can follow the host if it moves.
|
||||
ARG BASE=debian:bookworm
|
||||
FROM ${BASE}
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
opensc openjdk-17-jre-headless libapksig-java openssl libengine-pkcs11-openssl ca-certificates && \
|
||||
# Debian's apksigner package depends on the desktop JRE (default-jre: AWT, GTK, Mesa,
|
||||
# X11, icon themes - 115 packages instead of 42) though it is a command-line tool that
|
||||
# runs on the headless one. So its files are unpacked rather than the package
|
||||
# installed: one jar and its wrapper script, and the headless JRE and libapksig it
|
||||
# needs are installed above. dpkg's database never hears of it, so there is no
|
||||
# broken dependency left for a later apt-get to trip on.
|
||||
cd /tmp && apt-get download apksigner && dpkg-deb -x apksigner_*.deb / && rm apksigner_*.deb && \
|
||||
useradd -m -u 1000 user && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY hsm-sign.sh /usr/local/bin/hsm-sign
|
||||
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
ENTRYPOINT ["/usr/local/bin/hsm-sign"]
|
||||
130
signer/hsm-sign.sh
Executable file
130
signer/hsm-sign.sh
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
#!/bin/sh
|
||||
# Sign with a key held in an HSM, through PKCS#11.
|
||||
#
|
||||
# hsm-sign detached IN OUT a detached signature of IN (RSA PKCS#1 v1.5 over
|
||||
# SHA-256), written to OUT
|
||||
# hsm-sign apk IN.apk OUT.apk sign an APK (signature schemes v2 and v3), then verify
|
||||
# hsm-sign make-cert SUBJECT one-time setup for apk: a self-signed certificate for
|
||||
# the key, written to the token beside it, and printed
|
||||
# as PEM
|
||||
# hsm-sign list what Java sees on the token, for diagnosing an alias
|
||||
#
|
||||
# Environment:
|
||||
# PKCS11_PIN the user PIN (required)
|
||||
# PKCS11_MODULE the PKCS#11 library; default: opensc's
|
||||
# PKCS11_SLOT_INDEX which slot, by position in the slot list; default 0
|
||||
# KEY_ID the key's CKA_ID in hex; default 03
|
||||
# KEY_LABEL the label make-cert gives the certificate; default apk-cert
|
||||
# KEY_ALIAS which key apksigner uses, when the token holds more than one
|
||||
#
|
||||
# Why apk needs a certificate on the token: an APK signature embeds the signer's
|
||||
# certificate, and Java's PKCS#11 key store only offers a private key that has a
|
||||
# certificate with the same CKA_ID. A key used only for detached signatures has none
|
||||
# until make-cert writes one. Android pins an app to its signing certificate for good,
|
||||
# so make-cert runs once and its certificate is kept.
|
||||
set -eu
|
||||
|
||||
die() {
|
||||
echo "hsm-sign: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -n "${PKCS11_PIN:-}" ] || die "PKCS11_PIN is not set"
|
||||
module="${PKCS11_MODULE:-$(find /usr/lib -name opensc-pkcs11.so | head -n 1)}"
|
||||
[ -f "$module" ] || die "no PKCS#11 module at '$module'"
|
||||
slot_index="${PKCS11_SLOT_INDEX:-0}"
|
||||
key_id="${KEY_ID:-03}"
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
chmod 700 "$work"
|
||||
|
||||
# SunPKCS11's configuration: which library, which slot.
|
||||
cat > "$work/pkcs11.cfg" <<EOF
|
||||
name = HSM
|
||||
library = $module
|
||||
slotListIndex = $slot_index
|
||||
EOF
|
||||
|
||||
# What apksigner and keytool need to reach the token through Java. The PIN goes by
|
||||
# environment variable name, never on a command line.
|
||||
java_store() {
|
||||
printf '%s\n' -storetype PKCS11 -providerClass sun.security.pkcs11.SunPKCS11 \
|
||||
-providerArg "$work/pkcs11.cfg"
|
||||
}
|
||||
|
||||
# apksigner's --provider-class instantiates the provider by reflection, which the module
|
||||
# system refuses for SunPKCS11 on Java 9 and later. The JDK's own list of providers
|
||||
# already names SunPKCS11, unconfigured; this points that same entry at the token, so
|
||||
# the provider is installed by the JVM and apksigner only has to ask for a PKCS11 store.
|
||||
java_security() {
|
||||
conf="$(dirname "$(dirname "$(readlink -f "$(command -v java)")")")/conf/security/java.security"
|
||||
entry="$(grep -o '^security\.provider\.[0-9]*=SunPKCS11' "$conf" | cut -d= -f1)" ||
|
||||
die "no SunPKCS11 entry in $conf"
|
||||
printf '%s=SunPKCS11 %s\n' "$entry" "$work/pkcs11.cfg" > "$work/java.security"
|
||||
echo "-Djava.security.properties=$work/java.security"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
detached)
|
||||
[ $# -eq 3 ] || die "usage: hsm-sign detached IN OUT"
|
||||
# The signature action-hsm-sign has always made, so signatures verify (and match
|
||||
# sigstore entries) the same across versions: PKCS#1 v1.5 is deterministic.
|
||||
pkcs11-tool --module "$module" --slot-index "$slot_index" --login --pin env:PKCS11_PIN \
|
||||
--sign --id "$key_id" --mechanism SHA256-RSA-PKCS --input-file "$2" --output-file "$3"
|
||||
;;
|
||||
|
||||
apk)
|
||||
[ $# -eq 3 ] || die "usage: hsm-sign apk IN.apk OUT.apk"
|
||||
set -- "$2" "$3"
|
||||
alias_args=""
|
||||
[ -n "${KEY_ALIAS:-}" ] && alias_args="--ks-key-alias ${KEY_ALIAS}"
|
||||
# shellcheck disable=SC2086 # alias_args is two words or none, by construction
|
||||
JAVA_TOOL_OPTIONS="$(java_security)" apksigner sign \
|
||||
--ks NONE --ks-type PKCS11 --ks-pass env:PKCS11_PIN \
|
||||
$alias_args \
|
||||
--in "$1" --out "$2"
|
||||
# Proof, in the log, of what was signed and by whom.
|
||||
apksigner verify --verbose --print-certs "$2"
|
||||
;;
|
||||
|
||||
make-cert)
|
||||
[ $# -eq 2 ] || die "usage: hsm-sign make-cert '/CN=...'"
|
||||
subject="$2"
|
||||
# OpenSSL's PKCS#11 engine, pointed at the same module, signs the certificate with
|
||||
# the HSM's key. The PIN is in a file only this process can read, not an argument.
|
||||
cat > "$work/openssl.cnf" <<EOF
|
||||
openssl_conf = conf
|
||||
[conf]
|
||||
engines = engines
|
||||
[engines]
|
||||
pkcs11 = p11
|
||||
[p11]
|
||||
engine_id = pkcs11
|
||||
MODULE_PATH = $module
|
||||
PIN = $PKCS11_PIN
|
||||
init = 0
|
||||
[req]
|
||||
distinguished_name = dn
|
||||
[dn]
|
||||
EOF
|
||||
uri_id="$(printf '%s' "$key_id" | sed 's/../%&/g')"
|
||||
# Thirty years: the certificate's dates are not checked for APK signatures, and
|
||||
# replacing it would mean a new app identity on every device.
|
||||
OPENSSL_CONF="$work/openssl.cnf" openssl req -new -x509 -sha256 -days 10950 \
|
||||
-subj "$subject" -engine pkcs11 -keyform engine \
|
||||
-key "pkcs11:id=${uri_id};type=private" -out "$work/cert.pem"
|
||||
openssl x509 -in "$work/cert.pem" -outform DER -out "$work/cert.der"
|
||||
pkcs11-tool --module "$module" --slot-index "$slot_index" --login --pin env:PKCS11_PIN \
|
||||
--write-object "$work/cert.der" --type cert --id "$key_id" --label "${KEY_LABEL:-apk-cert}"
|
||||
cat "$work/cert.pem"
|
||||
;;
|
||||
|
||||
list)
|
||||
# shellcheck disable=SC2046 # java_store prints one argument per line
|
||||
keytool -list -v -storepass:env PKCS11_PIN -keystore NONE $(java_store)
|
||||
;;
|
||||
|
||||
*)
|
||||
die "usage: hsm-sign detached IN OUT | apk IN OUT | make-cert SUBJECT | list"
|
||||
;;
|
||||
esac
|
||||
88
signer/test.sh
Executable file
88
signer/test.sh
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
#!/bin/sh
|
||||
# The signer, end to end, against SoftHSM instead of the real HSM.
|
||||
#
|
||||
# test.sh detached signatures only
|
||||
# test.sh UNSIGNED.apk [SIGNED.apk] and APK signing; the second keeps the signed APK
|
||||
#
|
||||
# Builds the signer image exactly as the action does, adds SoftHSM to it, and makes a
|
||||
# token with an RSA key and no certificate, the state the real HSM's key is in. Then it
|
||||
# makes a detached signature and checks it with openssl against the token's public key
|
||||
# and, given an APK, runs make-cert and apk, and has apksigner verify the result.
|
||||
# Everything but pcscd and the hardware is the code that runs in CI. Uses docker, or
|
||||
# podman when there is no docker. Scratch space comes from mktemp, so TMPDIR moves it.
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo "usage: test.sh [UNSIGNED.apk [SIGNED.apk]]" >&2
|
||||
exit 2
|
||||
}
|
||||
[ $# -le 2 ] || usage
|
||||
apk_in=""
|
||||
apk_out="${2:-}"
|
||||
if [ $# -ge 1 ]; then
|
||||
[ -f "$1" ] || usage
|
||||
apk_in="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
|
||||
fi
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
engine="$(command -v docker || command -v podman)" || { echo "need docker or podman" >&2; exit 2; }
|
||||
|
||||
"$engine" build -q -t action-hsm-sign-signer:test "$here" >/dev/null
|
||||
"$engine" build -q -t action-hsm-sign-signer:softhsm - >/dev/null <<'EOF'
|
||||
FROM action-hsm-sign-signer:test
|
||||
USER root
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends softhsm2 && rm -rf /var/lib/apt/lists/*
|
||||
USER user
|
||||
EOF
|
||||
|
||||
out_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$out_dir"' EXIT
|
||||
chmod 777 "$out_dir"
|
||||
|
||||
set -- -v "$out_dir:/out"
|
||||
if [ -n "$apk_in" ]; then
|
||||
set -- "$@" -v "$apk_in:/home/user/in.apk:ro"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2016 # expanded inside the container
|
||||
"$engine" run --rm --entrypoint /bin/sh "$@" action-hsm-sign-signer:softhsm -euc '
|
||||
export SOFTHSM2_CONF="$HOME/softhsm2.conf"
|
||||
mkdir -p "$HOME/tokens"
|
||||
echo "directories.tokendir = $HOME/tokens" > "$SOFTHSM2_CONF"
|
||||
export PKCS11_MODULE="$(find /usr/lib -name libsofthsm2.so | head -n 1)"
|
||||
export PKCS11_PIN=123456 KEY_ID=03
|
||||
softhsm2-util --init-token --free --label hsm-sign-test --pin "$PKCS11_PIN" --so-pin 87654321 >/dev/null
|
||||
# A key and nothing else, as on the real HSM before make-cert.
|
||||
pkcs11-tool --module "$PKCS11_MODULE" --login --pin env:PKCS11_PIN \
|
||||
--keypairgen --key-type rsa:4096 --id "$KEY_ID" --label test-key >/dev/null
|
||||
pkcs11-tool --module "$PKCS11_MODULE" --read-object --type pubkey --id "$KEY_ID" |
|
||||
openssl pkey -pubin -inform DER -out "$HOME/pub.pem"
|
||||
want="$(openssl pkey -pubin -in "$HOME/pub.pem" -outform DER | sha256sum | cut -d" " -f1)"
|
||||
|
||||
echo "== detached"
|
||||
echo "an artifact" > artifact
|
||||
hsm-sign detached artifact /out/artifact.sig
|
||||
# What rekor and users do with the published public key.
|
||||
openssl dgst -sha256 -verify "$HOME/pub.pem" -signature /out/artifact.sig artifact
|
||||
|
||||
[ -f in.apk ] || exit 0
|
||||
echo "== make-cert"
|
||||
hsm-sign make-cert "/CN=hsm-sign test signer" > /out/cert.pem
|
||||
openssl x509 -in /out/cert.pem -noout -subject -fingerprint -sha256
|
||||
echo "== apk"
|
||||
hsm-sign apk in.apk /out/signed.apk
|
||||
echo "== independent verify"
|
||||
apksigner verify --verbose /out/signed.apk
|
||||
# The APK is signed by the key, not merely by a certificate naming it: the public key
|
||||
# apksigner reports is the token'"'"'s.
|
||||
got="$(apksigner verify --verbose --print-certs /out/signed.apk | sed -n "s/^Signer #1 public key SHA-256 digest: //p")"
|
||||
echo "token public key sha256: $want"
|
||||
echo "APK signer key sha256: $got"
|
||||
[ "$want" = "$got" ]
|
||||
'
|
||||
echo "test.sh: detached signature verified"
|
||||
if [ -n "$apk_in" ]; then
|
||||
if [ -n "$apk_out" ]; then
|
||||
cp "$out_dir/signed.apk" "$apk_out"
|
||||
fi
|
||||
echo "test.sh: signed and verified $(basename "$apk_in")"
|
||||
fi
|
||||
Loading…
Add table
Reference in a new issue