fix(#3813): seal with an RSA-OAEP key, the only mechanism the TPM has

`bao operator init` fails at the seal with CKR_MECHANISM_INVALID. The
pkcs11 seal asks for AES-GCM by default and the TPM does not offer it: a
TPM 2.0's symmetric modes are CBC/CFB/CTR/OFB/ECB, and openbao accepts
only AEAD mechanisms — AES-GCM or RSA-OAEP — so RSA-OAEP is the single
mechanism both sides implement.

Measured on the deployed token (`pkcs11-tool --list-mechanisms`, #3860):
no AES-GCM, and `RSA-PKCS-OAEP, keySize={1024,2048}, hw, encrypt,
decrypt` present. CBC is not a fallback — openbao's
`MechanismFromString` rejects `CKM_AES_CBC_PAD` as deprecated and its
encrypt path implements exactly the two AEAD mechanisms.

The key gets a new label so a store provisioned by the earlier module
keeps its unusable AES key without the two resolving to one label, and
the addkey step is now keyed on the label rather than on the store not
existing — otherwise an existing deployment never gains the RSA key.
This commit is contained in:
atlas 2026-09-01 09:10:33 +02:00
commit fe81dcaf59
2 changed files with 46 additions and 8 deletions

View file

@ -95,11 +95,19 @@ let
# name alone resolves to two different groups. See `deploy.bao.tpmGid`. # name alone resolves to two different groups. See `deploy.bao.tpmGid`.
tpmGroup = "swarm-bao-tpm"; tpmGroup = "swarm-bao-tpm";
# RSA rather than AES, and the label says so because a store may still hold
# the AES key an earlier version of this module created: openbao's seal
# accepts only AEAD mechanisms — AES-GCM or RSA-OAEP — and a TPM 2.0 offers
# no GCM, so RSA-OAEP is the only one both sides have. A label resolving to
# both keys at once is an error, hence a distinct one.
sealKeyLabel = "swarm-bao-seal-rsa";
sealSettings = lib.optionalAttrs (baoDeploy.seal == "pkcs11") { sealSettings = lib.optionalAttrs (baoDeploy.seal == "pkcs11") {
seal.pkcs11 = { seal.pkcs11 = {
lib = "${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so"; lib = "${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so";
token_label = "swarm-bao"; token_label = "swarm-bao";
key_label = "swarm-bao-seal"; key_label = sealKeyLabel;
mechanism = "CKM_RSA_PKCS_OAEP";
}; };
}; };
@ -795,15 +803,28 @@ in
--label=swarm-bao \ --label=swarm-bao \
--sopin="$(cat ${tokenStoreDir}/so-pin)" \ --sopin="$(cat ${tokenStoreDir}/so-pin)" \
--userpin="$(cat ${tokenStoreDir}/user-pin)" --userpin="$(cat ${tokenStoreDir}/user-pin)"
# AES rather than RSA on purpose: openbao discussion 1826
# reports an RSA keypair here yielding duplicate labels, so
# `bao operator init` fails with "got more than 1 key for the
# label" and then CKR_GENERAL_ERROR. The seal supports AES-GCM.
tpm2_ptool addkey --path ${tokenStoreDir} --label=swarm-bao \
--userpin="$(cat ${tokenStoreDir}/user-pin)" \
--algorithm=aes256 --key-label=swarm-bao-seal
fi fi
# Keyed on the label, not on the store's existence: a store
# provisioned by an earlier version of this module has a token
# and an unusable AES key, and has to gain this one without
# being destroyed. See `sealKeyLabel` for why it is RSA.
#
# Substitution rather than a `grep -q` pipeline: `grep -q`
# exits at the first match and SIGPIPEs the lister, which
# `pipefail` reports as failure — the one wrong answer here
# adds a second key under the label and breaks the seal.
objects=$(tpm2_ptool listobjects --path ${tokenStoreDir} \
--label=swarm-bao || true)
case "$objects" in
*${sealKeyLabel}*) ;;
*)
tpm2_ptool addkey --path ${tokenStoreDir} --label=swarm-bao \
--userpin="$(cat ${tokenStoreDir}/user-pin)" \
--algorithm=rsa2048 --key-label=${sealKeyLabel}
;;
esac
# Outside the branch above on purpose: a store provisioned by an # Outside the branch above on purpose: a store provisioned by an
# earlier version of this module is root-owned and unreadable to # earlier version of this module is root-owned and unreadable to
# the seal, and only the pins stay private to root. # the seal, and only the pins stay private to root.

View file

@ -226,6 +226,23 @@ let
in in
(c.bindMounts ? "/dev/tpmrm0") && builtins.any (d: d.node == "/dev/tpmrm0") c.allowedDevices; (c.bindMounts ? "/dev/tpmrm0") && builtins.any (d: d.node == "/dev/tpmrm0") c.allowedDevices;
} }
{
# The stanza's label and the label the unit creates are two literals that
# have to name one object, and the mechanism is asserted with them
# because it is valid only for an RSA key: openbao takes AES-GCM or
# RSA-OAEP and a TPM 2.0 has neither GCM nor an opinion about which the
# seal asked for. Every wrong combination renders and deploys, and
# surfaces as a pkcs11 error at `operator init`.
name = "the seal asks for the RSA key the provisioning unit creates";
ok =
let
p = (baoSettings baoPkcs11).seal.pkcs11;
in
(p.mechanism or "") == "CKM_RSA_PKCS_OAEP"
&& lib.hasInfix "--algorithm=rsa2048 --key-label=${p.key_label or ""}" (
(baoUnits baoPkcs11).swarm-bao-token.script or ""
);
}
{ {
# `DynamicUser` implies `ProtectSystem=strict`, so the token directory is # `DynamicUser` implies `ProtectSystem=strict`, so the token directory is
# read-only to the seal however it is owned, and the group is the only # read-only to the seal however it is owned, and the group is the only