diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 46347a86..f5e632c9 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -95,11 +95,19 @@ let # name alone resolves to two different groups. See `deploy.bao.tpmGid`. 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") { seal.pkcs11 = { lib = "${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so"; 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 \ --sopin="$(cat ${tokenStoreDir}/so-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 + # 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 # earlier version of this module is root-owned and unreadable to # the seal, and only the pins stay private to root. diff --git a/nix/module-eval.nix b/nix/module-eval.nix index 5ee51e0c..4028a376 100644 --- a/nix/module-eval.nix +++ b/nix/module-eval.nix @@ -226,6 +226,23 @@ let in (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 # read-only to the seal however it is owned, and the group is the only