Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fido2 pin #399

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/pins/fido2/clevis-decrypt-fido2
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash

# Copyright (c) 2023 Sebastian Kussl
# Author: Sebastian Kussl
#
# 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 <https://www.gnu.org/licenses/>.

set -eu

read -r -d . hdr64
if ! hdr="$(jose fmt --quote="$hdr64" --string --b64load --object --output=-)" ; then
echo 'JWE header corrupt' >&2
exit 1
fi
if [ "$(jose fmt --json="$hdr" --get clevis --get pin --unquote=-)" != 'fido2' ] ; then
echo 'JWE pin mismatch!' >&2
exit 1
fi
if ! hmac_salt="$(jose fmt --json="$hdr" --get clevis --get fido2 --get hmac_salt --unquote=-)" ; then
echo "JWE missing 'hmac_salt' header parameter!" >&2
exit 1
fi

if ! rp_id="$(jose fmt --json="$hdr" --get clevis --get fido2 --get rp_id --unquote=-)" ; then
echo "JWE missing 'rp_id' header parameter!" >&2
exit 1
fi
if ! cred_id="$(jose fmt --json="$hdr" --get clevis --get fido2 --get cred_id --unquote=-)" ; then
echo "JWE missing 'cred_id' header parameter!" >&2
exit 1
fi
if ! uv="$(jose fmt --json="$hdr" --get clevis --get fido2 --get uv --unquote=-)" ; then
echo "JWE missing 'uv' header parameter!" >&2
exit 1
fi
if ! up="$(jose fmt --json="$hdr" --get clevis --get fido2 --get up --unquote=-)" ; then
echo "JWE missing 'up' header parameter!" >&2
exit 1
fi
if ! pin="$(jose fmt --json="$hdr" --get clevis --get fido2 --get pin --unquote=-)" ; then
echo "JWE missing 'pin' header parameter!" >&2
exit 1
fi

fido2_tokens="$(fido2-token -L)"

if [ -z "${fido2_tokens}" ]; then
echo "Please insert your FIDO2 token." >&2
exit 1
fi

num_tokens="$(echo "${fido2_tokens}" | wc -l)"
if ((num_tokens > 1)); then
echo "Warning: There are multiple tokens. Will use the first one." >&2
fi

fido2_token="$(echo "${fido2_tokens}" | head -n1 | cut -d':' -f1)"

client_hash="$(dd if=/dev/urandom bs=1 count=32 status=none | base64 -w0)"

hmac="$(printf '%s\n%s\n%s\n%s\n' "${client_hash}" "${rp_id}" "${cred_id}" "${hmac_salt}" | \
fido2-assert -G -t "uv=${uv}" -t "up=${up}" -t "pin=${pin}" -h "${fido2_token}" | \
head -n5 | tail -n1 | jose b64 enc -I -)"

# use the secret in a key wrapping key
jwk='{"alg":"PBES2-HS512+A256KW", "kty":"oct"}'
jwk="$(jose fmt -j "${jwk}" -q "${hmac}" -s k -Uo-)"

( printf '%s' "$jwk$hdr64." ; cat ) | exec jose jwe dec --key=- --input=-
109 changes: 109 additions & 0 deletions src/pins/fido2/clevis-encrypt-fido2
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/bin/bash

# Copyright (c) 2023 Sebastian Kussl
# Author: Sebastian Kussl
#
# 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 <https://www.gnu.org/licenses/>.

function create_credential () {
local device
local rp_id
local type

device="$1"
rp_id="$2"
type="$3"
client_data="$(dd if=/dev/urandom bs=1 count=32 status=none | base64 -w0)"
user_id="$(echo -n 'clevis' | base64 -w0)"
cred_id="$(printf '%s\n%s\n%s\n%s\n' "${client_data}" "${rp_id}" 'clevis' "${user_id}" \
| fido2-cred -M -h "${device}" "${type}" \
| head -n5 | tail -n1)" >&2

echo -n "${cred_id}"
}

function generate_hmac () {
local device
local rp_id
local cred_id
local hmac_salt

device="$1"
rp_id="$2"
cred_id="$3"
hmac_salt="$4"

client_hash="$(dd if=/dev/urandom bs=1 count=32 status=none | base64 -w0)"
hmac="$(printf '%s\n%s\n%s\n%s\n' "${client_hash}" "${rp_id}" "${cred_id}" "${hmac_salt}" | \
fido2-assert -G -h -t "uv=${uv}" -t "up=${up}" -t "pin=${pin}" "${device}" | \
head -n5 | tail -n1 | jose b64 enc -I -)" >&2

echo -n "${hmac}"
}

cfg=''

if ! cfg="$(jose fmt -j- -Oo- <<< "$1" 2>/dev/null)"; then
echo "Error: Configuration is malformed!" >&2
exit 1
fi

type="$(jose fmt -j- -Og type -Bo- <<< "$cfg")" || type='es256'
uv="$(jose fmt -j- -Og uv -Bo- <<< "$cfg")" || uv='true'
up="$(jose fmt -j- -Og up -Bo- <<< "$cfg")" || up='true'
pin="$(jose fmt -j- -Og pin -Bo- <<< "$cfg")" || pin='false'
rp_id="$(jose fmt -j- -Og rp_id -Su- <<< "$cfg")" || rp_id='clevis'

if ! fido2_token="$(jose fmt -j- -Og device -u- <<< "$cfg")"; then
fido2_tokens="$(fido2-token -L)"

if [ -z "${fido2_tokens}" ]; then
echo "Please insert your FIDO2 token." >&2
exit 1
fi

fido2_token="$(echo "${fido2_tokens}" | head -n1 | cut -d':' -f1)"
num_tokens="$(echo "${fido2_tokens}" | wc -l)"
if ((num_tokens > 1)); then
echo "Warning: There are multiple tokens. Will use the first one (${fido2_token})." >&2
fi
fi

cred_id="$(jose fmt -j- -Og cred_id -Su- <<< "$cfg")" || cred_id="$(create_credential "${fido2_token}" "${rp_id}" "${type}")"

# generate a random salt for each encrypted payload
hmac_salt="$(dd if=/dev/urandom bs=1 count=32 status=none | base64 -w0)"

# retrieve the hmac result which will be the password to use for key wrapping a CEK.
hmac="$(generate_hmac "${fido2_token}" "${rp_id}" "${cred_id}" "${hmac_salt}")"

if [ -z "${hmac}" ]; then
echo "Error: could not generate key."
exit 1
fi

# use the secret in a key wrapping key
jwk='{"kty":"oct", "alg":"PBES2-HS512+A256KW"}'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anyone is reviewing this, I'd like to know your opinion on my choice of this "alg" here (if any).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't have a strong opinion regarding algorithm selection.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I changed this to AES256GCM. The thinking behind the exotic first choice of algorithm here was that it would be better to additionally do some key derivation from the hmac secret retrieved from the fido2 token, and also because I experienced some wrapping errors when using the raw value returned. Coming back to this, I changed my mind. I don't think an additional key derivation might be needed here as the hmac over the random salt should be sufficient (but I am happy to be proven wrong about it). The wrapping errors I experienced with the raw value were likely due to libfido2 returning values encoded in base64, and I probably didn't get the conversion to base64url right, which should now be fixed. So now the hmac output is used as a simple AES256GCM type key.

jwk="$(jose fmt -j "${jwk}" -q "${hmac}" -s k -Uo-)"

jwe='{"protected":{"enc":"A256GCM","clevis":{"pin":"fido2","fido2":{}}}}'
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g fido2 -q "${type}" -s type -UUUUo-)"
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g fido2 -q "${hmac_salt}" -s hmac_salt -UUUUo-)"
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g fido2 -q "${rp_id}" -s rp_id -UUUUo-)"
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g fido2 -q "${cred_id}" -s cred_id -UUUUo-)"
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g fido2 -q "${uv}" -s uv -UUUUo-)"
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g fido2 -q "${up}" -s up -UUUUo-)"
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g fido2 -q "${pin}" -s pin -UUUUo-)"

exec jose jwe enc -i- -k- -I- -c < <(echo -n "$jwe$jwk"; /bin/cat)
85 changes: 85 additions & 0 deletions src/pins/fido2/clevis-encrypt-fido2.1.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
CLEVIS-ENCRYPT-FIDO2(1)
======================
:doctype: manpage


== NAME

clevis-encrypt-fido2 - Encrypts using a FIDO2 token by using the hmac-secret extension for generating a symmetric key.

== SYNOPSIS

*clevis encrypt fido2* CONFIG [-y] < PT > JWE

== OVERVIEW

The *clevis encrypt fido2* command encrypts using a FIDO2 token.
Its only argument is the JSON configuration object.

FIDO2 is a standard for web authentication using secure tokens, such as a security key.
For symmetrically encrypting data using a FIDO2 token, the token must support the hmac-secret
extension. The encryption then works by generating a random 32 byte public hmac-salt that is
sent to the token/authenticator, where an hmac over the salt is created using a key only known
to the authenticator. This secret value is then used to as a "keyWrap" JWK.

Clevis provides support for encrypting data using such symmetric keys derived from a FIDO2
hardware token. The following shows a basic example, using the default configuration options:

$ clevis encrypt fido2 '{}' < PT > JWE
Enter PIN for /dev/hidraw0:

By default, a new (non-discoverable) credential will be generated and its credential id, as well
as the randomly generated hmac-salt, is stored as metadata along with the ciphertext. Creating
the credential might require entering the device PIN (as shown above) and verifying user presence
by touching the token. If the "pin" option is set to true, the PIN must be entered again and at
every decryption. For example:

$ clevis encrypt fido2 '{"pin": true}' < PT > JWE
Enter PIN for /dev/hidraw0:
Enter PIN for /dev/hidraw0:

The options "up" and "uv" can be used to set the desired behaviour for user presence and user
verification when decrypting the ciphertext (see below). In a "headless" setup, e.g., when
encrypting a LUKS partition, those could be set to "false" in order to automatically decrypt
without any user actions. Note that there are currently no prompts when you need to tap on
the device, but the token might signal that by blinking.

== CONFIG

This command uses the following configuration properties:

* *type* (string) :
The type of the credential, as supported by libfido2, i.e., "es256", "rs256" or "eddsa".
Default: "es256".

* *cred_id* (string) :
A credential id generated for the specific token. If not specified, a new
(non-discoverable) will be generated using the **fido2-cred** command. Please
note that the credential must have the "hmac-extension" enabled.

* *rp_id* (string) :
The reyling party id of the credential (that will be created or is provided via
the "cred_id" field).
Default: 'clevis'.

* *up* (boolean) :
Whether or not to ask the authenticator to require user presence.
Default: true.

* *uv* (string) :
Whether or not to ask the authenticator to require user verification.
Default: true.

* *pin* (string) :
Whether or not to ask the authenticator to require the PIN and user verification.
Default: false.

* *device* (string) :
The device, i.e., the fido2 token, to use (e.g., "/dev/hidraw0"). If not specified,
the first device from the list of connected tokens will be used. When setting this
option, you should be sure that the token's slot remains the same, as the decrypt
command will not be able to find the device, otherwise.

== SEE ALSO

link:clevis-decrypt.1.adoc[*clevis-decrypt*(1)]