diff --git a/.github/workflows/install-dependencies b/.github/workflows/install-dependencies index 87b52b5f..987230d4 100755 --- a/.github/workflows/install-dependencies +++ b/.github/workflows/install-dependencies @@ -23,8 +23,7 @@ debian:*|ubuntu:*) while ! apt-get -y install ${COMMON} \ build-essential pkg-config libssl-dev libjansson-dev libjose-dev \ luksmeta libluksmeta-dev libpwquality-tools libglib2.0-dev \ - libudisks2-dev libaudit-dev systemd; do - + libudisks2-dev libaudit-dev systemd opensc pcscd libsofthsm2-dev; do sleep 5 done ;; @@ -33,8 +32,10 @@ debian:*|ubuntu:*) printf 'max_parallel_downloads=10\nfastestmirror=1\n' >> /etc/dnf/dnf.conf dnf -y clean all dnf -y --setopt=deltarpm=0 update - dnf -y install dnf-utils jq socat cryptsetup keyutils cracklib-dicts lsof - command -v dnf5 && dnf5 -y install dnf5-command\(builddep\) || dnf -y install dnf-command\(builddep\) + dnf -y install dnf-utils jq socat cryptsetup keyutils cracklib-dicts lsof \ + opensc pcsc-lite softhsm + command -v dnf5 && dnf5 -y install dnf5-command\(builddep\) \ + || dnf -y install dnf-command\(builddep\) dnf -y builddep clevis ;; @@ -48,7 +49,8 @@ debian:*|ubuntu:*) yum -y --allowerasing install ${COMMON} yum -y install pkgconfig openssl-devel openssl zlib-devel \ jansson-devel findutils gcc libjose-devel luksmeta libluksmeta-devel \ - audit-libs-devel tpm2-tools desktop-file-utils cracklib-dicts + audit-libs-devel tpm2-tools desktop-file-utils cracklib-dicts opensc \ + pcsc-lite softhsm sed -i 's|>=1\.0\.2|>=1\.0\.1|' meson.build ;; esac diff --git a/README.md b/README.md index 1b2c9cf2..4e1784f7 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,239 @@ $ echo hi | clevis encrypt tpm2 '{}' > hi.jwe Clevis store the public and private keys of the encrypted key in the JWE object, so those can be fetched on decryption to unseal the key encrypted using the TPM2. +#### PIN: PKCS#11 + +Clevis can perform the role of a PKCS#11 application, as described in the [RFC 7512: The PKCS#11 URI Scheme](https://www.rfc-editor.org/rfc/rfc7512.html). + +PKCS#11 protocol determines that a PIN (Personal Identity Number) must be configured into the hardware device so that the unlocking process is successful. Clevis will allow users to unlock a particular encrypted disk, and will provide a way to get the PIN. There will be two possibilities: + +1 - Provide the PIN at boot time: In this first case, Clevis will detect PKCS#11 device and will prompt for its PIN. +In case PIN is wrong, Clevis will prompt for the PIN again. It is the user's responsibility to be aware of the possible lock / brick of the device in case PIN is unknown. + +2 - Provide the PIN at Clevis configuration time: In this second case, Clevis will be configured with the PIN value. + +Initially, RFC7512 defines a mechanism to specify a special kind of URI (the `pkcs11` URI), that allows identifying both a device and also the information required for it to be unlocked. Special attention deserves the parameters `pin-value`, which allow specifying the value of the PIN or the location of the PIN respectively. Clevis will understand, initially, the 'pin-value' parameter. Below you can find and example of PKCS#11 URIs using previous parameter: + +* PKCS#11 URI with `pin-value` defined: + +``` +pkcs11:token=Software%20PKCS%2311%20softtoken;manufacturer=Snake%20Oil,%20Inc.?pin-value=the-pin +``` + +In the next section, Clevis configuration examples are provided, so that it is clarified what are the different options for a PKCS#11 device to be bound to an encrypted disk. + +##### Clevis configuration + +Clevis will provide a mechanism for the user to bind a particular PKCS#11 device to an encrypted device. The name of the new pin for Clevis will be `pkcs11`, and the way to configure it will be the same that is currently used: + +``` +$ clevis luks bind -h +``` + +``` +Usage: clevis luks bind [-y] [-f] [-s SLT] [-k KEY] [-t TOKEN_ID] [-e EXISTING_TOKEN_ID] -d DEV PIN CFG +``` + +##### Configuration to provide a PKCS#11 URI to Clevis +As first example, a user can provide the information of the device by specifying its URI to Clevis: + +``` +$ clevis luks bind -d /dev/sda1 pkcs11 '{"uri": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II; +serial=0a35ba26b062b9c5;token=clevis;id=%02;object=Encryption%20Key"}' +``` + +##### Configuration to bind Clevis to the first PKCS#11 device found +An additional option is to provide Clevis a configuration so that the first PKCS#11 device found by Clevis is bound. To do so, an empty URI can be provided as shown below: + +``` +$ clevis luks bind -d /dev/sda1 pkcs11 '{"uri": "pkcs11:"}' +``` + +An even shorter configuration command, equivalent to the previous one, is shown below: + +``` +$ clevis luks bind -d /dev/sda1 pkcs11 '{}' +``` + +In this case, Clevis will be responsible for the detection of the device and, if no device is found, responsible for dumping the corresponding error. + +It must be clarified that providing an empty URI will make Clevis to prompt also to select one of the available keys matched on the token to avoid accidentally encryption with unwanted keys. + +##### Configuration to provide a module path to Clevis PKCS#11 pin: +A module path can be provided to Clevis, so that it uses that module to access a device. This is only required in case the card is not supported by underlying Clevis software (OpenSC). For this reason, the module path field is completely optional. To provide the module location the user can provide the "module-path" to the "uri" Clevis configuration: + +``` +$ clevis-luks-bind -d /dev/sda1 pkcs11 '{"uri": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II; +serial=0a35ba26b062b9c5;token=clevis;id=%02;object=Encryption%20Key? +module-path=/usr/local/lib64/libmypkcs11.so"}' +``` + +As it happens with the rest of devices, encrypted disks that have been bound to a PKCS#11 device can be checked with `clevis luks list` command: + +``` +$ clevis luks list -d /dev/sda1 +``` + +``` +1: pkcs11 '{"uri": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II; +serial=0a35ba26b062b9c5;token=clevis;id=%02;object=Encryption%20Key? +module-path=/usr/local/lib64/libmypkcs11.so"}' +``` + +##### Configuration to provide PKCS#11 tool a different mechanism + +In the first phase of development, Clevis will be used in top of OpenSC to provide PKCS#11 functionality. +OpenSC, and, in particular, `pkcs11-tool`, provides an option to indicate the mechanism to use for decryption. +For testing purposes, some libraries, such as [SoftHSM](https://www.opendnssec.org/softhsm)), don't work with default `pkcs11-tool` mechanism, +so it is required to provide a particular mechanism to use. For this reason, Clevis can be provided with +the mechanism to use, in case the default one, `RSA-PKCS-OAEP`, is not valid: + +``` +$ clevis luks bind -d /dev/sda1 pkcs11 '{"uri": "pkcs11:", "mechanism":"RSA-PKCS"}' +``` + +In order to check available mechanisms for a specific token, command `pkcs11-tool -M` can be used: + + +``` +$ pkcs11-tool -M +Using slot 0 with a present token (0x0) +Supported mechanisms: + SHA-1, digest +... + SHA512, digest + MD5, digest +... + RSA-PKCS-KEY-PAIR-GEN, keySize={2048,4096}, generate_key_pair +``` + +At this time, only RSA mechanisms are supported by Clevis. Due to a limitation of the rest of the algorithms, no other asymmetric cryptographic algorithm can do encryption easily. The ECC supports only signatures and key derivation, but not encryption. The encryption operation can be somehow constructed from the key derivation, but it is not a straightforward operation. + +It must be highlighted that the RSA-PKCS mechanism (PKCS#1.5 padding for encryption) is [considered to be not secure](https://people.redhat.com/~hkario/marvin/) and it is mostly provided for compatibility, but it is not recommended using it in production. + +##### Multi-device configuration +Clevis will allow specifying the slot where a PKCS#11 device is located through the parameters provided to the URI: + +``` +$ clevis luks bind -d /dev/sda1 pkcs11 '{"uri": "pkcs11:slot-id=0"}' +``` + +It must be clarified that providing just the slot information will make Clevis to guess one of the available keys matched on the token in the selected slot, which could cause accidentally encryption with unwanted keys. **It is not recommended to use slot as device selector, as slot id is a number that is not guaranteed to be stable across PKCS#11 module initializations**. However, there are certain libraries and modules that provide stable slot identifiers, so it can be used for these particular cases. + +There are two better options to distinguish between different PKCS#11 devices: + +1 - Multi-device configuration with public key object (**recommended**): + +With recent versions of `OpenSC` (from OpenSC 0.26.0 release) onwards, `pkcs11-tool`, which is used by Clevis to handle most of the PKCS#11 commands, the PKCS#11 URI is dumped for both the tokens and the objects of a particular token: + +``` +$ pkcs11-tool -L | grep uri + uri : pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II;serial=42facd1f749ece7f;token=clevis + uri : pkcs11:model=PKCS%2315%20emulated;manufacturer=OpenPGP%20project;serial=000f06080f4f;token=OpenPGP%20card%20%28User%20PIN%29 +$ pkcs11-tool -O --slot-index 1 --type pubkey | grep uri +ising slot 0 with a present token (0x0) + uri: pkcs11:model=PKCS%2315%20emulated;manufacturer=OpenPGP%20project;serial=000f06080f4f;token=OpenPGP%20card%20%28User%20PIN%29;id=%03;object=Authentication%20key;type=public +``` + +In this particular cases, when multiple PKCS#11 devices exist, select the public key of the particular device and bind it to Clevis: + +``` +$ clevis luks bind -d /dev/sda pkcs11 '{"uri":"pkcs11:model=PKCS%2315%20emulated;manufacturer=OpenPGP%20project;serial=000f06080f4f;token=OpenPGP%20card%20%28User%20PIN%29;id=%03;object=Authentication%20key;type=public"}' +``` +**In case you are using module-path, you will have to use the one returned when providing --module option:** + +``` +$ pkcs11-tool --module /usr/lib64/libykcs11.so -O --type pubkey | grep uri + /usr/local/bin/pkcs11-tool.manual --module /usr/lib64/libykcs11.so -O --type pubkey | grep uri +Using slot 0 with a present token (0x0) + uri: pkcs11:model=YubiKey%20YK5;manufacturer=Yubico%20%28www.yubico.com%29;serial=28083311;token=YubiKey%20PIV%20%2328083311;id=%03;object=Public%20key%20for%20Key%20Management;type=public + uri: pkcs11:model=YubiKey%20YK5;manufacturer=Yubico%20%28www.yubico.com%29;serial=28083311;token=YubiKey%20PIV%20%2328083311;id=%19;object=Public%20key%20for%20PIV%20Attestation;type=public +$ clevis luks bind -d /dev/sda pkcs11 '{"uri":"pkcs11:model=YubiKey%20YK5;manufacturer=Yubico%20%28www.yubico.com%29;serial=28083311;token=YubiKey%20PIV%20%2328083311;id=%03;object=Public%20key%20for%20Key%20Management;type=public;module-path=/usr/lib64/libykcs11.so"}' +``` + +2 - Multi-device configuration with serial + token specification: + +**For versions where `pkcs11-tool` does not dump the URI for the tokens/objects**, specific identification will be "tried" by Clevis by using the device `serial` + `token label` pair. +In this type of scenarios, identification can be performed with these two parameters, although `model` should be provided also to ease Clevis informing about the device when asking for the PIN: + +``` +# pkcs11-tool -L | grep "token label\|serial" + token label : OpenPGP card (User PIN) + serial num : 42facd1f749ece7f +$ clevis luks bind -d /dev/sda pkcs11 '{"uri":"pkcs11:model=PKCS%2315%20emulated;serial=000f06080f4f;token=OpenPGP%20card%20%28User%20PIN%29"}' +``` + +Remember that special characters must be defined in percent mode, as defined in [RFC 7512: The PKCS#11 URI Scheme](https://www.rfc-editor.org/rfc/rfc7512.html). + +##### Clevis PKCS#11 installation and configuration + +For installation and configuration of the clevis PKCS#11 feature, next steps must be followed: + +1 - Install Clevis required dependencies, including PKCS#11 dependencies: + +``` +$ sudo dnf install -y opensc pcsc-lite openssl socat +``` + +2 - The PKCS11 device must be accessible by “pkcs11-tool”: + +``` +$ pkcs11-tool -L +pkcs11-tool -L +Available slots: +Slot 0 (0x0): Yubico YubiKey OTP+CCID 00 00 + token label : clevis + ... + uri : pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II;serial=42facd1f749ece7f;token=clevis +``` + +3 - Configure device to bind with clevis: + +``` +$ sudo clevis luks bind -d /dev/sda5 pkcs11 '{"uri":"pkcs11:"}' +``` + +In case it is required to provide the module to use, it can be done through `module-path` URI parameter: + +``` +$ sudo clevis luks bind -d /dev/sda5 pkcs11 '{"uri":"pkcs11:module-path=/usr/lib64/libykcs11.so.2"}' +``` + +4 - Enable clevis-luks-pkcs11-askpass.socket unit: + +``` +$ sudo systemctl enable --now clevis-luks-pkcs11-askpass.socket +``` + +5 - /etc/crypttab configuration: + +For PKCS#11 feature to work appropriately, `/etc/crypttab` file must be configured so that systemd uses an AF\_UNIX socket to wait for the keyphrase that will unlock the disk and not to prompt it through the console. + +Clevis PKCS#11 unit file will configure a socket in path `/run/systemd/clevis-pkcs11.sock` to send and receive information about disk unlocking. For disks that will be unlocked through PKCS#11 Clevis pin, that socket file must be configured as key file. So, next change must be introduced in `/etc/crypttab` for unlocking to take place: + +``` +$ sudo diff -Nuar /etc/crypttab.ori /etc/crypttab +--- /etc/crypttab.ori 2024-07-04 10:46:16.295073739 +0200 ++++ /etc/crypttab 2024-07-03 17:14:27.764743860 +0200 +@@ -1 +1,2 @@ +-luks-6e38d5e1-7f83-43cc-819a-7416bcbf9f84 UUID=6e38d5e1-7f83-43cc-819a-7416bcbf9f84 - - ++luks-6e38d5e1-7f83-43cc-819a-7416bcbf9f84 UUID=6e38d5e1-7f83-43cc-819a-7416bcbf9f84 /run/systemd/clevis-pkcs11.sock keyfile-timeout=30s +``` + +It is highly recommended setting a `keyfile-timeout` option to configure a fall-through mechanism in case some unlocking error occurs and passphrase is required to be entered manually through console. + +6 - Reboot and test: + +System should boot and ask for the PKCS#11 device PIN, and decrypt the corresponding configured encrypted disk only in case PIN is correct. + +7 - In case no boot process needs to be tested, encrypt and decrypt with next command (note it is necessary to provide the PIN value for it to work appropriately) and check encryption/decryption of a string can be performed with this one-liner, and no error takes place: + +``` +$ echo "top secret" | clevis encrypt pkcs11 '{"uri":"pkcs11:module-path=/usr/lib64/libykcs11.so.2?pin-value=123456"}' | clevis decrypt +``` + +The `top secret` string should be returned + #### PIN: Shamir Secret Sharing Clevis provides a way to mix pins together to provide sophisticated unlocking diff --git a/src/luks/clevis-luks-common-functions.in b/src/luks/clevis-luks-common-functions.in index 59ac9af5..29e4631d 100644 --- a/src/luks/clevis-luks-common-functions.in +++ b/src/luks/clevis-luks-common-functions.in @@ -177,6 +177,22 @@ clevis_luks_print_pin_config() { local pin= case "${P}" in + pkcs11) + local uri + uri="$(jose fmt -j- -g uri -u- <<< "${content}")" + mechanism="$(jose fmt -j- -g mechanism -u- <<< "${content}")" + if [ -z "${mechanism}" ]; then + pin=$(printf '{"uri":"%s"}' "${uri}") + else + pin=$(printf '{"uri":"%s", "mechanism":"%s"}' "${uri}" "${mechanism}") + fi + printf "pkcs11 '%s'" "${pin}" + ;; + sss) + local threshold + threshold=$(jose fmt -j- -Og t -o- <<< "${content}") + clevis_luks_process_sss_pin "${content}" "${threshold}" + ;; tang) local url url="$(jose fmt -j- -g url -u- <<< "${content}")" @@ -197,11 +213,6 @@ clevis_luks_print_pin_config() { pin=${pin/#,/} printf "tpm2 '{%s}'" "${pin}" ;; - sss) - local threshold - threshold=$(jose fmt -j- -Og t -o- <<< "${content}") - clevis_luks_process_sss_pin "${content}" "${threshold}" - ;; *) printf "unknown pin '%s'" "${P}" ;; @@ -241,6 +252,7 @@ clevis_luks_process_sss_pin() { local jwe="${1}" local threshold="${2}" + local sss_pkcs11 local sss_tang local sss_tpm2 local sss @@ -255,6 +267,9 @@ clevis_luks_process_sss_pin() { fi read -r pin cfg <<< "${pin_cfg}" case "${pin}" in + pkcs11) + sss_pkcs11="${sss_pkcs11},${cfg}" + ;; tang) sss_tang="${sss_tang},${cfg}" ;; @@ -276,6 +291,10 @@ clevis_luks_process_sss_pin() { cfg="${cfg},"$(clevis_luks_join_sss_cfg "tpm2" "${sss_tpm2}") fi + if [ -n "${sss_pkcs11}" ]; then + cfg="${cfg},"$(clevis_luks_join_sss_cfg "pkcs11" "${sss_pkcs11}") + fi + if [ -n "${sss}" ]; then cfg=$(printf '%s,"sss":%s' "${cfg}" "${sss}") fi diff --git a/src/luks/dracut/clevis-pin-pkcs11/clevis-pkcs11-hook.sh b/src/luks/dracut/clevis-pin-pkcs11/clevis-pkcs11-hook.sh new file mode 100755 index 00000000..01a3062a --- /dev/null +++ b/src/luks/dracut/clevis-pin-pkcs11/clevis-pkcs11-hook.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +if [ ! -f /run/systemd/clevis-pkcs11.run ] && [ -d /run/systemd ]; +then + pcscd --disable-polkit + echo "" > /run/systemd/clevis-pkcs11.run + /usr/libexec/clevis-luks-pkcs11-askpin -d -r +fi diff --git a/src/luks/dracut/clevis-pin-pkcs11/clevis-pkcs11-prehook.sh b/src/luks/dracut/clevis-pin-pkcs11/clevis-pkcs11-prehook.sh new file mode 100755 index 00000000..085d1f90 --- /dev/null +++ b/src/luks/dracut/clevis-pin-pkcs11/clevis-pkcs11-prehook.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +if [ ! -f /run/systemd/clevis-pkcs11.pre.run ] && [ -d /run/systemd ]; +then + clevis-pkcs11-afunix-socket-unlock -l /run/systemd/clevis-pkcs11-dracut.log -f /run/systemd/clevis-pkcs11.sock -s 60 & + echo "" > /run/systemd/clevis-pkcs11.pre.run +fi diff --git a/src/luks/dracut/clevis-pin-pkcs11/meson.build b/src/luks/dracut/clevis-pin-pkcs11/meson.build new file mode 100644 index 00000000..c97ec5b8 --- /dev/null +++ b/src/luks/dracut/clevis-pin-pkcs11/meson.build @@ -0,0 +1,18 @@ +dracut = dependency('dracut', required: false) + +if dracut.found() + dracutdir = dracut.get_pkgconfig_variable('dracutmodulesdir') + '/60' + meson.project_name() + '-pin-pkcs11' + + configure_file( + input: 'module-setup.sh.in', + output: 'module-setup.sh', + install_dir: dracutdir, + configuration: data, + ) + + install_data('clevis-pkcs11-hook.sh', install_dir: dracutdir) + install_data('clevis-pkcs11-prehook.sh', install_dir: dracutdir) + +else + warning('Will not install dracut module clevis-pin-pkcs11 due to missing dependencies!') +endif diff --git a/src/luks/dracut/clevis-pin-pkcs11/module-setup.sh.in b/src/luks/dracut/clevis-pin-pkcs11/module-setup.sh.in new file mode 100755 index 00000000..39d06a04 --- /dev/null +++ b/src/luks/dracut/clevis-pin-pkcs11/module-setup.sh.in @@ -0,0 +1,60 @@ +#!/bin/bash +# +# Copyright (c) 2024 Red Hat, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# shellcheck disable=SC2154 +# +depends() { + echo clevis + return 255 +} + +install() { + inst_hook initqueue 60 "${moddir}/clevis-pkcs11-prehook.sh" + inst_hook initqueue/settled 60 "${moddir}/clevis-pkcs11-hook.sh" + inst_hook initqueue/online 60 "${moddir}/clevis-pkcs11-hook.sh" + + inst_multiple \ + awk \ + pcscd \ + pkcs11-tool \ + head \ + sed \ + socat \ + tail \ + tr \ + /usr/lib64/pcsc/drivers/ifd-ccid.bundle/Contents/Linux/libccid.so \ + /usr/lib64/pcsc/drivers/ifd-ccid.bundle/Contents/Info.plist \ + /usr/lib64/opensc-pkcs11.so \ + /usr/lib64/pkcs11/opensc-pkcs11.so \ + /usr/lib64/libopensc.so* \ + /etc/opensc.conf \ + /usr/lib64/ossl-modules/legacy.so \ + /lib64/libpcsclite.so.1 \ + /usr/libexec/clevis-luks-pkcs11-askpass \ + /usr/libexec/clevis-luks-pkcs11-askpin \ + clevis-luks-common-functions \ + clevis-pkcs11-afunix-socket-unlock \ + clevis-pkcs11-common \ + clevis-decrypt-pkcs11 + + # Include libraries from configuration automatically + . /usr/bin/clevis-pkcs11-common + for file in $(clevis_get_module_path_from_pkcs11_config | sed -e 's@;@\n@g'); do + inst_multiple ${file} + done + dracut_need_initqueue +} diff --git a/src/luks/dracut/meson.build b/src/luks/dracut/meson.build index 7ad5b14c..99282309 100644 --- a/src/luks/dracut/meson.build +++ b/src/luks/dracut/meson.build @@ -3,3 +3,4 @@ subdir('clevis-pin-tang') subdir('clevis-pin-tpm2') subdir('clevis-pin-sss') subdir('clevis-pin-null') +subdir('clevis-pin-pkcs11') diff --git a/src/luks/systemd/clevis-luks-pkcs11-askpass.in b/src/luks/systemd/clevis-luks-pkcs11-askpass.in new file mode 100755 index 00000000..245ac24f --- /dev/null +++ b/src/luks/systemd/clevis-luks-pkcs11-askpass.in @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +/usr/libexec/clevis-luks-pkcs11-askpin -r & +# Wait 60 seconds to attend key requests from systemd +# If control socket starts receiving information, this time is cancelled +clevis-pkcs11-afunix-socket-unlock -l /run/systemd/clevis-pkcs11-systemd.log -f /run/systemd/clevis-pkcs11.sock -s 60 diff --git a/src/luks/systemd/clevis-luks-pkcs11-askpass.service.in b/src/luks/systemd/clevis-luks-pkcs11-askpass.service.in new file mode 100644 index 00000000..60b5871e --- /dev/null +++ b/src/luks/systemd/clevis-luks-pkcs11-askpass.service.in @@ -0,0 +1,8 @@ +[Unit] +Description=Unencrypt through PKCS11 +DefaultDependencies=no +PartOf=clevis-luks-pkcs11-askpass.socket + +[Service] +Type=simple +ExecStart=/usr/libexec/clevis-luks-pkcs11-askpass diff --git a/src/luks/systemd/clevis-luks-pkcs11-askpass.socket b/src/luks/systemd/clevis-luks-pkcs11-askpass.socket new file mode 100644 index 00000000..17d39c1c --- /dev/null +++ b/src/luks/systemd/clevis-luks-pkcs11-askpass.socket @@ -0,0 +1,10 @@ +[Unit] +Description=Clevis PKCS11 socket handler +Requires=clevis-luks-pkcs11-askpass.service +After=cryptsetup-pre.target sockets.target systemd-ask-password-wall.service + +[Socket] +ListenDatagram=/run/systemd/clevis-pkcs11.sock + +[Install] +WantedBy=cryptsetup-pre.target sockets.target systemd-ask-password-wall.service diff --git a/src/luks/systemd/clevis-luks-pkcs11-askpin.in b/src/luks/systemd/clevis-luks-pkcs11-askpin.in new file mode 100755 index 00000000..8f4092f7 --- /dev/null +++ b/src/luks/systemd/clevis-luks-pkcs11-askpin.in @@ -0,0 +1,203 @@ +#!/bin/bash +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +. clevis-luks-common-functions +. clevis-pkcs11-common + +pkcs11_device="" +dracut_mode=false +retry_mode=false +too_many_errors=3 + +while getopts ":dr" o; do + case "${o}" in + d) dracut_mode=true;; + r) retry_mode=true;; + *) ;; + esac +done + + +get_pkcs11_error() { + if journalctl -u clevis-luks-pkcs11-askpass.service -b 0 | tail -3 \ + | egrep -E "A TPM2 device.{1,}needed" >/dev/null 2>&1; + then + echo "ERROR:TPM2 device not found. " + elif journalctl -u clevis-luks-pkcs11-askpass.service -b 0 | tail -3 \ + | egrep -E "Error.{1,}server" >/dev/null 2>&1; + then + echo "ERROR:Tang communication error. " + elif journalctl -u clevis-luks-pkcs11-askpass.service -b 0 | tail -3 \ + | grep "Invalid PIN" >/dev/null 2>&1; + then + echo "ERROR:Invalid PIN. " + else + echo "ERROR:Unknown error. " + fi + return 0 +} + +if command -v pcscd; then + echo "clevis-pkcs11: starting pcscd if not available ..." + PCSCD_PID=$(ps auxf | grep "[p]cscd") + echo -e "clevis-pkcs11: pcscd running?:[${PCSCD_PID}]\n" + if ! ps auxf | grep "[p]cscd"; + then + if pcscd pcscd --help | grep disable-polkit 1>/dev/null 2>/dev/null; then + echo "clevis-pkcs11: starting pcscd with --disable-polkit option ..." + pcscd --disable-polkit + else + echo "clevis-pkcs11: starting pcscd ..." + pcscd + fi + fi +fi + +if [ "${dracut_mode}" != true ]; then + pkcs11-tool -L +fi + +if ! pkcs11_device=$(pkcs11-tool -L 2>/dev/null | grep "Slot" | head -1 | \ + awk -F ":" '{print $2}' | sed -e 's@^ *@@g'); then + echo "No PKCS11 device detected (without module option) / pkcs11-tool error" + exit 1 +fi + +if ! pkcs11-tool -O 2>/dev/null 1>/dev/null; then + pkcs11_device="" + echo "No objects in PKCS11 device detected" +fi + +while [ -z "${pkcs11_device}" ]; do + if [ "${dracut_mode}" != true ]; then + module_paths=$(clevis_get_module_path_from_pkcs11_config "/etc/crypttab") + if [ -n "${module_paths}" ]; then + modules=$(echo ${module_paths} | tr ";" "\n") + for module in $modules; do + pkcs11_device=$(pkcs11-tool -L --module ${module} | grep "Slot" \ + | head -1 | awk -F ":" '{print $2}' | sed -e 's@^ *@@g') + if [ -n "${pkcs11_device}" ]; then + break; + fi + done + fi + fi + if [ -z "${pkcs11_device}" ]; then + if [ "${retry_mode}" == true ]; then + option=$(systemd-ask-password --echo "Detected no PKCS#11 device, retry PKCS#11 detection? [yY/nN]") + if [ "${option}" == "N" ] || [ "${option}" == "n" ] ; then + echo "Won't continue PKCS11 device detection" + exit 0 + fi + pkcs11_device=$(pkcs11-tool -L | grep "Slot" \ + | head -1 | awk -F ":" '{print $2}' | sed -e 's@^ *@@g') + if ! pkcs11-tool -O 2>/dev/null; then + pkcs11_device="" + echo "No objects in PKCS11 device detected" + fi + else + exit 0 + fi + fi +done +echo "Detected PKCS11 device:${pkcs11_device}" + +devices_array=() +# Let's analyze all entries from /etc/crypttab that contain clevis-pkcs11.sock entries +while read -r line; +do + if echo "${line}" | grep -E "clevis-pkcs11.sock" 1>/dev/null; + then + next_device=0 + errors=0 + msg="" + while [ ${next_device} -ne 1 ]; do + uuid=$(echo "${line}" | awk '{print $2}') + if ! mapped_device=$(clevis_map_device "${uuid}"); then + echo "Could not check mapped device for UID:${uuid}" + next_device=1 + continue + fi + if [ "${dracut_mode}" != true ]; then + if grep "${mapped_device}" /run/systemd/clevis-pkcs11-dracut.devices; then + next_device=1 + continue + fi + fi + # If no PKCS#11 configuration, advance to next device + if ! clevis luks list -d "${mapped_device}" | grep pkcs11 >/dev/null 2>&1; then + echo "Device:${mapped_device} does not contain PKCS#11 configuration" + next_device=1 + continue + fi + # Get configuration PKCS#11 URI + uri=$(clevis luks list -d "${mapped_device}" | awk -F '"uri":' '{print $2}' | awk -F '"' '{print $2}' | awk -F '"' '{print $1}') + slot_opt="" + if ! slot=$(clevis_get_pkcs11_final_slot_from_uri "${uri}"); then + echo "Could not find slot for uri:${uri}" + else + slot_opt="--slot-index ${slot}" + fi + module_opt="" + module=$(clevis_get_module_path_from_uri "${uri}") + if [ -n "${module}" ]; then + module_opt="--module ${module}" + fi + echo "Device:${mapped_device}, slot_opt:${slot_opt}, module_opt:${module_opt}" + if ! pkcs11-tool -O ${module_opt} ${slot_opt}; then + echo "No objects on slot:${slot}, module_opt:${module_opt}" + next_device=1 + continue + fi + if ! model=$(clevis_get_model_from_uri "${uri}"); then + if ! model="device with serial number:$(clevis_get_serial_from_uri ${uri})"; then + model=${pkcs11_device} + fi + fi + if ! pin=$(clevis_get_pin_value_from_uri "${uri}"); then + pin=$(systemd-ask-password "${msg}Please, insert PIN for ${model} (${uuid}):") + fi + # Get key from PKCS11 pin here and feed AF_UNIX socket program + echo "${pin}" > /run/systemd/clevis-pkcs11.pin + if ! passphrase=$(clevis_luks_unlock_device "${mapped_device}") || [ -z "${passphrase}" ]; then + echo "Could not unlock device:${mapped_device}" + msg="$(get_pkcs11_error)" + ((errors++)) + if [ ${errors} -eq ${too_many_errors} ]; then + echo "Too many errors !!!" 1>&2 + next_device=1 + fi + continue + fi + next_device=1 + echo "Device:${mapped_device} unlocked successfully by clevis" + if [ "${dracut_mode}" == true ]; then + echo "${mapped_device}" >> /run/systemd/clevis-pkcs11-dracut.devices + fi + # Store passphrases to send to control socket + systemd_device=$(echo "${line}" | awk '{print $1}') + devices_array+=("${systemd_device},${passphrase}") + done + fi +done < <(grep -v "^#" /etc/crypttab) + +# Send passphrases to control socket +for ((ix=${#devices_array[*]}-1; ix>=0; ix--)) +do + echo -n "${devices_array[$ix]}" | socat UNIX-CONNECT:/run/systemd/clevis-pkcs11.control.sock - +done diff --git a/src/luks/systemd/meson.build b/src/luks/systemd/meson.build index b10494e3..eda879b4 100644 --- a/src/luks/systemd/meson.build +++ b/src/luks/systemd/meson.build @@ -19,15 +19,33 @@ if systemd.found() and sd_reply_pass.found() install_dir: unitdir, configuration: data, ) - + configure_file( + input: 'clevis-luks-pkcs11-askpass.service.in', + output: 'clevis-luks-pkcs11-askpass.service', + install_dir: unitdir, + configuration: data, + ) configure_file( input: 'clevis-luks-askpass.in', output: 'clevis-luks-askpass', install_dir: libexecdir, configuration: data ) + configure_file( + input: 'clevis-luks-pkcs11-askpass.in', + output: 'clevis-luks-pkcs11-askpass', + install_dir: libexecdir, + configuration: data + ) + configure_file( + input: 'clevis-luks-pkcs11-askpin.in', + output: 'clevis-luks-pkcs11-askpin', + install_dir: libexecdir, + configuration: data + ) install_data('clevis-luks-askpass.path', install_dir: unitdir) + install_data('clevis-luks-pkcs11-askpass.socket', install_dir: unitdir) else warning('Will not install systemd support due to missing dependencies!') endif diff --git a/src/pins/meson.build b/src/pins/meson.build index 12670ae8..a115e1eb 100644 --- a/src/pins/meson.build +++ b/src/pins/meson.build @@ -1,3 +1,4 @@ subdir('sss') subdir('tang') subdir('tpm2') +subdir('pkcs11') diff --git a/src/pins/pkcs11/clevis-decrypt-pkcs11 b/src/pins/pkcs11/clevis-decrypt-pkcs11 new file mode 100755 index 00000000..cb021781 --- /dev/null +++ b/src/pins/pkcs11/clevis-decrypt-pkcs11 @@ -0,0 +1,134 @@ +#!/bin/bash +set -eo pipefail +# vim: set ts=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Correia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +PIN_NAME=pkcs11 +PIN_FILE="/run/systemd/clevis-pkcs11.pin" +. clevis-pkcs11-common + +[ $# -eq 1 ] && [ "$1" == "--summary" ] && exit 2 + +if [ -t 0 ]; then + exec >&2 + echo + echo "Usage: clevis decrypt pkcs11 < JWE > PLAINTEXT" + echo + exit 2 +fi + +on_exit() { + [ -d "${CLEVIS_PKCS11}" ] || exit 0 + rm -rf "${CLEVIS_PKCS11}" +} + +unset CLEVIS_PKCS11 + +read -r -d . hdr64 + +# Check for corruption in the header. +if ! hdr="$(jose fmt --quote="${hdr64}" --string --b64load --object \ + --output=- 2>/dev/null)" ; then + echo "JWE header corrupt" >&2 + exit 1 +fi + +# Check if the pin is the expected one. +if ! pin="$(jose fmt --json="${hdr}" --get clevis --get pin \ + --unquote=- 2>/dev/null)" || [ -z "${pin}" ]; then + echo "Invalid JWE header: unable to identify 'pin'" >&2 + exit 1 +fi +if [ "${pin}" != "${PIN_NAME}" ]; then + echo "JWE pin mismatch: found: ${pin}; expected: ${PIN_NAME}" >&2 + exit 1 +fi + +if ! uri="$(jose fmt -j- -Og clevis -g "${PIN_NAME}" -g uri -Su- <<< "${hdr}")"; then + echo "URI missing required 'clevis.pkcs11.uri' header parameter!" >&2 + exit 1 +fi + +mechanism_option="" +mechanism="$(jose fmt -j- -Og clevis -g "${PIN_NAME}" -g mechanism -Su- <<< "${hdr}")" \ + 2>/dev/null || : +if [ -n "${mechanism}" ]; then + mechanism_option="--mechanism ${mechanism}" +fi + +if ! clevis_valid_pkcs11_uri "${uri}"; then + echo "PKCS#11 URI with invalid format:[${uri}]" >&2 + echo "PKCS#11 URI expected format:[${URI_EXPECTED_FORMAT}]" >&2 + exit 1 +fi + +module_opt="" +if module_path="$(clevis_get_module_path_from_uri ${uri})"; then + module_opt="--module ${module_path}" +fi + +if ! slot=$(clevis_get_pkcs11_final_slot_from_uri "${uri}"); then + slot_opt="" +else + slot_opt=" --slot-index=${slot}" +fi + +# Check if key parameter is present. +if ! enc_jwk="$(jose fmt --json="${hdr}" --get clevis --get "${PIN_NAME}" \ + --get key --unquote=- 2>/dev/null)" \ + || [ -z "${enc_jwk}" ]; then + echo "JWE missing 'clevis.${PIN_NAME}.key' header parameter" >&2 + exit 1 +fi + +if ! CLEVIS_PKCS11="$(mktemp -d)" || [ -z "${CLEVIS_PKCS11}" ]; then + echo "Creating a temporary dir for PKCS11 files failed" >&2 + exit 1 +fi +trap 'on_exit' EXIT + +# Error file +ERR="${CLEVIS_PKCS11}/decerr" + +# Decrypt the key. +ENC="${CLEVIS_PKCS11}/enc" +if ! printf '%s' "${enc_jwk}" | jose b64 dec -i- > "${ENC}" 2>"${ERR}"; then + cat "${ERR}" >&2 + echo "Unable to base64-decode the JWK" >&2 + exit 1 +fi + +PIN_value="" +if ! PIN_value="$(clevis_get_pin_value_from_uri ${uri})"; then + PIN_value=$(cat "${PIN_FILE}" 2>/dev/null || :) +fi + +if ! jwk="$(pkcs11-tool --login --decrypt --input-file ${ENC} \ + -p ${PIN_value} ${module_opt} ${mechanism_option} ${slot_opt} 2>${ERR})" \ + || [ -z "${jwk}" ]; then + cat "${ERR}" >&2 + echo "Unable to decrypt the JWK" >&2 + # TODO: Verify invalid PIN more accurately + echo "Invalid PIN?" >&2 + exit 1 +fi + +rm -rf "${PIN_FILE}" 2>/dev/null || : + +# Decrypt the data using the decrypted JWK. +( printf '%s' "${jwk}${hdr64}." ; cat ) | exec jose jwe dec --key=- --input=- diff --git a/src/pins/pkcs11/clevis-encrypt-pkcs11 b/src/pins/pkcs11/clevis-encrypt-pkcs11 new file mode 100755 index 00000000..2a477c8f --- /dev/null +++ b/src/pins/pkcs11/clevis-encrypt-pkcs11 @@ -0,0 +1,134 @@ +#!/bin/bash +set -eo pipefail +# vim: set ts=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Correia +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +PIN_NAME=pkcs11 + +SUMMARY="Encrypts using a PKCS#11 token" + +if [ "$1" == "--summary" ]; then + echo "$SUMMARY" + exit 0 +fi + +if [ -t 0 ]; then + exec >&2 + echo + echo "Usage: clevis encrypt pkcs11 CONFIG < PLAINTEXT > JWE" + echo + echo "$SUMMARY" + echo + echo "This command uses the following configuration properties:" + echo + echo " uri: The PKCS#11 URI (REQUIRED)" + echo + exit 2 +fi + +. clevis-pkcs11-common + +on_exit() { + [ -d "${CLEVIS_PKCS11}" ] || exit 0 + rm -rf "${CLEVIS_PKCS11}" +} +unset CLEVIS_PKCS11 + +# We first check whether the configuration is a valid JSON. +if ! cfg="$(jose fmt --json="$1" --object --output=-)" ; then + echo 'Configuration is malformed' >&2 + exit 1 +fi + +if ! uri="$(jose fmt -j- -Og uri -u- <<< "$cfg")"; then + uri="pkcs11:" +fi + +mechanism="$(jose fmt -j- -Og mechanism -u- <<< "$cfg" 2>/dev/null || :)" + +if ! clevis_valid_pkcs11_uri "${uri}"; then + echo "PKCS#11 URI with invalid format:[${uri}]" >&2 + echo "PKCS#11 URI expected format:[${URI_EXPECTED_FORMAT}]" >&2 + exit 1 +fi + +if ! module_path=$(clevis_get_module_path_from_uri "${uri}"); then + module_opt="" +else + module_opt=" --module ${module_path}" +fi + +if ! slot=$(clevis_get_pkcs11_final_slot_from_uri "${uri}"); then + slot_opt="" +else + slot_opt=" --slot-index=${slot}" +fi + +if ! CLEVIS_PKCS11="$(mktemp -d)" || [ -z "${CLEVIS_PKCS11}" ]; then + echo "Creating a temporary dir for PKCS11 files failed" >&2 + exit 1 +fi +trap 'on_exit' EXIT + +# Error file +ERR="${CLEVIS_PKCS11}/encerr" + +# Let's generate a key. +if ! jwk="$(jose jwk gen --input='{"alg":"A256GCM"}')" \ + || [ -z "${jwk}" ]; then + echo "Unable to generate JWK" >&2 + exit 1 +fi + +# Now let's encrypt it with the device public key. +PKEY="${CLEVIS_PKCS11}/pubkey" +if ! id=$(pkcs11-tool ${slot_opt} -O 2>${ERR} ${module_opt} \ + | grep -i 'Public' -A10 2>${ERR} | grep 'ID:' \ + | head -1 | awk -F 'ID:' '{print $2}' | tr -d ' '); then + cat "${ERR}" >&2 + echo "Unable to obtain public key ID from PKCS#11 device" >&2 + exit 1 +fi + +if ! pkcs11-tool ${slot_opt} ${module_opt} --read-object --type pubkey --id "${id}" \ + 2> "${ERR}" > "${PKEY}"; then + cat "${ERR}" >&2 + echo "Unable to obtain a public key from PKCS#11 device" >&2 + exit 1 +fi + +if ! jwk_enc="$(printf '%s' "${jwk}" | openssl rsautl -encrypt -pubin \ + -inkey "${PKEY}" 2>${ERR} \ + | jose b64 enc -I-)"; then + cat "${ERR}" >&2 + echo "Unable to encrypt JWK with PKCS#11 public key" >&2 + exit 1 +fi + +# And the JWE. +template=$(printf '{"protected":{"clevis":{"pin":"%s","%s":{"uri":"%s", "mechanism":"%s"}}}}' \ + "${PIN_NAME}" "${PIN_NAME}" "${uri}" "${mechanism}") + +# Save key. +jwe="$(jose fmt --json="${template}" --get protected --get clevis \ + --get "${PIN_NAME}" --quote "${jwk_enc}" --set key -UUUU --output=-)" + +# Now we encrypt the data using our key. +( printf '%s' "${jwe}${jwk}" ; cat ) | exec jose jwe enc \ + --input=- --key=- \ + --detached=- --compact diff --git a/src/pins/pkcs11/clevis-encrypt-pkcs11.1.adoc b/src/pins/pkcs11/clevis-encrypt-pkcs11.1.adoc new file mode 100644 index 00000000..f41119be --- /dev/null +++ b/src/pins/pkcs11/clevis-encrypt-pkcs11.1.adoc @@ -0,0 +1,78 @@ +CLEVIS-ENCRYPT-PKCS11(1) +======================== +:doctype: manpage + + +== NAME + +clevis-encrypt-pkcs11 - Encrypts using a PKCS#11 device + +== SYNOPSIS + +*clevis encrypt pkcs11* CONFIG < PT > JWE + +== OVERVIEW + +The *clevis encrypt pkcs11* command encrypts using a PKCS#11 device. +Its only argument is the JSON configuration object. + +When using this pin, we create a new random key which is encrypted using the PKCS#11 chip. +Then at decryption time, the key is decrypted again using the PKCS#11 chip, normally, +by providing a PIN (Personal Identity Number) at boot time. +Configuration object must be provided with *uri* JSON key, and JSON value +associated to *uri* key must start with *pkcs11:* word: + + $ clevis encrypt pkcs11 '{"uri":"pkcs11:"}' < PT > JWE + +As an alternative, PIN can be stored at configuration time. For security reasons, +this is NOT recommended. But, if still required, it can be done through +*pin-value* parameter: + + $ clevis encrypt pkcs11 '{"uri":"pkcs11:?pin-value=123456"}' < PT > JWE + +In case it is required to provide a module library, it can be done through URI +*module-path" parameter: + + $ clevis encrypt pkcs11 '{"uri":"pkcs11:module-path=/usr/lib64/libykcs11.so"}' < PT > JWE + +Clevis will be used in top of OpenSC to provide PKCS#11 functionality. OpenSC, and, +in particular, `pkcs11-tool`, provides an option to indicate the mechanism to use for decryption. +For testing purposes, some libraries, such as https://www.opendnssec.org/softhsm[SoftHSM], +don't work with default `pkcs11-tool` mechanism, so it is required to provide a particular +mechanism for them to work. For this reason, Clevis can be provided with +the mechanism to use, in case the default one, *RSA-PKCS-OAEP*, is not valid: + + $ clevis luks bind -d /dev/sda1 pkcs11 '{"uri": "pkcs11:", "mechanism":"RSA-PKCS"}' + +To decrypt the data, simply provide the ciphertext (JWE): + + $ clevis decrypt < JWE > PT + +Note that like other pins no configuration is used for decryption, this is due +clevis storing the public and private keys to unseal the encrypted object +in the JWE so clevis can fetch that information from there. + +== CONFIG + +This command uses the following configuration properties: + +* *uri* (string) : + The PKCS#11 URI to use (REQUIRED) + +* *mechanism* (string) : + Mechanism to be used when working with pkcs11-tool. + It must be supported by pkcs11-tool. Examples of supported mechanisms are: + - *RSA-PKCS-OAEP* (default one) + - *RSA-PKCS* + +For a complete list of supported mechanisms, execute next command: + + $ pkcs11-tool -M + +It must be highlighted that previous command will show if the mechanism allows +encryption/decryption, something that is required for PKCS#11 Clevis pin to work +appropriately. + +== SEE ALSO + +link:clevis-decrypt.1.adoc[*clevis-decrypt*(1)] diff --git a/src/pins/pkcs11/clevis-pkcs11-afunix-socket-unlock.c b/src/pins/pkcs11/clevis-pkcs11-afunix-socket-unlock.c new file mode 100644 index 00000000..a6ecc631 --- /dev/null +++ b/src/pins/pkcs11/clevis-pkcs11-afunix-socket-unlock.c @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2024 Red Hat, Inc. + * Author: Sergio Arroutbi + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef GIT_VERSION +const char* VERSION = GIT_VERSION; +#else +const char* VERSION = "v0.0.1"; +#endif + +#define MAX_DEVICE 1024 +#define MAX_ENTRIES 1024 +#define MAX_KEY 1024 + +const uint16_t DEFAULT_MAX_ITERATIONS = 3; +const uint16_t MAX_PATH = 1024; +const uint16_t MAX_CONTROL_MSG = 1024; +const uint8_t WAIT_CONTROL_THREAD_TIMER = 1; + +// Time to wait before trying to write key +const uint16_t DEFAULT_START_DELAY = 0; + +typedef struct { + char dev[MAX_DEVICE+1]; + char key[MAX_KEY+1]; +} key_entry_t; +key_entry_t keys[MAX_ENTRIES]; +uint16_t entry_counter = 0; +uint8_t thread_loop = 1; +uint8_t control_thread_info = 0; +pthread_mutex_t mutex; +FILE* logfile = NULL; + +static void +get_control_socket_name(const char* file_sock, char* control_sock, uint32_t control_sock_len) { + char *p = strstr(file_sock, ".sock"); + size_t prefix_length = strlen(file_sock) - strlen(p); + memset(control_sock, 0, control_sock_len); + memcpy(control_sock, file_sock, prefix_length); + if (prefix_length + strlen(".control.sock") < control_sock_len) { + strcat(control_sock + prefix_length, ".control.sock"); + } +} + +static void insert_device(const char* dev) { + if(MAX_ENTRIES == entry_counter) { + perror("No more entries accepted\n"); + return; + } + pthread_mutex_lock(&mutex); + strncpy(keys[entry_counter].dev, dev, MAX_DEVICE); + pthread_mutex_unlock(&mutex); +} + +static void insert_key(const char* key) { + if(MAX_ENTRIES == entry_counter) { + perror("No more entries accepted\n"); + return; + } + pthread_mutex_lock(&mutex); + strncpy(keys[entry_counter++].key, key, MAX_KEY); + pthread_mutex_unlock(&mutex); +} + +static const char* get_key(const char* dev) { + for(int e = 0; e < entry_counter; e++) { + pthread_mutex_lock(&mutex); + if(0 == strcmp(keys[e].dev, dev)) { + pthread_mutex_unlock(&mutex); + return keys[e].key; + } + pthread_mutex_unlock(&mutex); + } + return NULL; +} + +static void* control_thread(void *targ) { + // Create a socket to listen on control socket + struct sockaddr_un control_addr, accept_addr; + int s = 0, a = 0, r = 0; + char control_msg[MAX_CONTROL_MSG+1]; + const char* control_sock = (const char*)targ; + socklen_t len = 0; + memset(&control_addr, 0, sizeof(control_addr)); + control_addr.sun_family = AF_UNIX; + strncpy(control_addr.sun_path, control_sock, sizeof(control_addr.sun_path)-1); + unlink(control_sock); + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s == -1) { + perror("control socket"); + fprintf(logfile, "Control socket error\n"); + pthread_exit("control socket"); + } + if (bind(s, (struct sockaddr *)&control_addr, sizeof(control_addr)) == -1) { + perror("control bind"); + fprintf(logfile, "Control bind error\n"); + pthread_exit("control bind"); + } + if (listen(s, SOMAXCONN) == -1) { + perror("control listen"); + fprintf(logfile, "Control listen error\n"); + pthread_exit("control listen"); + } + while (thread_loop) { + a = accept(s, (struct sockaddr *)&accept_addr, &len); + if (a == -1) { + perror("control accept"); + fprintf(logfile, "Control accept\n"); + pthread_exit("control accept"); + } + memset(control_msg, 0, MAX_CONTROL_MSG); + if((r = recv(a, control_msg, MAX_CONTROL_MSG, 0)) < 0) { + perror("recv error"); + fprintf(logfile, "Error on reception\n"); + close(a); + pthread_exit("control recv"); + } else { + control_msg[r] = '\0'; + } + char* t = control_msg; + int is_device = 1; + fprintf(logfile, "Received control message:[%s]\n", t); + while((t = strtok(t, ","))) { + if (is_device) { + fprintf(logfile, "Adding device:%s\n", t); + insert_device(t); + is_device = 0; + } else { + fprintf(logfile, "Adding key:%s\n", t); + insert_key(t); + // As long as some key is inserted, we store it + // in the control_thread_info variable + control_thread_info = 1; + } + t = strtok(NULL, ","); + } + close(a); + } + return NULL; +} + +static int usage(const char* name, uint32_t ecode) { + printf("\nUsage:\n\t%s -f socket_file [-c control_socket] [-k key] " + "[-l logfile] [-t iterations, 3 by default]" + "[-s start delay, 0s by default] [-v(version)] [-h(help)]\n\n", name); + exit(ecode); +} + +static void dump_version(void) { + printf("VERSION: [%s]\n", VERSION); +} + +static void dump_wide_version(void) { + printf("\n"); + dump_version(); + printf("\n"); +} + +static void int_handler(int s) { + if(logfile) { + fprintf(logfile, "Closing, signal:[%d]\n", s); + fclose(logfile); + } + exit(EXIT_FAILURE); +} + +int main(int argc, char* argv[]) { + int s, a, opt; + struct sockaddr_un sock_addr, accept_addr, peer_addr; + socklen_t pathlen; + char key[MAX_KEY]; + char lfile[MAX_PATH]; + char sock_file[MAX_PATH]; + char sock_control_file[MAX_PATH]; + socklen_t len = sizeof(accept_addr); + uint8_t wait_control_thread = 1; + uint32_t iterations = DEFAULT_MAX_ITERATIONS; + uint32_t startdelay = DEFAULT_START_DELAY; + uint32_t ic = 0; + uint32_t time = 0; + memset(lfile, 0, MAX_PATH); + memset(sock_file, 0, MAX_PATH); + memset(sock_control_file, 0, MAX_PATH); + memset(key, 0, MAX_KEY); + + signal(SIGTERM | SIGKILL, int_handler); + for (uint16_t e = 0; e < MAX_ENTRIES; e++) { + memset(&keys[e], 0, sizeof(key_entry_t)); + } + while ((opt = getopt(argc, argv, "c:f:k:i:l:s:t:hv")) != -1) { + int ret_code = EXIT_FAILURE; + switch (opt) { + case 'c': + strncpy(sock_control_file, optarg, MAX_PATH - 1); + break; + case 'f': + strncpy(sock_file, optarg, MAX_PATH - 1); + break; + case 'k': + strncpy(key, optarg, MAX_KEY - 1); + break; + case 'l': + strncpy(lfile, optarg, MAX_PATH - 1); + logfile = fopen(lfile, "w+"); + break; + case 't': + iterations = strtoul(optarg, 0, 10); + break; + case 's': + startdelay = strtoul(optarg, 0, 10); + break; + case 'v': + dump_wide_version(); + exit(EXIT_SUCCESS); + break; + case 'h': + ret_code = EXIT_SUCCESS; + __attribute__ ((fallthrough)); + default: + usage(argv[0], ret_code); + } + } + if(!logfile) { + logfile = stdout; + strncpy(lfile, "stdout", MAX_PATH - 1); + } + if(0 == strlen(sock_file)) { + fprintf(logfile, "\nSocket file name must be provided\n"); + usage(argv[0], EXIT_FAILURE); + } + if(0 == strlen(sock_control_file) ) { + get_control_socket_name(sock_file, sock_control_file, MAX_PATH); + } + fprintf(logfile, "LOG FILE: [%s]\n", lfile); + fprintf(logfile, "FILE: [%s]\n", sock_file); + fprintf(logfile, "KEY: [%s]\n", key); + fprintf(logfile, "START DELAY: [%u] seconds\n", startdelay); + fprintf(logfile, "TRY ITERATIONS: [%u]\n", iterations); + dump_version(); + + pthread_t thid; + void* tret; + if (pthread_create(&thid, NULL, control_thread, sock_control_file) != 0) { + perror("pthread_create() error"); + goto efailure; + } + + memset(&sock_addr, 0, sizeof(sock_addr)); + sock_addr.sun_family = AF_UNIX; + strncpy(sock_addr.sun_path, sock_file, sizeof(sock_addr.sun_path)-1); + unlink(sock_file); + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s == -1) { + perror("socket"); + goto efailure; + } + if (bind(s, (struct sockaddr *)&sock_addr, sizeof(sock_addr)) == -1) { + perror("bind"); + goto efailure; + } + if (listen(s, SOMAXCONN) == -1) { + perror("listen"); + goto efailure; + } + while (ic < iterations) { + if (time++ < startdelay && !control_thread_info) { + sleep(1); + fprintf(logfile, "Start time elapsed: [%u/%u] seconds\n", + time, startdelay); + continue; + } + if (control_thread_info && wait_control_thread) { + sleep(WAIT_CONTROL_THREAD_TIMER); + fprintf(logfile, "Waiting %d second for control thread " + "to receive complete information\n", + WAIT_CONTROL_THREAD_TIMER); + wait_control_thread = 0; + } + a = accept(s, (struct sockaddr *)&accept_addr, &len); + if (a == -1) { + perror("accept"); + goto efailure; + } + pathlen = len - offsetof(struct sockaddr_un, sun_path); + len = sizeof(peer_addr); + if (getpeername(a, (struct sockaddr *)&peer_addr, &len)== -1) { + perror("getpeername"); + goto efailure; + } + pathlen = len - offsetof(struct sockaddr_un, sun_path); + char peer[pathlen]; + memset(peer, 0, pathlen); + strncpy(peer, peer_addr.sun_path+1, pathlen-1); + fprintf(logfile, "Try: [%u/%u]\n", ic, iterations); + fprintf(logfile, "getpeername sun_path(peer): [%s]\n", peer); + char* t = peer; + const char* unlocking_device = ""; + while((t = strtok(t, "/"))) { + if(t) { + unlocking_device = t; + } + t = strtok(NULL, ","); + } + fprintf(logfile, "Trying to unlock device:[%s]\n", unlocking_device); + // Now we have all the information in peer, something like: + // \099226072855ae2d8/cryptsetup/luks-6e38d5e1-7f83-43cc-819a-7416bcbf9f84 + // NUL random /cryptsetup/ DEVICE + // If we need to unencrypt device, pick it from peer information + // To return the key, just respond to socket returned by accept + if(strlen(key)) { + if (send(a, key, strlen(key), 0) < 0) { + perror("key send error"); + goto efailure; + } + } else { + const char* entry_key; + if((entry_key = get_key(unlocking_device))) { + if (send(a, entry_key, strlen(entry_key), 0)< 0) { + perror("key entry send error"); + goto efailure; + } + fprintf(logfile, "Sending:[%s] to device:[%s]\n", + entry_key, unlocking_device); + } else { + fprintf(logfile, "Device not found: [%s]\n", unlocking_device); + } + } + close(a); + ic++; + } + fprintf(logfile, "Closing (max tries reached)\n"); + pthread_kill(thid, SIGKILL); + thread_loop = 0; + if (pthread_join(thid, &tret) != 0) { + perror("pthread_join error"); + goto efailure; + } + return EXIT_SUCCESS; +efailure: + if(logfile) { + fclose(logfile); + logfile = NULL; + } + exit(EXIT_FAILURE); +} diff --git a/src/pins/pkcs11/clevis-pkcs11-common b/src/pins/pkcs11/clevis-pkcs11-common new file mode 100755 index 00000000..4c0629c4 --- /dev/null +++ b/src/pins/pkcs11/clevis-pkcs11-common @@ -0,0 +1,245 @@ +#!/bin/bash +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +if [ "$1" = "--summary" ]; then + exit 1 +fi + +. clevis-luks-common-functions + +serial_devices_array="" + +URI_EXPECTED_FORMAT="pkcs11:" +DEFAULT_CRYPTTAB_FILE="/etc/crypttab" + +clevis_parse_devices_array() { + INPUT_ARRAY=$(pkcs11-tool -L | grep Slot) + counter=0 + while read -r; do + serial=$(pkcs11-tool -L | sed -n "/Slot ${counter}/,/Slot $((counter+1))/p" | grep -i "serial num"| awk -F ":" '{print $2}' | tr -d ' ') + serial_devices_array[$counter]="${serial}" + ((counter++)) + done <<< "${INPUT_ARRAY}" +} + +clevis_get_serial_by_slot() { + clevis_parse_devices_array + serial=${serial_devices_array[$1]} + if [ -z "${serial}" ]; then + return 1 + fi + echo "${serial}" + return 0 +} + +clevis_valid_pkcs11_uri() { + echo "$1" | grep -E "^${URI_EXPECTED_FORMAT}" >/dev/null 2>&1 || return 1 +} + +clevis_get_module_path_from_uri() { + echo "$1" | grep -E "module-path=" >/dev/null 2>&1 || return 1 + echo "$1" | awk -F 'module-path=' '{print $2}' | awk -F ";" '{print $1}' \ + | awk -F "?" '{print $1}' +} + +clevis_get_module_path_from_pkcs11_config() { + CRYPTTABFILE="$1" + module_list="" + [ -z "${CRYPTTABFILE}" ] && CRYPTTABFILE="${DEFAULT_CRYPTTAB_FILE}" + while read -r line; do + uuid=$(echo "${line}" | awk '{print $2}') + if ! mapped_device=$(clevis_map_device "${uuid}"); then + echo "Could not check mapped device for UID:${uuid}" + continue + fi + # If no PKCS#11 configuration, advance to next device + if ! clevis luks list -d "${mapped_device}" | grep pkcs11 >/dev/null 2>&1; then + echo "Device:${mapped_device} does not contain PKCS#11 configuration" + continue + fi + # Get configuration PKCS#11 URI + uri=$(clevis luks list -d "${mapped_device}" | awk -F '"uri":' '{print $2}' \ + | awk -F '"' '{print $2}' | awk -F '"' '{print $1}') + if module_path=$(clevis_get_module_path_from_uri "${uri}"); then + if [ -z "${module_list}" ]; then + module_list="${module_path}" + else + module_list="${module_list};${module_path}" + fi + fi + done < <(grep -v "^#" "${CRYPTTABFILE}") + echo "${module_list}" +} + +clevis_get_pin_value_from_uri() { + echo "$1" | grep -E "pin-value=" >/dev/null 2>&1 || return 1 + echo "$1" | awk -F 'pin-value=' '{print $2}' | awk -F ";" '{print $1}' +} + +clevis_get_slot_from_uri() { + echo "$1" | grep -E "slot-id=" >/dev/null 2>&1 || return 1 + echo "$1" | awk -F 'slot-id=' '{print $2}' | awk -F ";" '{print $1}' \ + | awk -F "?" '{print $1}' +} + +clevis_percent_unencoding() { + echo "$1" | sed -E 's@%20@ @g' | sed -E 's@%21@!@g' | sed -E 's@%22@"@g' \ + | sed -E 's@%23@#@g' | sed -E 's@%24@$@g' | sed -E 's@%25@%@g' \ + | sed -E 's@%26@&@g' | sed -E "s@%27@'@g" | sed -E 's@%28@(@g' \ + | sed -E 's@%29@)@g' | sed -E "s@%2A@*@g" | sed -E 's@%2B@+@g' \ + | sed -E 's@%2C@,@g' | sed -E "s@%2F@/@g" | sed -E 's@%3A@:@g' \ + | sed -E 's@%3B@;@g' | sed -E "s@%3D@=@g" | sed -E 's@%3F@?@g' \ + | sed -E 's!%40!@!g' | sed -E "s@%5B@[@g" | sed -E 's@%5D@]@g' +} + +clevis_get_model_from_uri() { + if ! echo "$1" | grep -E "model=" >/dev/null 2>&1; then + return 1 + fi + model=$(echo "$1" | awk -F 'model=' '{print $2}' | awk -F ";" '{print $1}' \ + | awk -F "?" '{print $1}') + clevis_percent_unencoding "${model}" +} + +clevis_get_token_from_uri() { + if ! echo "$1" | grep -E "token=" >/dev/null 2>&1; then + return 1 + fi + token=$(echo "$1" | awk -F 'token=' '{print $2}' | awk -F ";" '{print $1}' \ + | awk -F "?" '{print $1}') + clevis_percent_unencoding "${token}" +} + +clevis_get_serial_from_uri() { + if ! echo "$1" | grep -E "serial=" >/dev/null 2>&1; then + return 1 + fi + serial=$(echo "$1" | awk -F 'serial=' '{print $2}' | awk -F ";" '{print $1}' \ + | awk -F "?" '{print $1}') + clevis_percent_unencoding "${serial}" +} + +clevis_get_max_pkcs11_slot() { + if [ -z "${1}" ]; then + module_opt="" + else + module_opt="--module ${1}" + fi + if ! pkcs11-tool -L ${module_opt} | grep -i "^slot" | awk '{print $2}' | tail -1; then + echo "-1" + fi +} + +clevis_pkcs11_filter_uri() { + echo "$1" | sed -E 's@module-path=[a-z,A-Z,0-9,/,.]{1,};{0,1}@@' | sed -E 's@;$@@g' +} + +clevis_get_slot_by_serial_from_uri() { + module_path="" + if ! module_path=$(clevis_get_module_path_from_uri "${1}"); then + module_opt="" + else + module_opt=" --module ${module_path}" + fi + serialuri=$(clevis_get_serial_from_uri "${1}") + for ((s=0; s<=$(clevis_get_max_pkcs11_slot "${module_path}"); s++)) + do + if ! pkcs11-tool -O --slot-index=${s} 1>/dev/null 2>/dev/null; then + continue + fi + serial=$(clevis_get_serial_by_slot "${s}") + if [ "${serial}" = "${serialuri}" ]; then + echo "${s}" + return 0 + fi + done + return 1 +} + +clevis_get_slot_by_serial_and_token_from_uri() { + module_path="" + if ! module_path=$(clevis_get_module_path_from_uri "${1}"); then + module_opt="" + else + module_opt=" --module ${module_path}" + fi + serialuri=$(clevis_get_serial_from_uri "${1}") + tokenuri="$(clevis_get_token_from_uri "${1}")" + for ((s=0; s<=$(clevis_get_max_pkcs11_slot "${module_path}"); s++)) + do + if pkcs11-tool -L ${module_opt} 2>/dev/null | \ + grep -i 'serial num' | head -$((s+1)) | tail -1 | \ + awk -F ':' '{print $2}' | tr -d ' ' | \ + grep "${serialuri}" 2>/dev/null 1>/dev/null; then + pkcs11tokenuri="$(pkcs11-tool -L ${module_opt} 2>/dev/null \ + | grep -i 'token label' | head -$((s+1)) | tail -1 \ + | awk -F ':' '{print $2}' | sed -E 's@^ {0,}@@g')" + if [ "${tokenuri}" = "${pkcs11tokenuri}" ]; then + echo "${s}" + return 0 + fi + fi + done + return 1 +} + +clevis_get_pkcs11_pubkey_slot_from_uri() { + module_path="" + if ! module_path=$(clevis_get_module_path_from_uri "${1}"); then + module_opt="" + else + module_opt=" --module ${module_path}" + fi + uri=$(clevis_pkcs11_filter_uri "${1}") + for ((s=0; s<=$(clevis_get_max_pkcs11_slot "${module_path}"); s++)) + do + if pkcs11-tool -O ${module_opt} --slot-index ${s} --type pubkey 2>/dev/null \ + | grep -i 'uri:' | awk -F 'uri:' '{print $2}' | tr -d ' ' \ + | grep "${uri}" 2>/dev/null 1>/dev/null; then + echo "${s}" + return 0 + fi + done + return 1 +} + +clevis_get_pkcs11_final_slot_from_uri() { + if slot=$(clevis_get_slot_from_uri "${1}"); then + echo "${slot}" + return 0 + fi + if [ -z "${slot}" ]; then + if slot=$(clevis_get_pkcs11_pubkey_slot_from_uri "${1}"); then + echo "${slot}" + return 0 + fi + fi + if [ -z "${slot}" ]; then + if slot=$(clevis_get_slot_by_serial_and_token_from_uri "${1}"); then + echo "${slot}" + return 0 + fi + fi + if [ -z "${slot}" ]; then + if slot=$(clevis_get_slot_by_serial_from_uri "${1}"); then + echo "${slot}" + return 0 + fi + fi + return 1 +} diff --git a/src/pins/pkcs11/meson.build b/src/pins/pkcs11/meson.build new file mode 100644 index 00000000..12852859 --- /dev/null +++ b/src/pins/pkcs11/meson.build @@ -0,0 +1,43 @@ +pcscd = find_program('pcscd', required: false) +pkcs11tool = find_program('pkcs11-tool', required: false) +pcscd_disable_polkit = false +git = find_program('git', required: false) + +if git.found() + git_version_cmd = run_command('git', 'rev-parse', '--short', 'HEAD', check: false) + git_version = 'release-v' + meson.project_version() + '-' + git_version_cmd.stdout().strip() +else + git_version = 'release-v' + meson.project_version() + '-' + 'UNKNOWN_GIT_VERSION' +endif +GIT_VERSION_FLAG = '-DGIT_VERSION="' + git_version + '"' + +if pcscd.found() + pcscd_options = run_command('pcscd', '--help', check: false) + pcscd_disable_polkit = pcscd_options.stdout().strip().contains('disable-polkit') + if not pcscd_disable_polkit + warning('pcscd does not have --disable-polkit option') + endif +endif + +if pcscd.found() and pkcs11tool.found() + bins += join_paths(meson.current_source_dir(), 'clevis-decrypt-pkcs11') + bins += join_paths(meson.current_source_dir(), 'clevis-encrypt-pkcs11') + bins += join_paths(meson.current_source_dir(), 'clevis-pkcs11-common') + mans += join_paths(meson.current_source_dir(), 'clevis-encrypt-pkcs11.1') + ### TODO: Include man pages + # mans += join_paths(meson.current_source_dir(), 'clevis-decrypt-pkcs11.1') + subdir('tests') + executable('clevis-pkcs11-afunix-socket-unlock', ['clevis-pkcs11-afunix-socket-unlock.c'], + install_dir: bindir, + install: true, + c_args: GIT_VERSION_FLAG + ) +else + warning('Will not install pkcs11 pin due to missing dependencies!') + if not pcscd.found() + warning('pcscd not found') + endif + if not pkcs11tool.found() + warning('pkcs11-tool not found') + endif +endif diff --git a/src/pins/pkcs11/tests/meson.build b/src/pins/pkcs11/tests/meson.build new file mode 100644 index 00000000..9212b60f --- /dev/null +++ b/src/pins/pkcs11/tests/meson.build @@ -0,0 +1,23 @@ +env = environment() +env.prepend('PATH', + join_paths(meson.source_root(), 'src'), + join_paths(meson.source_root(), 'src', 'pins', 'pkcs11'), + join_paths(meson.source_root(), 'src', 'pins', 'pkcs11', 'tests'), + join_paths(meson.build_root(), 'src'), + join_paths(meson.build_root(), 'src', 'pins'), + join_paths(meson.build_root(), 'src', 'pins', 'pkcs11'), + join_paths(meson.build_root(), 'src', 'pins', 'pkcs11', 'tests'), + join_paths(meson.source_root(), 'src'), + join_paths(meson.source_root(), 'src', 'luks'), + join_paths(meson.source_root(), 'src', 'luks', 'tests'), + join_paths(meson.build_root(), 'src'), + join_paths(meson.build_root(), 'src', 'luks'), + join_paths(meson.build_root(), 'src', 'luks', 'tests'), + join_paths(meson.source_root(), 'src', 'pins', 'tang'), + join_paths(meson.source_root(), 'src', 'pins', 'tang', 'tests'), + join_paths(meson.build_root(), 'src', 'pins', 'tang'), + join_paths(meson.build_root(), 'src', 'pins', 'tang', 'tests'), + separator: ':' +) + +test('pin-pkcs11', find_program('pin-pkcs11'), env: env) diff --git a/src/pins/pkcs11/tests/pin-pkcs11 b/src/pins/pkcs11/tests/pin-pkcs11 new file mode 100755 index 00000000..b7ec4032 --- /dev/null +++ b/src/pins/pkcs11/tests/pin-pkcs11 @@ -0,0 +1,161 @@ +#!/bin/bash -xe +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# shellcheck disable=SC1091 +. pkcs11-common-tests +. tests-common-functions +. clevis-luks-common-functions + +on_exit() { + exit_status=$? + [ -d "$TMP" ] && rm -rf "$TMP" + exit "${exit_status}" +} + +if [[ ! -f "${P11LIB}" ]]; then + echo "WARNING: The SoftHSM is not installed. Can not run this test" + exit 77; +fi + +trap 'on_exit' EXIT + +TMP="$(mktemp -d)" + +softhsm_lib_setup +test "$?" == 0 + +SECRET_WORD="secret" +SUPPORTED_MECHANISM="RSA-PKCS" +CLEVIS_PIN="pkcs11" +DEFAULT_SLOT="0" + +sword=$(echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:module-path=${P11LIB}?pin-value=${PIN}\"\ +,\"mechanism\":\"${SUPPORTED_MECHANISM}\"}" | clevis decrypt) +test "${sword}" == "${SECRET_WORD}" + +sword=$(echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:module-path=${P11LIB};slot=${DEFAULT_SLOT}\ +?pin-value=${PIN}\",\"mechanism\":\"${SUPPORTED_MECHANISM}\"}" \ +| clevis decrypt) +test "${sword}" == "${SECRET_WORD}" + +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:module-path=${P11LIB}?pin-value=${PIN}\" \ +,\"mechanism\":\"INVALID_MECHANISM\"}" 2>/dev/null + +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:module-path=${P11LIB}?pin-value=${PIN}\"\ +,\"mechanism\":\"INVALID_MECHANISM\"}" 2>/dev/null + +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:module-path=${P11LIB};slot=1?pin-value=${PIN}\" \ +,\"mechanism\":\"${SUPPORTED_MECHANISM}\"}" 2>/dev/null + +sword=$(echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:module-path=${P11LIB}?pin-value=INVALID_PIN\"\ +,\"mechanism\":\"${SUPPORTED_MECHANISM}\"}" 2>/dev/null | clevis decrypt 2>/dev/null || :) +test "${sword}" != "${SECRET_WORD}" 2>/dev/null + +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:module-path=/usr/lib/wrong_modulepath.so?pin-value=${PIN}\"\ +,\"mechanism\":\"${SUPPORTED_MECHANISM}\"}" 2>/dev/null + +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 \ +"{\"uri\":\"pkcs11:?pin-value=${PIN}\"\ +,\"mechanism\":\"${SUPPORTED_MECHANISM}\"}" 2>/dev/null + +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 "{\"uri\":\"pkcs12:\"}" \ +2>/dev/null +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 "{\"uri\":\":\"}" 2>/dev/null +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 "{\"uri\":\"\"}" 2>/dev/null +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 "{\"uri\":}" 2>/dev/null +! echo "${SECRET_WORD}" | clevis encrypt pkcs11 "{}" 2>/dev/null + +# Let's try some bindings +# LUKS2. +DEV="${TMP}/luks2-device" +new_device "luks2" "${DEV}" + +CFG=$(printf '{"uri": "pkcs11:module-path=%s?pin-value=%s", "mechanism":"%s"}' \ +"${P11LIB}" "${PIN}" "${SUPPORTED_MECHANISM}") +if ! clevis luks bind -f -d "${DEV}" "${CLEVIS_PIN}" "${CFG}" <<< \ +"${DEFAULT_PASS}"; then + error "${TEST}: Binding is expected to succeed when given a correct \ +(${DEFAULT_PASS}) password." +fi + +SLT=1 +if ! read -r slot pin cfg < <(clevis luks list -d "${DEV}" -s "${SLT}"); then + error "${TEST}: clevis luks list is expected to succeed for device(${DEV}) \ +and slot (${SLT})" +fi + +if [[ "${slot}" != "${SLT}:" ]]; then + error "${TEST}: slot (${slot}) is expected to be ${SLT}" +fi + +if [[ "${pin}" != "${CLEVIS_PIN}" ]]; then + error "${TEST}: pin (${pin}) is expected to be '${CLEVIS_PIN}'" +fi + +# Check configuration has "uri:" +if ! [[ "${cfg}" == *"uri"* ]]; then + error "${TEST}: configuration (${cfg}) is expected to be contain uri" +fi + +# Test the passphrase +SLT=1 +PASS=$(clevis luks pass -d "${DEV}" -s "${SLT}") +if ! clevis_luks_check_valid_key_or_keyfile "${DEV}" "${PASS}" "" "${SLT}"; then + error "Passphrase obtained from clevis luks pass failed." +fi + +if ! clevis luks unbind -f -d "${DEV}" -s "${SLT}"; then + error "${TEST}: Unbind is expected to succeed for device ${DEV} and slot ${SLT}" +fi + +SLT=0 +if clevis luks unbind -f -d "${DEV}" -s "${SLT}"; then + error "${TEST}: Unbind is expected to fail for device ${DEV}:${SLT} \ +that is not bound with clevis" +fi + +WRONGCFG=$(printf '{"uri": "pkcs12:"}') +if clevis luks bind -f -d "${DEV}" "${CLEVIS_PIN}" "${WRONGCFG}" <<< "${DEFAULT_PASS}"; \ +then + error "${TEST}: Binding is expected to fail when given an incorrect configuration:\ +(${WRONGCFG})" +fi + +WRONGCFG=$(printf '{}') +if clevis luks bind -f -d "${DEV}" "${CLEVIS_PIN}" "${WRONGCFG}" <<< "${DEFAULT_PASS}"; \ +then + error "${TEST}: Binding is expected to fail when given an empty configuration:\ +(${WRONGCFG})" +fi + +WRONGCFG=$(printf '{"uri":""}') +if clevis luks bind -f -d "${DEV}" "${CLEVIS_PIN}" "${WRONGCFG}" <<< "${DEFAULT_PASS}"; \ +then + error "${TEST}: Binding is expected to fail when given an empty uri:\ +(${WRONGCFG})" +fi + +softhsm_lib_cleanup +test "$?" == 0 diff --git a/src/pins/pkcs11/tests/pkcs11-common-tests b/src/pins/pkcs11/tests/pkcs11-common-tests new file mode 100644 index 00000000..2a5226ab --- /dev/null +++ b/src/pins/pkcs11/tests/pkcs11-common-tests @@ -0,0 +1,106 @@ +#!/bin/bash +# +# Copyright (c) 2024 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# +# This file is based on OpenSC common test infrastructure: +# https://github.com/OpenSC/OpenSC/blob/master/tests/common.sh +# +SOPIN="12345678" +PIN="123456" +P11LIB="" +SOFTHSM_PATHS="/usr/local/lib/softhsm/libsofthsm2.so \ +/usr/lib/softhsm/libsofthsm2.so \ +/usr/lib64/pkcs11/libsofthsm2.so" + +for hsmlib in ${SOFTHSM_PATHS}; do + if [[ -f "${hsmlib}" ]]; then + P11LIB="${hsmlib}" + break + fi +done + +# Check required libraries +if [[ -z "${P11LIB}" ]]; then + echo "Warning: Could not find the softhsm pkcs11 module library" + echo "Warning: Searched files:${SOFTHSM_PATHS}" +fi + +# Check required commands +if ! type pkcs11-tool; then + echo "Warning: Could not find pkcs11-tool" +fi +if ! type openssl; then + echo "Warning: Could not find openssl command" +fi + +function generate_public_key() { + TYPE="$1" + ID="$2" + LABEL="$3" + + # Generate key pair + if ! pkcs11-tool --keypairgen --key-type="${TYPE}" --login --pin="${PIN}" \ + --module="${P11LIB}" --label="${LABEL}" --id="${ID}"; then + echo "Couldn't generate ${TYPE} key pair" + return 1 + fi + + # Extract public key + if ! pkcs11-tool --read-object --id "${ID}" --type pubkey --output-file \ + "${ID}".der --module="${P11LIB}"; + then + echo "Couldn't read generated ${TYPE} public key" + return 1 + fi + + if [[ ${TYPE:0:3} == "RSA" ]]; then + openssl rsa -inform DER -outform PEM -in "${ID}".der -pubin > "${ID}".pub + else + echo "Unsupported key type:${TYPE}" + return 1 + fi + rm "${ID}".der +} + +function softhsm_initialize() { + echo "directories.tokendir = $(realpath .tokens)" > .softhsm2.conf + if [ -d ".tokens" ]; then + rm -rf ".tokens" + fi + mkdir ".tokens" + SOFTHSM2_CONF=$(realpath ".softhsm2.conf") + export SOFTHSM2_CONF + # Init token + softhsm2-util --init-token --slot 0 --label "Clevis PKCS11 test" \ + --so-pin="${SOPIN}" --pin="${PIN}" +} + +function softhsm_lib_setup() { + softhsm_initialize + # Generate 2048b RSA Key pair (this will generate 00.pub file) + generate_public_key "RSA:2048" "00" "RSA2048" || return 1 +} + +function softhsm_cleanup() { + rm -rf .softhsm2.conf + rm -rf ".tokens" +} + +function softhsm_lib_cleanup() { + softhsm_cleanup + rm 00.pub +}