diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 3dca3a97..dcd34c40 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -40,4 +40,7 @@ jobs: - name: Set up Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc # install - name: Run CLI tests - run: hatch run cli:test + run: | + sudo apt update + sudo apt install softhsm2 gnutls-bin + hatch run cli:test diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index e969a631..7f7fdc7e 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -51,6 +51,6 @@ jobs: if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then sudo apt update sudo apt install softhsm2 gnutls-bin - ./scripts/pkcs11-tests/softhsm_setup setup + ./scripts/tests/softhsm_setup setup fi hatch test -c -py ${{ matrix.python-version }} -m integration diff --git a/CHANGELOG.md b/CHANGELOG.md index 849d19d9..4b391983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ All versions prior to 1.0.0 are untracked. ## [Unreleased] ### Added --Added the `digest` subcommand to compute and print a model's digest. This enables other tools to easily pair the attestations with a model directory. +- Added the `digest` subcommand to compute and print a model's digest. This enables other tools to easily pair the attestations with a model directory. +- Added `--module-paths` option to PKCS #11 signing methods pkcs11-key and pkcs11-certificate. ### Changed - Standardized CLI flags to use hyphens (e.g., `--trust-config` instead of `--trust_config`). Underscore variants are still accepted for backwards compatibility via token normalization. diff --git a/scripts/pkcs11-tests/test_pkcs11.sh b/scripts/pkcs11-tests/test_pkcs11.sh deleted file mode 100755 index 019725d1..00000000 --- a/scripts/pkcs11-tests/test_pkcs11.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -DIR=$(dirname "$0") - -PATH=$PATH:${PWD}/${DIR} -TMPDIR=$(mktemp -d) || exit 1 - -cleanup() { - softhsm_setup teardown &>/dev/null - rm -rf "${TMPDIR}" -} -trap cleanup SIGTERM EXIT - -if ! msg=$(softhsm_setup setup); then - echo -e "Could not setup softhsm:\n${msg}" - exit 77 -fi -pkcs11uri=$(echo "${msg}" | sed -n 's|^keyuri: \(.*\)|\1|p') - -model_sig=${TMPDIR}/model.sig -pub_key=${TMPDIR}/pubkey.pem -model_path=${TMPDIR} - -if ! msg=$(softhsm_setup getpubkey > "${pub_key}"); then - echo -e "Could not get public key:\n${msg}" - exit 77 -fi - -if ! python -m model_signing sign pkcs11-key \ - --signature "${model_sig}" \ - --pkcs11_uri "${pkcs11uri}" \ - "${model_path}"; then - echo "Could not sign." - exit 77 -fi - -if ! python -m model_signing verify key \ - --signature "${model_sig}" \ - --public_key "${pub_key}" \ - "${model_path}"; then - echo "Could not verify signature." - exit 77 -fi - -exit 0 diff --git a/scripts/pkcs11-tests/softhsm_setup b/scripts/tests/softhsm_setup similarity index 100% rename from scripts/pkcs11-tests/softhsm_setup rename to scripts/tests/softhsm_setup diff --git a/scripts/tests/test-pkcs11.sh b/scripts/tests/test-pkcs11.sh new file mode 100755 index 00000000..0751efe1 --- /dev/null +++ b/scripts/tests/test-pkcs11.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +DIR=$(dirname "$0") + +PATH=$PATH:${PWD}/${DIR} +TMPDIR=$(mktemp -d) || exit 1 + +cleanup() { + softhsm_setup teardown &>/dev/null + rm -rf "${TMPDIR}" +} +trap cleanup SIGTERM EXIT + +if ! msg=$(softhsm_setup setup); then + echo -e "Could not setup softhsm:\n${msg}" + exit 77 +fi +pkcs11uri=$(echo "${msg}" | sed -n 's|^keyuri: \(.*\)|\1|p') + +model_sig=${TMPDIR}/model.sig +pub_key=${TMPDIR}/pubkey.pem +model_path=${TMPDIR} + +# The SoftHSM PKCS #11 module is in a special path on Ubuntu +for p in "/usr/lib/pkcs11" "/usr/lib64/pkcs11" "/usr/lib/softhsm"; do + add_options+=" --module-paths ${p}" +done + +if ! softhsm_setup getpubkey &>"${pub_key}"; then + echo -e "Could not get public key:\n${msg}" + echo "${pub_key}" + exit 77 +fi + +if ! python -m model_signing sign pkcs11-key \ + --signature "${model_sig}" \ + --pkcs11_uri "${pkcs11uri}" \ + ${add_options:+${add_options}} \ + "${model_path}"; then + echo "Could not sign." + exit 77 +fi + +if ! python -m model_signing verify key \ + --signature "${model_sig}" \ + --public_key "${pub_key}" \ + "${model_path}"; then + echo "Could not verify signature." + exit 77 +fi + +if type -P openssl >/dev/null; then + pub_key_cert=${TMPDIR}/pubkey-cert.pem + ca_key="${TMPDIR}/ca-key.pem" + ca_cert="${TMPDIR}/ca-cert.pem" + v3ext="${TMPDIR}/v3.ext" + + if ! err=$(openssl req \ + -new \ + -x509 \ + -nodes \ + -days 3650\ + -subj "/CN=MyRootCA" \ + -keyout "${ca_key}" \ + -addext "basicConstraints=critical,CA:TRUE" \ + -addext "keyUsage=critical,keyCertSign" \ + -out "${ca_cert}" 2>&1); then + echo "Could not create CA certificate." + echo "${err}" + exit 77 + fi + echo "Created CA." + + cat <<- _EOF_ > "${v3ext}" + keyUsage=critical, digitalSignature + _EOF_ + + if ! err=$(openssl x509 \ + -new \ + -CAkey "${ca_key}" \ + -CA "${ca_cert}" \ + -force_pubkey "${pub_key}" \ + -subj "/CN=MyCery" \ + -extfile "${v3ext}" \ + -out "${pub_key_cert}" 2>&1); then + echo "Could not create certificate for HSM public key." + echo "${err}" + exit 77 + fi + echo "Signed HSM public key with CA key." + + if ! python -m model_signing sign pkcs11-certificate \ + --signature "${model_sig}" \ + --pkcs11_uri "${pkcs11uri}" \ + --signing_certificate "${pub_key_cert}" \ + --certificate_chain "${pub_key_cert}" \ + ${add_options:+${add_options}} \ + "${model_path}"; then + echo "Could not sign with pkcs11-certificate method." + exit 77 + fi + + if ! python -m model_signing verify certificate \ + --signature "${model_sig}" \ + --certificate_chain "${ca_cert}" \ + "${model_path}"; then + echo "Could not verify signature created with pkcs11-certificate method." + exit 77 + fi +fi + +exit 0 diff --git a/src/model_signing/_cli.py b/src/model_signing/_cli.py index 94da05be..299d7342 100644 --- a/src/model_signing/_cli.py +++ b/src/model_signing/_cli.py @@ -120,6 +120,15 @@ def set_attribute(self, key, value): help="PKCS #11 URI of the private key.", ) +# Decorator for paths to the PKCS #11 modules +_module_paths_option = click.option( + "--module-paths", + type=str, + metavar="PKCS11_MODULE_PATHS", + multiple=True, + help="PKCS #11 module paths.", +) + # Decorator for the commonly used option to pass a certificate chain to # establish root of trust (when signing or verifying using certificates). _certificate_root_of_trust_option = click.option( @@ -507,6 +516,7 @@ def _sign_private_key( @_allow_symlinks_option @_write_signature_option @_pkcs11_uri_option +@_module_paths_option def _sign_pkcs11_key( model_path: pathlib.Path, ignore_paths: Iterable[pathlib.Path], @@ -514,6 +524,7 @@ def _sign_pkcs11_key( allow_symlinks: bool, signature: pathlib.Path, pkcs11_uri: str, + module_paths: Iterable[str], ) -> None: """Sign using a private key using a PKCS #11 URI. @@ -524,6 +535,9 @@ def _sign_pkcs11_key( Traditionally, signing could be achieved by using a public/private key pair. Pass the PKCS #11 URI of the signing key using `--pkcs11_uri`. + Paths in PKCS11_MODULE_PATHS provide access to PKCS #11 modules located + outside the default paths of /usr/lib/pkcs11 and /usr/lib64/pkcs11. + Note that this method does not provide a way to tie to the identity of the signer, outside of pairing the keys. Also note that we don't offer key management protocols. @@ -533,7 +547,7 @@ def _sign_pkcs11_key( model_path, list(ignore_paths) + [signature] ) model_signing.signing.Config().use_pkcs11_signer( - pkcs11_uri=pkcs11_uri + pkcs11_uri=pkcs11_uri, module_paths=module_paths ).set_hashing_config( model_signing.hashing.Config() .set_ignored_paths(paths=ignored, ignore_git_paths=ignore_git_paths) @@ -610,6 +624,7 @@ def _sign_certificate( @_pkcs11_uri_option @_signing_certificate_option @_certificate_root_of_trust_option +@_module_paths_option def _sign_pkcs11_certificate( model_path: pathlib.Path, ignore_paths: Iterable[pathlib.Path], @@ -619,6 +634,7 @@ def _sign_pkcs11_certificate( pkcs11_uri: str, signing_certificate: pathlib.Path, certificate_chain: Iterable[pathlib.Path], + module_paths: Iterable[str], ) -> None: """Sign using a certificate. @@ -635,6 +651,9 @@ def _sign_pkcs11_certificate( root of trust (this option can be repeated as needed, or all cerificates could be placed in a single file). + Paths in PKCS11_MODULE_PATHS provide access to PKCS #11 modules located + outside the default paths of /usr/lib/pkcs11 and /usr/lib64/pkcs11. + Note that we don't offer certificate and key management protocols. """ try: @@ -645,6 +664,7 @@ def _sign_pkcs11_certificate( pkcs11_uri=pkcs11_uri, signing_certificate=signing_certificate, certificate_chain=certificate_chain, + module_paths=module_paths, ).set_hashing_config( model_signing.hashing.Config() .set_ignored_paths(paths=ignored, ignore_git_paths=ignore_git_paths) diff --git a/tests/_signing/pkcs11uri_test.py b/tests/_signing/pkcs11uri_test.py index 31b55357..ea4bedfa 100644 --- a/tests/_signing/pkcs11uri_test.py +++ b/tests/_signing/pkcs11uri_test.py @@ -274,7 +274,7 @@ class TestPkcs11SoftHSMSigning: def run_softhsm_setup(self, cmd: str) -> tuple[bytes | None, int]: curr_dir = os.path.dirname(os.path.realpath(__file__)) softhsm_setup = os.path.join( - curr_dir, "../../scripts/pkcs11-tests/softhsm_setup" + curr_dir, "../../scripts/tests/softhsm_setup" ) result = subprocess.run([softhsm_setup, cmd], stdout=subprocess.PIPE) return result.stdout, result.returncode