From 1c3017122e07ee5d27ee949ec9932ceb38254bc5 Mon Sep 17 00:00:00 2001 From: Sergio Arroutbi Date: Tue, 22 Sep 2026 14:16:00 +0200 Subject: [PATCH] tang: skip storing adv when server supports tang_pub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At provisioning time (clevis encrypt tang), call GET /version on the Tang server. When features.tang_pub is true, create a new-format binding that stores only the Tang URL and key identifier — the full advertisement is not persisted in the JWE header. When /version returns 404 or tang_pub is absent, fall back to current behavior. At recovery time (clevis decrypt tang), check the binding format: new-format bindings extract tang_pub from the POST /rec/$kid response for ECMR recovery; legacy bindings use the stored advertisement unchanged. If a new-format recovery fails because tang_pub is missing from the server response, fail clearly without silent fallback. clevis luks regen automatically migrates eligible legacy bindings to the new format by re-querying /version during re-encryption. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Sergio Arroutbi --- src/pins/tang/clevis-decrypt-tang | 65 +++++++++++++++--------- src/pins/tang/clevis-encrypt-tang | 11 +++- src/pins/tang/clevis-encrypt-tang.1.adoc | 18 +++++++ src/pins/tang/tests/meson.build | 1 + src/pins/tang/tests/pin-tang-no-adv | 60 ++++++++++++++++++++++ 5 files changed, 129 insertions(+), 26 deletions(-) create mode 100755 src/pins/tang/tests/pin-tang-no-adv diff --git a/src/pins/tang/clevis-decrypt-tang b/src/pins/tang/clevis-decrypt-tang index 67a30c80..75017ab9 100755 --- a/src/pins/tang/clevis-decrypt-tang +++ b/src/pins/tang/clevis-decrypt-tang @@ -50,35 +50,29 @@ if ! kid="$(jose fmt -j- -Og kid -Su- <<< "$jhd")"; then exit 1 fi -# Tang advertisement validation. -if ! keys="$(jose fmt -j- -Og clevis -g tang -g adv -Oo- <<< "${jhd}")"; then - echo "JWE missing required 'clevis.tang.adv' header parameter!" >&2 - exit 1 -fi - -# Check if the thumbprint we have in `kid' is in the advertised keys. CLEVIS_DEFAULT_THP_ALG=S256 # SHA-256. CLEVIS_DEFAULT_THP_LEN=43 # Length of SHA-256 thumbprint. CLEVIS_ALTERNATIVE_THP_ALGS=S1 # SHA-1. -# Issue a warning if we are using a hash that has a shorter length than the -# default one. -if [ "${#kid}" -lt "${CLEVIS_DEFAULT_THP_LEN}" ]; then - echo "WARNING: tang using a deprecated hash for the JWK thumbprints" >&2 -fi +# The adv field is optional when the server provides tang_pub in recovery. +# However, if adv IS present, the kid must match — a mismatch indicates +# a corrupted or tampered header and must fail immediately. +srv= +if keys="$(jose fmt -j- -Og clevis -g tang -g adv -Oo- <<< "${jhd}" 2>/dev/null)"; then + if [ "${#kid}" -lt "${CLEVIS_DEFAULT_THP_LEN}" ]; then + echo "WARNING: tang using a deprecated hash for the JWK thumbprints" >&2 + fi -if ! srv="$(jose jwk thp -i- -f "${kid}" -a "${CLEVIS_DEFAULT_THP_ALG}" \ - <<< "${keys}")"; then - # `kid' thumprint not in the advertised keys, but it's possible it was - # generated using a different algorithm than the default one. - # Let us try the alternative supported algorithms to make sure `kid' - # really is not part of the advertised keys. - for alg in ${CLEVIS_ALTERNATIVE_THP_ALGS}; do - srv="$(jose jwk thp -i- -f "$kid" -a "${alg}" <<< "${keys}")" && break - done - if [ -z "${srv}" ]; then - echo "JWE header validation of 'clevis.tang.adv' failed: key thumbprint does not match" >&2 - exit 1 + if ! srv="$(jose jwk thp -i- -f "${kid}" -a "${CLEVIS_DEFAULT_THP_ALG}" \ + <<< "${keys}")"; then + for alg in ${CLEVIS_ALTERNATIVE_THP_ALGS}; do + srv="$(jose jwk thp -i- -f "$kid" -a "${alg}" <<< "${keys}")" \ + && break + done + if [ -z "${srv}" ]; then + echo "JWE header validation of 'clevis.tang.adv' failed: key thumbprint does not match" >&2 + exit 1 + fi fi fi @@ -120,11 +114,32 @@ xfr="$(jose jwk exc -i '{"alg":"ECMR"}' -l- -r- <<< "$clt$eph")" rec_url="$url/rec/$kid" ct="Content-Type: application/jwk+json" -if ! rep="$(curl -sfg "${curl_opts[@]}" -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then +if ! full_rep="$(curl -sfg "${curl_opts[@]}" -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then echo "Error communicating with server $url" >&2 exit 1 fi +# Use tang_pub from recovery response when available. +if tang_pub="$(jose fmt -j- -Og tang_pub -Oo- <<< "$full_rep" 2>/dev/null)"; then + if tang_pub_key="$(jose jwk thp -i- -f "${kid}" -a "${CLEVIS_DEFAULT_THP_ALG}" \ + <<< "$tang_pub" 2>/dev/null)"; then + srv="$tang_pub_key" + else + for alg in ${CLEVIS_ALTERNATIVE_THP_ALGS}; do + tang_pub_key="$(jose jwk thp -i- -f "$kid" -a "${alg}" \ + <<< "$tang_pub" 2>/dev/null)" \ + && srv="$tang_pub_key" && break + done + fi +fi + +if [ -z "$srv" ]; then + echo "Unable to determine server exchange key from tang_pub in recovery response" >&2 + exit 1 +fi + +rep="$(jose fmt -j- -Od tang_pub -o- <<< "$full_rep" 2>/dev/null)" || rep="$full_rep" + if ! rep="$(jose fmt -j- -Og kty -q EC -EUUg crv -q "$crv" -EUUo- <<< "$rep")"; then echo "Received invalid server reply!" >&2 exit 1 diff --git a/src/pins/tang/clevis-encrypt-tang b/src/pins/tang/clevis-encrypt-tang index 476b07d4..03aabd18 100755 --- a/src/pins/tang/clevis-encrypt-tang +++ b/src/pins/tang/clevis-encrypt-tang @@ -119,6 +119,7 @@ curl_opts=() [ -n "$key" ] && curl_opts+=(--key "$key") ### Get the advertisement +has_tang_pub= if jws="$(jose fmt -j- -g adv -Oo- <<< "$cfg")"; then thp="${thp:-any}" elif jws="$(jose fmt -j- -g adv -Su- <<< "$cfg")"; then @@ -136,6 +137,12 @@ elif jws="$(jose fmt -j- -g adv -Su- <<< "$cfg")"; then elif ! jws="$(curl -sfg "${curl_opts[@]}" "$url/adv/$thp")"; then echo "Unable to fetch advertisement: '$url/adv/$thp'!" >&2 exit 1 +else + if ver_resp="$(curl -sfg "${curl_opts[@]}" "$url/version" 2>/dev/null)" \ + && [ "$(jose fmt -j- -Og features -Og tang_pub -o- \ + <<< "$ver_resp" 2>/dev/null)" = "true" ]; then + has_tang_pub=yes + fi fi if ! jwks="$(jose fmt --json="${jws}" -Og payload -SyOg keys \ @@ -202,5 +209,7 @@ jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$url" -s url -UUUUo [ -n "$cacert" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$cacert" -s cacert -UUUUo-)" [ -n "$cert" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$cert" -s cert -UUUUo-)" [ -n "$key" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$key" -s key -UUUUo-)" -jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -j- -s adv -UUUUo- <<< "$jwks")" +if [ -z "$has_tang_pub" ]; then + jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -j- -s adv -UUUUo- <<< "$jwks")" +fi exec jose jwe enc -i- -k- -I- -c < <(echo -n "$jwe$jwk"; /bin/cat) diff --git a/src/pins/tang/clevis-encrypt-tang.1.adoc b/src/pins/tang/clevis-encrypt-tang.1.adoc index 30c2366c..fc7836a0 100644 --- a/src/pins/tang/clevis-encrypt-tang.1.adoc +++ b/src/pins/tang/clevis-encrypt-tang.1.adoc @@ -122,6 +122,24 @@ first place. You may also prefer installing the Tang server's CA into the system trust store (for example with *update-ca-trust* or *update-ca-certificates*) and omitting *cacert*. +== ADVERTISEMENT STORAGE + +At bind time, when the advertisement is fetched from the Tang server, Clevis +calls *GET /version* on the server to check for *features.tang_pub*. When +the server reports this capability, the server exchange key can be obtained at +decryption time directly from the recovery response, so Clevis skips storing +the advertisement (*clevis.tang.adv*) in the JWE header. This reduces the +metadata written to the LUKS header. + +If the server does not expose */version*, or *features.tang_pub* is not +present, or the advertisement is provided offline (via the *adv* configuration +property), the advertisement is stored in the JWE header as before, maintaining +full backward compatibility. + +Running *clevis luks regen* against an existing binding will re-query the +server and automatically migrate to the new format when the server supports +*tang_pub*. + == OPTIONS * *-y* : diff --git a/src/pins/tang/tests/meson.build b/src/pins/tang/tests/meson.build index dae38121..7c8b447a 100644 --- a/src/pins/tang/tests/meson.build +++ b/src/pins/tang/tests/meson.build @@ -51,5 +51,6 @@ env.prepend('PATH', test('pin-tang', find_program('pin-tang'), env: env) test('pin-tang-mtls', find_program('pin-tang-mtls'), env: env) +test('pin-tang-no-adv', find_program('pin-tang-no-adv'), env: env) test('tang-validate-adv', find_program('tang-validate-adv'), env: env) test('default-thp-alg', find_program('default-thp-alg'), env: env) diff --git a/src/pins/tang/tests/pin-tang-no-adv b/src/pins/tang/tests/pin-tang-no-adv new file mode 100755 index 00000000..0b19f0a5 --- /dev/null +++ b/src/pins/tang/tests/pin-tang-no-adv @@ -0,0 +1,60 @@ +#!/bin/bash -xe +# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: +# +# Copyright (c) 2026 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 . +# + +. tang-common-test-functions + +on_exit() { + exit_status=$? + tang_stop "${TMP}" + [ -d "$TMP" ] && rm -rf "$TMP" + exit "${exit_status}" +} + +trap 'on_exit' EXIT + +TMP="$(mktemp -d)" + +tang_run "${TMP}" sig exc +port=$(tang_get_port "${TMP}") + +thp="$(jose jwk thp -i "$TMP/db/sig.jwk")" +adv="${TMP}/adv.jws" +tang_get_adv "${port}" "${adv}" +url="http://localhost:${port}" + +# Encrypt and decrypt must work regardless of tang_pub support. +cfg="$(printf '{"url":"%s","thp":"%s"}' "$url" "$thp")" +enc="$(echo -n "hi" | clevis encrypt tang "$cfg")" +dec="$(echo -n "$enc" | clevis decrypt)" +test "$dec" == "hi" + +# Verify consistent behavior: if adv is absent from the JWE header, +# the server must actually return tang_pub in recovery responses. +hdr="$(echo -n "$enc" | cut -d. -f1)" +jhd="$(jose b64 dec -i- <<< "$hdr")" +if ! jose fmt -j- -Og clevis -g tang -g adv -Oo- <<< "$jhd" >/dev/null 2>&1; then + ver_resp="$(curl -sf "http://localhost:${port}/version")" + [ "$(jose fmt -j- -Og features -Og tang_pub -o- <<< "$ver_resp" \ + 2>/dev/null)" = "true" ] +fi + +# Server down must cause decrypt to fail. +tang_stop "${TMP}" +! echo "$enc" | clevis decrypt