Skip to content

Commit 3bc2cb7

Browse files
author
Yue Li
committed
tang: add optional mTLS support to the tang pin
Allow the tang pin to authenticate to the Tang server with mutual TLS. Three new optional config properties are accepted by clevis-encrypt-tang: cacert - CA bundle to verify the server certificate (curl --cacert) cert - client certificate for mTLS (curl --cert) key - private key for the client certificate (curl --key) When none are set, behaviour is unchanged. The paths are persisted into the JWE protected header so clevis-decrypt-tang reuses them for the recovery (POST /rec) request without extra configuration. Add a pin-tang-mtls integration test (TLS-fronted tangd via socat with mandatory client-cert verification) plus tang_generate_certs and tang_run_mtls helpers. The test skips cleanly when openssl or a socat built with OpenSSL support is unavailable.
1 parent 6df9b69 commit 3bc2cb7

6 files changed

Lines changed: 270 additions & 2 deletions

File tree

src/pins/tang/clevis-decrypt-tang

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,25 @@ if ! url="$(jose fmt -j- -Og clevis -g tang -g url -Su- <<< "$jhd")"; then
8787
exit 1
8888
fi
8989

90+
# Optional (m)TLS parameters. Absent for JWEs created without mTLS, in which
91+
# case curl behaves exactly as before.
92+
cacert="$(jose fmt -j- -Og clevis -g tang -g cacert -Su- <<< "$jhd")" || true
93+
cert="$(jose fmt -j- -Og clevis -g tang -g cert -Su- <<< "$jhd")" || true
94+
key="$(jose fmt -j- -Og clevis -g tang -g key -Su- <<< "$jhd")" || true
95+
96+
# Fail with a targeted diagnostic if a referenced (m)TLS file is missing.
97+
# Otherwise curl -sfg fails silently below and the generic "Error
98+
# communicating with server" sends the operator chasing network issues,
99+
# when the real cause is a rotated cert or a file left out of the initramfs.
100+
[ -n "$cacert" ] && [ ! -f "$cacert" ] && { echo "mTLS CA cert '$cacert' not found (is it in the initramfs?)" >&2; exit 1; }
101+
[ -n "$cert" ] && [ ! -f "$cert" ] && { echo "mTLS client cert '$cert' not found (is it in the initramfs?)" >&2; exit 1; }
102+
[ -n "$key" ] && [ ! -f "$key" ] && { echo "mTLS client key '$key' not found (is it in the initramfs?)" >&2; exit 1; }
103+
104+
curl_opts=()
105+
[ -n "$cacert" ] && curl_opts+=(--cacert "$cacert")
106+
[ -n "$cert" ] && curl_opts+=(--cert "$cert")
107+
[ -n "$key" ] && curl_opts+=(--key "$key")
108+
90109
if ! crv="$(jose fmt -j- -Og crv -Su- <<< "$clt")"; then
91110
echo "Unable to determine EPK's curve!" >&2
92111
exit 1
@@ -101,7 +120,7 @@ xfr="$(jose jwk exc -i '{"alg":"ECMR"}' -l- -r- <<< "$clt$eph")"
101120

102121
rec_url="$url/rec/$kid"
103122
ct="Content-Type: application/jwk+json"
104-
if ! rep="$(curl -sfg -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then
123+
if ! rep="$(curl -sfg "${curl_opts[@]}" -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then
105124
echo "Error communicating with server $url" >&2
106125
exit 1
107126
fi

src/pins/tang/clevis-encrypt-tang

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ if [ -t 0 ]; then
4545
echo " adv: <string> A filename containing a trusted advertisement"
4646
echo " adv: <object> A trusted advertisement (raw JSON)"
4747
echo
48+
echo " cacert: <string> Path to a CA bundle used to verify the Tang"
49+
echo " server's TLS certificate (curl --cacert)"
50+
echo
51+
echo " cert: <string> Path to the client TLS certificate for mutual"
52+
echo " TLS authentication (curl --cert)"
53+
echo
54+
echo " key: <string> Path to the private key for the client TLS"
55+
echo " certificate (curl --key). May be omitted if the"
56+
echo " key is bundled in the 'cert' file"
57+
echo
4858
echo "Obtaining the thumbprint of a trusted signing key is easy. If you"
4959
echo "have access to the Tang server's database directory, simply do:"
5060
echo
@@ -77,6 +87,28 @@ fi
7787

7888
thp="$(jose fmt -j- -Og thp -Su- <<< "$cfg")" || true
7989

90+
# Optional (m)TLS parameters. If none are set, curl behaves exactly as before
91+
# (plain HTTP/TLS, no client certificate and default CA verification).
92+
cacert="$(jose fmt -j- -Og cacert -Su- <<< "$cfg")" || true
93+
cert="$(jose fmt -j- -Og cert -Su- <<< "$cfg")" || true
94+
key="$(jose fmt -j- -Og key -Su- <<< "$cfg")" || true
95+
96+
# Validate the (m)TLS file paths up front, matching the 'adv' check below.
97+
# Otherwise a typo'd path either surfaces as a misleading "Unable to fetch
98+
# advertisement" error, or (when 'adv' is inline and curl is never called)
99+
# gets baked into the JWE header and only fails later at decrypt time.
100+
[ -n "$cacert" ] && [ ! -f "$cacert" ] && { echo "CA cert file '$cacert' not found!" >&2; exit 1; }
101+
[ -n "$cert" ] && [ ! -f "$cert" ] && { echo "Client cert file '$cert' not found!" >&2; exit 1; }
102+
[ -n "$key" ] && [ ! -f "$key" ] && { echo "Client key file '$key' not found!" >&2; exit 1; }
103+
104+
# 'key' on its own is a no-op: curl ignores --key unless --cert is also given.
105+
[ -n "$key" ] && [ -z "$cert" ] && { echo "'key' requires 'cert' to also be set!" >&2; exit 1; }
106+
107+
curl_opts=()
108+
[ -n "$cacert" ] && curl_opts+=(--cacert "$cacert")
109+
[ -n "$cert" ] && curl_opts+=(--cert "$cert")
110+
[ -n "$key" ] && curl_opts+=(--key "$key")
111+
80112
### Get the advertisement
81113
if jws="$(jose fmt -j- -g adv -Oo- <<< "$cfg")"; then
82114
thp="${thp:-any}"
@@ -92,7 +124,7 @@ elif jws="$(jose fmt -j- -g adv -Su- <<< "$cfg")"; then
92124
fi
93125

94126
thp="${thp:-any}"
95-
elif ! jws="$(curl -sfg "$url/adv/$thp")"; then
127+
elif ! jws="$(curl -sfg "${curl_opts[@]}" "$url/adv/$thp")"; then
96128
echo "Unable to fetch advertisement: '$url/adv/$thp'!" >&2
97129
exit 1
98130
fi
@@ -156,5 +188,10 @@ kid="$(jose jwk thp -i- -a "${CLEVIS_DEFAULT_THP_ALG}" <<< "$jwk")"
156188
jwe='{"protected":{"alg":"ECDH-ES","enc":"A256GCM","clevis":{"pin":"tang","tang":{}}}}'
157189
jwe="$(jose fmt -j "$jwe" -g protected -q "$kid" -s kid -UUo-)"
158190
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$url" -s url -UUUUo-)"
191+
# Persist any (m)TLS parameters so decryption can reuse them. These are file
192+
# paths (not secrets); the JWE header is stored in plaintext.
193+
[ -n "$cacert" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$cacert" -s cacert -UUUUo-)"
194+
[ -n "$cert" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$cert" -s cert -UUUUo-)"
195+
[ -n "$key" ] && jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -q "$key" -s key -UUUUo-)"
159196
jwe="$(jose fmt -j "$jwe" -g protected -g clevis -g tang -j- -s adv -UUUUo- <<< "$jwks")"
160197
exec jose jwe enc -i- -k- -I- -c < <(echo -n "$jwe$jwk"; /bin/cat)

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,50 @@ This command uses the following configuration properties:
7676
* *adv* (object) :
7777
A trusted advertisement (raw JSON)
7878

79+
* *cacert* (string) :
80+
Path to a CA bundle used to verify the Tang server's TLS certificate
81+
(passed to curl as --cacert). Only needed when the server certificate is
82+
signed by a CA that is not in the system trust store.
83+
84+
* *cert* (string) :
85+
Path to the client TLS certificate used for mutual TLS (mTLS)
86+
authentication (passed to curl as --cert).
87+
88+
* *key* (string) :
89+
Path to the private key for the client TLS certificate (passed to curl as
90+
--key). May be omitted if the private key is bundled in the *cert* file.
91+
92+
The *cacert*, *cert* and *key* properties are optional. When none are set,
93+
Clevis talks to the Tang server exactly as before (plain HTTP/HTTPS with
94+
default CA verification). These properties are file paths, not secrets, and
95+
are stored in the JWE header so that decryption reuses them automatically.
96+
97+
NOTE: When used for automatic unlocking at boot (for example with LUKS), the
98+
referenced certificate and key files must also be present at the same paths
99+
inside the initramfs, since decryption happens there.
100+
101+
== mTLS EXAMPLE
102+
103+
$ cfg='{"url":"https://tang.srv","cacert":"/etc/clevis/ca.crt",
104+
"cert":"/etc/clevis/client.crt","key":"/etc/clevis/client.key"}'
105+
$ clevis encrypt tang "$cfg" < PT > JWE
106+
107+
== mTLS SECURITY MODEL
108+
109+
The *cacert*, *cert* and *key* properties are stored as plaintext in the JWE
110+
header without authentication, so there is a chance they could be modified.
111+
In particular, if an attacker changes *cacert* to a CA they control, curl will
112+
trust a certificate signed by that CA, which lets a rogue server impersonate
113+
the Tang server. Exploiting this requires the attacker to have write
114+
access to the storage holding the JWE *and* a network position to impersonate
115+
the Tang server.
116+
117+
To keep the trust anchor under administrator control, you may prefer installing
118+
the Tang server's CA into the system trust store (for example with
119+
*update-ca-trust* or *update-ca-certificates*) and omitting *cacert*. Store any
120+
*cacert*, *cert* and *key* files referenced by the JWE in a root-owned location
121+
that is not writable by untrusted users.
122+
79123
== OPTIONS
80124

81125
* *-y* :

src/pins/tang/tests/meson.build

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

5252
test('pin-tang', find_program('pin-tang'), env: env)
53+
test('pin-tang-mtls', find_program('pin-tang-mtls'), env: env)
5354
test('tang-validate-adv', find_program('tang-validate-adv'), env: env)
5455
test('default-thp-alg', find_program('default-thp-alg'), env: env)

src/pins/tang/tests/pin-tang-mtls

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/bin/bash -xe
2+
# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
3+
#
4+
# Copyright (c) Meta Platforms, Inc. and affiliates.
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
#
19+
20+
. tang-common-test-functions
21+
22+
on_exit() {
23+
exit_status=$?
24+
[ -n "${TMP}" ] && tang_stop "${TMP}"
25+
[ -d "${TMP}" ] && rm -rf "$TMP"
26+
exit "${exit_status}"
27+
}
28+
29+
trap 'on_exit' EXIT
30+
31+
# Bail out early (as a skip) if the environment cannot do mTLS.
32+
tang_mtls_sanity_check
33+
34+
TMP="$(mktemp -d)"
35+
36+
# Generate CA, server and client certificates.
37+
tang_generate_certs "${TMP}"
38+
39+
# Start a TLS tang server that *requires* a client certificate (mutual TLS).
40+
tang_run_mtls "${TMP}" sig exc
41+
port=$(tang_get_port "${TMP}")
42+
43+
url="https://localhost:${port}"
44+
cacert="${TMP}/ca.crt"
45+
cert="${TMP}/client.crt"
46+
key="${TMP}/client.key"
47+
48+
# 1. Positive case: with a valid client certificate, encryption (which fetches
49+
# the advertisement via GET /adv) and decryption (POST /rec) must both work
50+
# over mutual TLS. '-y' skips the interactive trust check.
51+
cfg="$(printf '{"url":"%s","cacert":"%s","cert":"%s","key":"%s"}' \
52+
"$url" "$cacert" "$cert" "$key")"
53+
enc="$(echo -n "hi" | clevis encrypt tang "$cfg" -y)"
54+
dec="$(echo -n "$enc" | clevis decrypt)"
55+
test "$dec" == "hi"
56+
57+
# 2. The (m)TLS parameters must have been persisted into the JWE protected
58+
# header so that decryption can reuse them without any extra configuration.
59+
hdr="$(echo -n "${enc}" | cut -d. -f1)"
60+
jhd="$(jose b64 dec -i- <<< "${hdr}")"
61+
test "$(jose fmt -j- -Og clevis -g tang -g cacert -Su- <<< "$jhd")" == "${cacert}"
62+
test "$(jose fmt -j- -Og clevis -g tang -g cert -Su- <<< "$jhd")" == "${cert}"
63+
test "$(jose fmt -j- -Og clevis -g tang -g key -Su- <<< "$jhd")" == "${key}"
64+
65+
# 3. Negative case: mutual TLS must actually be enforced. Without a client
66+
# certificate the TLS handshake fails, so fetching the advertisement (and
67+
# therefore encryption) must fail.
68+
cfg_noclient="$(printf '{"url":"%s","cacert":"%s"}' "$url" "$cacert")"
69+
! echo -n "hi" | clevis encrypt tang "${cfg_noclient}" -y
70+
71+
# 4. Negative case: 'key' without 'cert' is a no-op for curl, so encryption
72+
# must be rejected up front rather than silently dropping the client key.
73+
cfg_keynocert="$(printf '{"url":"%s","cacert":"%s","key":"%s"}' \
74+
"$url" "$cacert" "$key")"
75+
! echo -n "hi" | clevis encrypt tang "${cfg_keynocert}" -y
76+
77+
# 5. Negative case: a referenced (m)TLS file that does not exist must be
78+
# reported at encrypt time instead of being baked into the JWE.
79+
cfg_badcert="$(printf '{"url":"%s","cacert":"%s","cert":"%s","key":"%s"}' \
80+
"$url" "$cacert" "${TMP}/does-not-exist.crt" "$key")"
81+
! echo -n "hi" | clevis encrypt tang "${cfg_badcert}" -y

src/pins/tang/tests/tang-common-test-functions.in

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,89 @@ run_test_server() {
179179
process_wait_until_port_ready "${pid}"
180180
process_find_port "${pid}" "tang" > "${portfile}"
181181
}
182+
183+
# Extra sanity check for the mutual-TLS (mTLS) tests: on top of the regular
184+
# tang requirements, we need the 'openssl' command and a socat built with
185+
# OpenSSL support (for the OPENSSL-LISTEN address type).
186+
tang_mtls_sanity_check() {
187+
tang_sanity_check
188+
command -v openssl >/dev/null 2>&1 \
189+
|| skip_test "mTLS test needs the 'openssl' command"
190+
"${SOCAT}" -V 2>/dev/null | grep -q -i 'openssl' \
191+
|| skip_test "mTLS test needs socat built with OpenSSL support"
192+
return 0
193+
}
194+
195+
# Generate a CA plus server and client certificates for the mTLS tests.
196+
# Creates, under ${basedir}:
197+
# ca.crt - CA certificate (curl --cacert / socat cafile=)
198+
# server.pem - server cert+key (socat cert=)
199+
# client.crt - client certificate (curl --cert)
200+
# client.key - client private key (curl --key)
201+
tang_generate_certs() {
202+
local basedir="${1}"
203+
[ -z "${basedir}" ] && error "tang_generate_certs: please specify 'basedir'"
204+
205+
local ca_key="${basedir}/ca.key"
206+
local ca_crt="${basedir}/ca.crt"
207+
local srv_key="${basedir}/server.key"
208+
local srv_crt="${basedir}/server.crt"
209+
local srv_csr="${basedir}/server.csr"
210+
local srv_pem="${basedir}/server.pem"
211+
local cli_key="${basedir}/client.key"
212+
local cli_crt="${basedir}/client.crt"
213+
local cli_csr="${basedir}/client.csr"
214+
215+
# Self-signed CA.
216+
openssl req -x509 -newkey rsa:2048 -nodes -keyout "${ca_key}" \
217+
-out "${ca_crt}" -subj "/CN=clevis-test-ca" -days 1
218+
219+
# Server certificate signed by the CA. The subjectAltName must include
220+
# 'localhost' or curl's hostname verification would reject it.
221+
openssl req -newkey rsa:2048 -nodes -keyout "${srv_key}" \
222+
-out "${srv_csr}" -subj "/CN=localhost"
223+
openssl x509 -req -in "${srv_csr}" -CA "${ca_crt}" -CAkey "${ca_key}" \
224+
-CAcreateserial -out "${srv_crt}" -days 1 \
225+
-extfile <(printf 'subjectAltName=DNS:localhost,IP:127.0.0.1')
226+
cat "${srv_crt}" "${srv_key}" > "${srv_pem}"
227+
228+
# Client certificate signed by the same CA.
229+
openssl req -newkey rsa:2048 -nodes -keyout "${cli_key}" \
230+
-out "${cli_csr}" -subj "/CN=clevis-test-client"
231+
openssl x509 -req -in "${cli_csr}" -CA "${ca_crt}" -CAkey "${ca_key}" \
232+
-CAcreateserial -out "${cli_crt}" -days 1
233+
234+
return 0
235+
}
236+
237+
# Start a test tang server fronted by TLS with mandatory client-certificate
238+
# verification, i.e. mutual TLS. Requires tang_generate_certs to have been
239+
# run against the same basedir first.
240+
tang_run_mtls() {
241+
tang_mtls_sanity_check
242+
local basedir="${1}"
243+
local sig_name="${2:-}"
244+
local exc_name="${3:-}"
245+
246+
[ -z "${basedir}" ] && error "tang_run_mtls: please specify 'basedir'"
247+
248+
if ! tang_new_keys "${basedir}" "" "${sig_name}" "${exc_name}"; then
249+
error "Error creating new keys for tang server"
250+
fi
251+
252+
local KEYS="${basedir}/db"
253+
local srv_pem="${basedir}/server.pem"
254+
local ca_crt="${basedir}/ca.crt"
255+
256+
local pid pidfile portfile
257+
pidfile="${basedir}/tang.pid"
258+
portfile="${basedir}/tang.port"
259+
260+
"${SOCAT}" OPENSSL-LISTEN:0,fork,reuseaddr,cert="${srv_pem}",cafile="${ca_crt}",verify=1 \
261+
exec:"${TANGD} ${KEYS}" &
262+
pid=$!
263+
264+
echo "${pid}" > "${pidfile}"
265+
process_wait_until_port_ready "${pid}"
266+
process_find_port "${pid}" "tang" > "${portfile}"
267+
}

0 commit comments

Comments
 (0)