[RFC] example request-key helper script

Chris Leech
2026-04-23T03:08:51Z

This is a prototype of a request-key helper to instantiate a "PSK request" key.

Assisted-by: Claude:claude-sonnet-4.5
---
#!/usr/bin/bash
#
# NVMe PSK Provider
#
# This script is called by /sbin/request-key when the kernel requests
# NVMe TLS PSKs via the nvme-psk-request key type.
#
# configure /etc/request-key.conf with an entry like the following:
# create nvme-psk-request * * /usr/libexec/nvme-psk-provider %k %d %c %S

KEY_ID=$1
DESCRIPTION=$2
CALLOUT_INFO=$3
KEYRING=$4

exec > >(logger -t "key.nvme-psk") 2>&1
echo "$0: $@"

# Parse callout info
eval $(echo "$CALLOUT_INFO" | tr ' ' '\n' | grep '=')

KEYFILE="/etc/nvme/tls-keys"
HOST_NQN=${hostnqn}
SUBSYS_NQN=${subnqn}

if [ -z "$KEYFILE" ] || [ -z "$HOST_NQN" ] || [ -z "$SUBSYS_NQN" ]; then
echo "Error: Missing required parameters"
keyctl negate "$KEY_ID" 1 "$KEYRING"
exit 1
fi

if [ ! -f "$KEYFILE" ]; then
echo "Error: Keyfile '$KEYFILE' not found"
keyctl negate "$KEY_ID" 1 "$KEYRING"
exit 1
fi

# Temporary file for binary payload
PAYLOAD_FILE=$(mktemp)
trap "rm -f $PAYLOAD_FILE" EXIT

KEY_COUNT=0

# Search for all matching entries in keyfile
while IFS= read -r line; do
# Skip empty lines and comments
[ -z "$line" ] && continue
[[ "$line" =~ ^#.* ]] && continue

# Extract the last space-separated field (the encoded key)
encoded_key="${line##* }"
# Extract everything before the last space (the description)
description="${line% *}"

# Parse description: NVMe<ver><type>0<hmac> <hostnqn> <subsysnqn> <digest>
if [[ "$description" =~ ^NVMe([0-9])([RG])([0-9][0-9])[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)$ ]]; then
ver="${BASH_REMATCH[1]}"
type="${BASH_REMATCH[2]}"
hmac="${BASH_REMATCH[3]}"
desc_hostnqn="${BASH_REMATCH[4]}"
desc_subsysnqn="${BASH_REMATCH[5]}"
desc_digest="${BASH_REMATCH[6]}"

# Only process Retained keys (not Generated)
if [ "$type" != "R" ]; then
continue
fi

# Check if this matches our search criteria
if [ "$desc_hostnqn" = "$HOST_NQN" ] && [ "$desc_subsysnqn" = "$SUBSYS_NQN" ]; then
# Validate the encoded key format: NVMeTLSkey-1:xx:...:
if [[ "$encoded_key" =~ ^NVMeTLSkey-1:([0-9][0-9]):(.+):$ ]]; then
key_hmac="${BASH_REMATCH[1]}"
base64_data="${BASH_REMATCH[2]}"

echo "Found matching PSK: version=$ver type=$type hmac=$hmac"

# Decode base64
tmpfile=$(mktemp)
echo -n "$base64_data" | base64 -d > "$tmpfile"
decoded_len=$(stat -c%s "$tmpfile")

if [ "$decoded_len" -lt 36 ]; then
echo "Warning: Decoded data too short ($decoded_len bytes), skipping"
rm -f "$tmpfile"
continue
fi

# Key length is decoded_len - 4 (CRC is last 4 bytes)
key_len=$((decoded_len - 4))

# Verify CRC32 (optional - nvme-cli already validated on import)
# Extract key data (without CRC)
key_data=$(dd if="$tmpfile" bs=1 count="$key_len" 2>/dev/null)

# Decode digest from base64
digest_data=$(echo -n "$desc_digest" | base64 -d)
digest_len=$(echo -n "$digest_data" | wc -c)

# Pack binary: hmac(1) + key_len(1) + digest_len(1) + key_data[key_len] + digest[digest_len]
# Convert hmac from decimal string to hex byte
printf "\\x$(printf '%02x' $key_hmac)" >> "$PAYLOAD_FILE"
printf "\\x$(printf '%02x' $key_len)" >> "$PAYLOAD_FILE"
printf "\\x$(printf '%02x' $digest_len)" >> "$PAYLOAD_FILE"
echo -n "$key_data" >> "$PAYLOAD_FILE"
echo -n "$digest_data" >> "$PAYLOAD_FILE"

KEY_COUNT=$((KEY_COUNT + 1))
rm -f "$tmpfile"
else
echo "Warning: Invalid encoded key format, skipping"
fi
fi
fi
done < "$KEYFILE"

if [ "$KEY_COUNT" -eq 0 ]; then
echo "Error: No matching keys found for host-nqn '$HOST_NQN' and subsystem-nqn '$SUBSYS_NQN'"
keyctl negate "$KEY_ID" 1 "$KEYRING"
exit 1
fi

# Instantiate the nvme-psk-request key with binary payload
echo keyctl instantiate "$KEY_ID" @s "$KEYRING" < "$PAYLOAD_FILE"
keyctl pinstantiate "$KEY_ID" "$KEYRING" < "$PAYLOAD_FILE"
if [ $? -eq 0 ]; then
echo "Successfully instantiated $KEY_COUNT PSK(s)"
exit 0
else
echo "Error: Failed to instantiate request key"
keyctl negate "$KEY_ID" 1 "$KEYRING"
exit 1
fi