Skip to content

Commit dc8aefe

Browse files
sarroutbiclaude
andcommitted
tang: skip storing adv when server supports tang_pub
When the Tang server advertises the tang_pub capability via the Tang-Capabilities HTTP header in its /adv response, skip storing the advertisement (clevis.tang.adv) in the JWE protected header. At decrypt time, the server exchange key is obtained directly from the tang_pub field in the /rec recovery response instead. This significantly reduces the metadata written to the LUKS header while maintaining full backward compatibility: when the capability header is absent or the advertisement is provided offline, the adv is stored as before. Encrypt side: - Capture /adv response headers and check for Tang-Capabilities - Conditionally omit adv from JWE header when tang_pub is available Decrypt side: - Make adv extraction optional (non-fatal when absent) - Extract tang_pub from recovery response and validate its thumbprint - Fail fast if adv IS present but kid does not match (tamper detection) - Fall back to stored adv for servers without tang_pub support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Sergio Arroutbi <sarroutb@redhat.com>
1 parent 1c9e927 commit dc8aefe

6 files changed

Lines changed: 137 additions & 28 deletions

File tree

‎src/luks/dracut/clevis-pin-tang/module-setup.sh.in‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,6 @@ EOF
5252

5353
inst_multiple \
5454
clevis-decrypt-tang \
55-
curl
55+
curl \
56+
mktemp
5657
}

‎src/pins/tang/clevis-decrypt-tang‎

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,35 +50,29 @@ if ! kid="$(jose fmt -j- -Og kid -Su- <<< "$jhd")"; then
5050
exit 1
5151
fi
5252

53-
# Tang advertisement validation.
54-
if ! keys="$(jose fmt -j- -Og clevis -g tang -g adv -Oo- <<< "${jhd}")"; then
55-
echo "JWE missing required 'clevis.tang.adv' header parameter!" >&2
56-
exit 1
57-
fi
58-
59-
# Check if the thumbprint we have in `kid' is in the advertised keys.
6053
CLEVIS_DEFAULT_THP_ALG=S256 # SHA-256.
6154
CLEVIS_DEFAULT_THP_LEN=43 # Length of SHA-256 thumbprint.
6255
CLEVIS_ALTERNATIVE_THP_ALGS=S1 # SHA-1.
6356

64-
# Issue a warning if we are using a hash that has a shorter length than the
65-
# default one.
66-
if [ "${#kid}" -lt "${CLEVIS_DEFAULT_THP_LEN}" ]; then
67-
echo "WARNING: tang using a deprecated hash for the JWK thumbprints" >&2
68-
fi
57+
# The adv field is optional when the server provides tang_pub in recovery.
58+
# However, if adv IS present, the kid must match — a mismatch indicates
59+
# a corrupted or tampered header and must fail immediately.
60+
srv=
61+
if keys="$(jose fmt -j- -Og clevis -g tang -g adv -Oo- <<< "${jhd}" 2>/dev/null)"; then
62+
if [ "${#kid}" -lt "${CLEVIS_DEFAULT_THP_LEN}" ]; then
63+
echo "WARNING: tang using a deprecated hash for the JWK thumbprints" >&2
64+
fi
6965

70-
if ! srv="$(jose jwk thp -i- -f "${kid}" -a "${CLEVIS_DEFAULT_THP_ALG}" \
71-
<<< "${keys}")"; then
72-
# `kid' thumprint not in the advertised keys, but it's possible it was
73-
# generated using a different algorithm than the default one.
74-
# Let us try the alternative supported algorithms to make sure `kid'
75-
# really is not part of the advertised keys.
76-
for alg in ${CLEVIS_ALTERNATIVE_THP_ALGS}; do
77-
srv="$(jose jwk thp -i- -f "$kid" -a "${alg}" <<< "${keys}")" && break
78-
done
79-
if [ -z "${srv}" ]; then
80-
echo "JWE header validation of 'clevis.tang.adv' failed: key thumbprint does not match" >&2
81-
exit 1
66+
if ! srv="$(jose jwk thp -i- -f "${kid}" -a "${CLEVIS_DEFAULT_THP_ALG}" \
67+
<<< "${keys}")"; then
68+
for alg in ${CLEVIS_ALTERNATIVE_THP_ALGS}; do
69+
srv="$(jose jwk thp -i- -f "$kid" -a "${alg}" <<< "${keys}")" \
70+
&& break
71+
done
72+
if [ -z "${srv}" ]; then
73+
echo "JWE header validation of 'clevis.tang.adv' failed: key thumbprint does not match" >&2
74+
exit 1
75+
fi
8276
fi
8377
fi
8478

@@ -120,11 +114,32 @@ xfr="$(jose jwk exc -i '{"alg":"ECMR"}' -l- -r- <<< "$clt$eph")"
120114

121115
rec_url="$url/rec/$kid"
122116
ct="Content-Type: application/jwk+json"
123-
if ! rep="$(curl -sfg "${curl_opts[@]}" -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then
117+
if ! full_rep="$(curl -sfg "${curl_opts[@]}" -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then
124118
echo "Error communicating with server $url" >&2
125119
exit 1
126120
fi
127121

122+
# Use tang_pub from recovery response when available.
123+
if tang_pub="$(jose fmt -j- -Og tang_pub -Oo- <<< "$full_rep" 2>/dev/null)"; then
124+
if tang_pub_key="$(jose jwk thp -i- -f "${kid}" -a "${CLEVIS_DEFAULT_THP_ALG}" \
125+
<<< "$tang_pub" 2>/dev/null)"; then
126+
srv="$tang_pub_key"
127+
else
128+
for alg in ${CLEVIS_ALTERNATIVE_THP_ALGS}; do
129+
tang_pub_key="$(jose jwk thp -i- -f "$kid" -a "${alg}" \
130+
<<< "$tang_pub" 2>/dev/null)" \
131+
&& srv="$tang_pub_key" && break
132+
done
133+
fi
134+
fi
135+
136+
if [ -z "$srv" ]; then
137+
echo "Unable to determine server key!" >&2
138+
exit 1
139+
fi
140+
141+
rep="$(jose fmt -j- -Od tang_pub -o- <<< "$full_rep" 2>/dev/null)" || rep="$full_rep"
142+
128143
if ! rep="$(jose fmt -j- -Og kty -q EC -EUUg crv -q "$crv" -EUUo- <<< "$rep")"; then
129144
echo "Received invalid server reply!" >&2
130145
exit 1

‎src/pins/tang/clevis-encrypt-tang‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,25 +119,36 @@ curl_opts=()
119119
[ -n "$key" ] && curl_opts+=(--key "$key")
120120

121121
### Get the advertisement
122+
has_tang_pub=
123+
adv_hdr_file="$(mktemp)"
124+
trap 'rm -f "$adv_hdr_file"' EXIT INT TERM
122125
if jws="$(jose fmt -j- -g adv -Oo- <<< "$cfg")"; then
123126
thp="${thp:-any}"
124127
elif jws="$(jose fmt -j- -g adv -Su- <<< "$cfg")"; then
125128
if ! [ -f "$jws" ]; then
126129
echo "Advertisement file '$jws' not found!" >&2
130+
rm -f "$adv_hdr_file"
127131
exit 1
128132
fi
129133

130134
if ! jws="$(jose fmt --json="${jws}" -Oo- 2>/dev/null)"; then
131135
echo "Advertisement file '$jws' is malformed!" >&2
136+
rm -f "$adv_hdr_file"
132137
exit 1
133138
fi
134139

135140
thp="${thp:-any}"
136-
elif ! jws="$(curl -sfg "${curl_opts[@]}" "$url/adv/$thp")"; then
141+
elif ! jws="$(curl -sfg -D "$adv_hdr_file" "${curl_opts[@]}" "$url/adv/$thp")"; then
137142
echo "Unable to fetch advertisement: '$url/adv/$thp'!" >&2
143+
rm -f "$adv_hdr_file"
138144
exit 1
139145
fi
140146

147+
if grep -qi "^Tang-Capabilities:.*tang_pub" "$adv_hdr_file" 2>/dev/null; then
148+
has_tang_pub=yes
149+
fi
150+
rm -f "$adv_hdr_file"
151+
141152
if ! jwks="$(jose fmt --json="${jws}" -Og payload -SyOg keys \
142153
-AUo- 2>/dev/null)"; then
143154
echo "Advertisement is malformed!" >&2
@@ -202,5 +213,7 @@ jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$url" -s url -UUUUo
202213
[ -n "$cacert" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$cacert" -s cacert -UUUUo-)"
203214
[ -n "$cert" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$cert" -s cert -UUUUo-)"
204215
[ -n "$key" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$key" -s key -UUUUo-)"
205-
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -j- -s adv -UUUUo- <<< "$jwks")"
216+
if [ -z "$has_tang_pub" ]; then
217+
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -j- -s adv -UUUUo- <<< "$jwks")"
218+
fi
206219
exec jose jwe enc -i- -k- -I- -c < <(echo -n "$jwe$jwk"; /bin/cat)

‎src/pins/tang/clevis-encrypt-tang.1.adoc‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,23 @@ first place. You may also prefer installing the Tang server's CA into the
122122
system trust store (for example with *update-ca-trust* or
123123
*update-ca-certificates*) and omitting *cacert*.
124124

125+
== ADVERTISEMENT STORAGE
126+
127+
At bind time, when the advertisement is fetched from the Tang server, Clevis
128+
checks the HTTP response for a *Tang-Capabilities* header containing
129+
*tang_pub*. When present, the server exchange key can be obtained at decryption
130+
time directly from the recovery response, so Clevis skips storing the
131+
advertisement (*clevis.tang.adv*) in the JWE header. This reduces the metadata
132+
written to the LUKS header.
133+
134+
If the server does not advertise the *tang_pub* capability, or if the
135+
advertisement is provided offline (via the *adv* configuration property), the
136+
advertisement is stored in the JWE header as before, maintaining full backward
137+
compatibility.
138+
139+
Running *clevis luks regen* against an existing binding will re-fetch the
140+
advertisement and automatically update the metadata format.
141+
125142
== OPTIONS
126143

127144
* *-y* :

‎src/pins/tang/tests/meson.build‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@ env.prepend('PATH',
5151

5252
test('pin-tang', find_program('pin-tang'), env: env)
5353
test('pin-tang-mtls', find_program('pin-tang-mtls'), env: env)
54+
test('pin-tang-no-adv', find_program('pin-tang-no-adv'), env: env)
5455
test('tang-validate-adv', find_program('tang-validate-adv'), env: env)
5556
test('default-thp-alg', find_program('default-thp-alg'), env: env)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/bin/bash -xe
2+
# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
3+
#
4+
# Copyright (c) 2026 Red Hat, Inc.
5+
# Author: Sergio Arroutbi <sarroutb@redhat.com>
6+
#
7+
# This program is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU General Public License as published by
9+
# the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
#
20+
21+
. tang-common-test-functions
22+
23+
on_exit() {
24+
exit_status=$?
25+
tang_stop "${TMP}"
26+
[ -d "$TMP" ] && rm -rf "$TMP"
27+
exit "${exit_status}"
28+
}
29+
30+
trap 'on_exit' EXIT
31+
32+
TMP="$(mktemp -d)"
33+
34+
tang_run "${TMP}" sig exc
35+
port=$(tang_get_port "${TMP}")
36+
37+
thp="$(jose jwk thp -i "$TMP/db/sig.jwk")"
38+
adv="${TMP}/adv.jws"
39+
tang_get_adv "${port}" "${adv}"
40+
url="http://localhost:${port}"
41+
42+
# Encrypt and decrypt must work regardless of tang_pub support.
43+
cfg="$(printf '{"url":"%s","thp":"%s"}' "$url" "$thp")"
44+
enc="$(echo -n "hi" | clevis encrypt tang "$cfg")"
45+
dec="$(echo -n "$enc" | clevis decrypt)"
46+
test "$dec" == "hi"
47+
48+
# Verify consistent behavior: if adv is absent from the JWE header,
49+
# the server must actually return tang_pub in recovery responses.
50+
hdr="$(echo -n "$enc" | cut -d. -f1)"
51+
jhd="$(jose b64 dec -i- <<< "$hdr")"
52+
if ! jose fmt -j- -Og clevis -g tang -g adv -Oo- <<< "$jhd" >/dev/null 2>&1; then
53+
exc_pub="$(jose jwk pub -i "$TMP/db/exc.jwk")"
54+
exc_kid="$(jose jwk thp -i- <<< "$exc_pub")"
55+
probe_rep="$(curl -sf -X POST -H "Content-Type: application/jwk+json" \
56+
--data-binary @- "http://localhost:${port}/rec/${exc_kid}" <<< "$exc_pub")"
57+
jose fmt -j- -Og tang_pub -Oo- <<< "$probe_rep" > /dev/null
58+
fi
59+
60+
# Server down must cause decrypt to fail.
61+
tang_stop "${TMP}"
62+
! echo "$enc" | clevis decrypt

0 commit comments

Comments
 (0)