From 2415f451a3218609960558de5539f5a0659e4180 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 10:45:00 +0200 Subject: [PATCH 01/16] Rework tpm2_capabilities.py Split the linear code into functions, and use argparse for argument parsing. The output remains the same. --- providers/tpm2/bin/tpm2_capabilities.py | 363 ++++++++++++++---------- 1 file changed, 206 insertions(+), 157 deletions(-) diff --git a/providers/tpm2/bin/tpm2_capabilities.py b/providers/tpm2/bin/tpm2_capabilities.py index 234267dd90..687076cb76 100755 --- a/providers/tpm2/bin/tpm2_capabilities.py +++ b/providers/tpm2/bin/tpm2_capabilities.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -import yaml +import argparse import subprocess -import sys + +import yaml # From TCG Algorithm Registry: Definition of TPM2_ALG_ID Constants # https://trustedcomputinggroup.org/wp-content/uploads/TCG-_Algorithm_Registry_r1p32_pub.pdf @@ -70,163 +71,211 @@ # TPM2_ALG_ECC # TPM2_ALG_SYMCIPHER -TPM2_CAP = { - "assymetric": set(), - "symmetric": set(), - "hash": set(), - "keyed_hash": set(), - "mask_generation_functions": set(), - "signature_schemes": set(), - "assymetric_encryption_scheme": set(), - "key_derivation_functions": set(), - "aes_modes": set(), - "pcr_banks": set(), -} - -try: - algs_caps = subprocess.check_output(["tpm2_getcap", "algorithms"]) - pcrs_caps = subprocess.check_output(["tpm2_getcap", "pcrs"]) -except (subprocess.CalledProcessError, FileNotFoundError): - raise SystemExit( - "Please make sure you have installed tpm-tools and tpm chip." - ) +def build_capabilities(): + """Query the TPM and return a dict of supported capability groups.""" + tpm2_cap = { + "assymetric": set(), + "symmetric": set(), + "hash": set(), + "keyed_hash": set(), + "mask_generation_functions": set(), + "signature_schemes": set(), + "assymetric_encryption_scheme": set(), + "key_derivation_functions": set(), + "aes_modes": set(), + "pcr_banks": set(), + } -algs_list = yaml.load(algs_caps, Loader=yaml.FullLoader) -pcrs_list = yaml.load(pcrs_caps, Loader=yaml.FullLoader) - -for alg, prop in algs_list.items(): - # Assymetric - if prop["value"] in (TPM2_ALG_RSA, TPM2_ALG_ECC): - TPM2_CAP["assymetric"].add(alg) - - # Symmetric - if prop["value"] in ( - TPM2_ALG_TDES, - TPM2_ALG_AES, - TPM2_ALG_CAMELLIA, - TPM2_ALG_SYMCIPHER, - ): - TPM2_CAP["symmetric"].add(alg) - - # Hash - if prop["value"] in ( - TPM2_ALG_SHA1, - TPM2_ALG_SHA256, - TPM2_ALG_SHA384, - TPM2_ALG_SHA512, - TPM2_ALG_SM3_256, - TPM2_ALG_SHA3_256, - TPM2_ALG_SHA3_384, - TPM2_ALG_SHA3_512, - ): - TPM2_CAP["hash"].add(alg) - - # Keyed hash - if prop["value"] in ( - TPM2_ALG_HMAC, - TPM2_ALG_XOR, - TPM2_ALG_CMAC, - TPM2_ALG_KEYEDHASH, - ): - TPM2_CAP["keyed_hash"].add(alg) - - # Mask Generation Functions - if prop["value"] in (TPM2_ALG_MGF1,): - TPM2_CAP["mask_generation_functions"].add(alg) - - # Signature Schemes - if prop["value"] in ( - TPM2_ALG_RSASSA, - TPM2_ALG_RSAPSS, - TPM2_ALG_ECDSA, - TPM2_ALG_ECDAA, - TPM2_ALG_ECSCHNORR, - TPM2_ALG_SM2, - TPM2_ALG_SM4, - ): - TPM2_CAP["signature_schemes"].add(alg) - - # Assymetric Encryption Scheme - if prop["value"] in (TPM2_ALG_OAEP, TPM2_ALG_RSAES, TPM2_ALG_ECDH): - TPM2_CAP["assymetric_encryption_scheme"].add(alg) - - # Key derivation functions - if prop["value"] in ( - TPM2_ALG_KDF1_SP800_56A, - TPM2_ALG_KDF2, - TPM2_ALG_KDF1_SP800_108, - TPM2_ALG_ECMQV, - ): - TPM2_CAP["key_derivation_functions"].add(alg) - - # AES Modes - if prop["value"] in ( - TPM2_ALG_CTR, - TPM2_ALG_OFB, - TPM2_ALG_CBC, - TPM2_ALG_CFB, - TPM2_ALG_ECB, - ): - TPM2_CAP["aes_modes"].add(alg) - -if "aes" in TPM2_CAP["symmetric"]: - for alg_type in ("aes", "aes128", "aes192", "aes256"): - try: - subprocess.check_call( - ["tpm2_testparms", alg_type], stderr=subprocess.DEVNULL - ) - TPM2_CAP["symmetric"].add(alg_type) - except subprocess.CalledProcessError: + try: + algs_caps = subprocess.check_output(["tpm2_getcap", "algorithms"]) + pcrs_caps = subprocess.check_output(["tpm2_getcap", "pcrs"]) + except (subprocess.CalledProcessError, FileNotFoundError): + raise SystemExit( + "Please make sure you have installed tpm-tools and tpm chip." + ) + + algs_list = yaml.load(algs_caps, Loader=yaml.FullLoader) + pcrs_list = yaml.load(pcrs_caps, Loader=yaml.FullLoader) + + for alg, prop in algs_list.items(): + # Assymetric + if prop["value"] in (TPM2_ALG_RSA, TPM2_ALG_ECC): + tpm2_cap["assymetric"].add(alg) + + # Symmetric + if prop["value"] in ( + TPM2_ALG_TDES, + TPM2_ALG_AES, + TPM2_ALG_CAMELLIA, + TPM2_ALG_SYMCIPHER, + ): + tpm2_cap["symmetric"].add(alg) + + # Hash + if prop["value"] in ( + TPM2_ALG_SHA1, + TPM2_ALG_SHA256, + TPM2_ALG_SHA384, + TPM2_ALG_SHA512, + TPM2_ALG_SM3_256, + TPM2_ALG_SHA3_256, + TPM2_ALG_SHA3_384, + TPM2_ALG_SHA3_512, + ): + tpm2_cap["hash"].add(alg) + + # Keyed hash + if prop["value"] in ( + TPM2_ALG_HMAC, + TPM2_ALG_XOR, + TPM2_ALG_CMAC, + TPM2_ALG_KEYEDHASH, + ): + tpm2_cap["keyed_hash"].add(alg) + + # Mask Generation Functions + if prop["value"] in (TPM2_ALG_MGF1,): + tpm2_cap["mask_generation_functions"].add(alg) + + # Signature Schemes + if prop["value"] in ( + TPM2_ALG_RSASSA, + TPM2_ALG_RSAPSS, + TPM2_ALG_ECDSA, + TPM2_ALG_ECDAA, + TPM2_ALG_ECSCHNORR, + TPM2_ALG_SM2, + TPM2_ALG_SM4, + ): + tpm2_cap["signature_schemes"].add(alg) + + # Assymetric Encryption Scheme + if prop["value"] in (TPM2_ALG_OAEP, TPM2_ALG_RSAES, TPM2_ALG_ECDH): + tpm2_cap["assymetric_encryption_scheme"].add(alg) + + # Key derivation functions + if prop["value"] in ( + TPM2_ALG_KDF1_SP800_56A, + TPM2_ALG_KDF2, + TPM2_ALG_KDF1_SP800_108, + TPM2_ALG_ECMQV, + ): + tpm2_cap["key_derivation_functions"].add(alg) + + # AES Modes + if prop["value"] in ( + TPM2_ALG_CTR, + TPM2_ALG_OFB, + TPM2_ALG_CBC, + TPM2_ALG_CFB, + TPM2_ALG_ECB, + ): + tpm2_cap["aes_modes"].add(alg) + + if "aes" in tpm2_cap["symmetric"]: + for alg_type in ("aes", "aes128", "aes192", "aes256"): try: - TPM2_CAP["symmetric"].remove(alg_type) - except KeyError: - pass - -if "ecc" in TPM2_CAP["assymetric"]: - for alg_type in ("ecc", "ecc192", "ecc224", "ecc256", "ecc384", "ecc521"): - try: - subprocess.check_call( - ["tpm2_testparms", alg_type], stderr=subprocess.DEVNULL - ) - TPM2_CAP["assymetric"].add(alg_type) - except subprocess.CalledProcessError: + subprocess.check_call( + ["tpm2_testparms", alg_type], stderr=subprocess.DEVNULL + ) + tpm2_cap["symmetric"].add(alg_type) + except subprocess.CalledProcessError: + try: + tpm2_cap["symmetric"].remove(alg_type) + except KeyError: + pass + + if "ecc" in tpm2_cap["assymetric"]: + for alg_type in ( + "ecc", + "ecc192", + "ecc224", + "ecc256", + "ecc384", + "ecc521", + ): try: - TPM2_CAP["assymetric"].remove(alg_type) - except KeyError: - pass - -if "rsa" in TPM2_CAP["assymetric"]: - for alg_type in ("rsa", "rsa1024", "rsa2048", "rsa4096"): - try: - subprocess.check_call( - ["tpm2_testparms", alg_type], stderr=subprocess.DEVNULL - ) - TPM2_CAP["assymetric"].add(alg_type) - except subprocess.CalledProcessError: + subprocess.check_call( + ["tpm2_testparms", alg_type], stderr=subprocess.DEVNULL + ) + tpm2_cap["assymetric"].add(alg_type) + except subprocess.CalledProcessError: + try: + tpm2_cap["assymetric"].remove(alg_type) + except KeyError: + pass + + if "rsa" in tpm2_cap["assymetric"]: + for alg_type in ("rsa", "rsa1024", "rsa2048", "rsa4096"): try: - TPM2_CAP["assymetric"].remove(alg_type) - except KeyError: - pass - -for pcr in pcrs_list["selected-pcrs"]: - for pcr_bank, pcr_ids in pcr.items(): - if set(range(24)).issubset(set(pcr_ids)): - TPM2_CAP["pcr_banks"].add(pcr_bank) - -if len(sys.argv) == 1: - # with no args print as resource unit - for k, v in TPM2_CAP.items(): - print("{}: {}".format(k, " ".join(sorted(v)))) - sys.exit(0) - -if len(sys.argv) != 3: - raise SystemExit("ERROR: use [capability] [supported-values] to test") - -try: - if sys.argv[2] in TPM2_CAP[sys.argv[1]]: - print("{} supports {}".format(*sys.argv[-2:])) - else: - raise SystemExit("{} does not support {}".format(*sys.argv[-2:])) -except KeyError: - raise SystemExit('Unknown capability "{}"'.format(sys.argv[1])) + subprocess.check_call( + ["tpm2_testparms", alg_type], stderr=subprocess.DEVNULL + ) + tpm2_cap["assymetric"].add(alg_type) + except subprocess.CalledProcessError: + try: + tpm2_cap["assymetric"].remove(alg_type) + except KeyError: + pass + + for pcr in pcrs_list["selected-pcrs"]: + for pcr_bank, pcr_ids in pcr.items(): + if set(range(24)).issubset(set(pcr_ids)): + tpm2_cap["pcr_banks"].add(pcr_bank) + + return tpm2_cap + + +def parse_args(argv=None): + parser = argparse.ArgumentParser( + description=( + "Query TPM2 capabilities. With no arguments, print all " + "capabilities as a Checkbox resource. With both CAPABILITY " + "and VALUE, check whether VALUE is supported for CAPABILITY." + ) + ) + parser.add_argument( + "capability", + nargs="?", + help=( + "Capability group to query " + "(e.g. hash, assymetric, pcr_banks)" + ), + ) + parser.add_argument( + "value", + nargs="?", + help="Value to check within that capability (e.g. sha256, rsa)", + ) + args = parser.parse_args(argv) + + if (args.capability is None) != (args.value is None): + parser.error("use [capability] [supported-values] to test") + + return args + + +def main(argv=None): + args = parse_args(argv) + tpm2_cap = build_capabilities() + + if args.capability is None: + # with no args print as resource unit + for k, v in tpm2_cap.items(): + print("{}: {}".format(k, " ".join(sorted(v)))) + return + + try: + if args.value in tpm2_cap[args.capability]: + print("{} supports {}".format(args.capability, args.value)) + else: + raise SystemExit( + "{} does not support {}".format(args.capability, args.value) + ) + except KeyError: + raise SystemExit('Unknown capability "{}"'.format(args.capability)) + + +if __name__ == "__main__": + main() From 5dbf27d61b6883d817c2132df05331e80d176ecf Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 10:49:01 +0200 Subject: [PATCH 02/16] Add --resource flag --- providers/tpm2/bin/tpm2_capabilities.py | 23 ++++++++++++++++++----- providers/tpm2/units/resource.pxu | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/providers/tpm2/bin/tpm2_capabilities.py b/providers/tpm2/bin/tpm2_capabilities.py index 687076cb76..3db4b8216e 100755 --- a/providers/tpm2/bin/tpm2_capabilities.py +++ b/providers/tpm2/bin/tpm2_capabilities.py @@ -230,11 +230,16 @@ def build_capabilities(): def parse_args(argv=None): parser = argparse.ArgumentParser( description=( - "Query TPM2 capabilities. With no arguments, print all " + "Query TPM2 capabilities. With --resource, print all " "capabilities as a Checkbox resource. With both CAPABILITY " "and VALUE, check whether VALUE is supported for CAPABILITY." ) ) + parser.add_argument( + "--resource", + action="store_true", + help="Print all capabilities as a Checkbox resource unit", + ) parser.add_argument( "capability", nargs="?", @@ -250,8 +255,16 @@ def parse_args(argv=None): ) args = parser.parse_args(argv) - if (args.capability is None) != (args.value is None): - parser.error("use [capability] [supported-values] to test") + if args.resource: + if args.capability is not None or args.value is not None: + parser.error( + "--resource cannot be combined with capability/value" + ) + else: + if args.capability is None or args.value is None: + parser.error( + "use --resource, or [capability] [supported-values] to test" + ) return args @@ -260,8 +273,8 @@ def main(argv=None): args = parse_args(argv) tpm2_cap = build_capabilities() - if args.capability is None: - # with no args print as resource unit + if args.resource: + # print as resource unit for k, v in tpm2_cap.items(): print("{}: {}".format(k, " ".join(sorted(v)))) return diff --git a/providers/tpm2/units/resource.pxu b/providers/tpm2/units/resource.pxu index f58571a765..6bd5c5ceae 100644 --- a/providers/tpm2/units/resource.pxu +++ b/providers/tpm2/units/resource.pxu @@ -44,4 +44,4 @@ estimated_duration: 1 _summary: tpm2 capabilities (algorithms and pcr banks) user: root command: - tpm2_capabilities.py + tpm2_capabilities.py --resource From d7ad1e69de0de9069c6c3382d5574cfeef78fc18 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 13:33:31 +0200 Subject: [PATCH 03/16] Add PCR banks resource to TPM2 provider Modify the script to add a `--resource-pcr-banks` option that spits out pcr_bank resource records for each supported PCR bank. For example: pcr_bank: sha256 pcr_bank: sha384 --- providers/tpm2/bin/tpm2_capabilities.py | 28 ++++++++++++++++++++++--- providers/tpm2/units/resource.pxu | 9 ++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/providers/tpm2/bin/tpm2_capabilities.py b/providers/tpm2/bin/tpm2_capabilities.py index 3db4b8216e..737a083e81 100755 --- a/providers/tpm2/bin/tpm2_capabilities.py +++ b/providers/tpm2/bin/tpm2_capabilities.py @@ -240,6 +240,14 @@ def parse_args(argv=None): action="store_true", help="Print all capabilities as a Checkbox resource unit", ) + parser.add_argument( + "--resource-pcr-banks", + action="store_true", + help=( + "Print each available PCR bank as a separate Checkbox " + "resource record" + ), + ) parser.add_argument( "capability", nargs="?", @@ -255,15 +263,21 @@ def parse_args(argv=None): ) args = parser.parse_args(argv) - if args.resource: + modes = [args.resource, args.resource_pcr_banks] + if any(modes): + if sum(modes) > 1: + parser.error( + "--resource and --resource-pcr-banks are mutually exclusive" + ) if args.capability is not None or args.value is not None: parser.error( - "--resource cannot be combined with capability/value" + "resource flags cannot be combined with capability/value" ) else: if args.capability is None or args.value is None: parser.error( - "use --resource, or [capability] [supported-values] to test" + "use --resource, --resource-pcr-banks, or " + "[capability] [supported-values] to test" ) return args @@ -279,6 +293,14 @@ def main(argv=None): print("{}: {}".format(k, " ".join(sorted(v)))) return + if args.resource_pcr_banks: + # print each PCR bank as a separate resource record + banks = sorted(tpm2_cap["pcr_banks"]) + print( + "\n\n".join("pcr_bank: {}".format(bank) for bank in banks) + ) + return + try: if args.value in tpm2_cap[args.capability]: print("{} supports {}".format(args.capability, args.value)) diff --git a/providers/tpm2/units/resource.pxu b/providers/tpm2/units/resource.pxu index 6bd5c5ceae..7e593042fa 100644 --- a/providers/tpm2/units/resource.pxu +++ b/providers/tpm2/units/resource.pxu @@ -45,3 +45,12 @@ _summary: tpm2 capabilities (algorithms and pcr banks) user: root command: tpm2_capabilities.py --resource + +id: tpm2_pcr_banks +category_id: tpm2 +plugin: resource +estimated_duration: 1 +_summary: Print each TPM2 PCR bank as a separate resource record +user: root +command: + tpm2_capabilities.py --resource-pcr-banks From acf5e720639152297d16cca77ae2dbcbd1f3e53a Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 13:37:10 +0200 Subject: [PATCH 04/16] New template jobs to test RSA and ECC encryption/decryption using Clevis For each PCR bank identified with the resource job, a separate job is created to test the encryption against the specific PCR bank. Since it's standard practice to have the same value for hashing algorithm and PCR bank, the same {pcr_bank} value is used when running the clevis-encrypt-tpm2 for both `hash` and `pcr_bank`. The clevis-automated test plan is updated to use the template jobs instead of the older ones. --- providers/tpm2/units/clevis.pxu | 38 ++++++++++++++++++++++++++++++ providers/tpm2/units/test-plan.pxu | 10 ++++---- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/providers/tpm2/units/clevis.pxu b/providers/tpm2/units/clevis.pxu index 2cd8fb30d2..e445cca1f1 100644 --- a/providers/tpm2/units/clevis.pxu +++ b/providers/tpm2/units/clevis.pxu @@ -53,6 +53,25 @@ command: echo "+ Compare with the original string" [[ $result == $rand ]] +unit: template +template-resource: tpm2_pcr_banks +template-unit: job +template-id: clevis-encrypt-tpm2/rsa_pcr_bank +id: clevis-encrypt-tpm2/rsa_{pcr_bank} +category_id: tpm2 +plugin: shell +depends: clevis-encrypt-tpm2/detect-rsa-capabilities +estimated_duration: 30 +_summary: clevis encrypt/decrypt key rsa using {pcr_bank} PCR bank and hash +user: root +command: + echo "+ Generate a random string" + rand=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c32768) + echo "+ Encrypt and decrypt the string using the TPM" + result=$(echo -n $rand | clevis-encrypt-tpm2 '{{"hash": "{pcr_bank}", "key":"rsa", "pcr_bank":"{pcr_bank}","pcr_ids":"0,1"}}' | clevis-decrypt-tpm2) + echo "+ Compare with the original string" + [[ $result == $rand ]] + id: clevis-encrypt-tpm2/detect-ecc-capabilities category_id: tpm2 plugin: shell @@ -78,3 +97,22 @@ command: result=$(echo -n $rand | clevis-encrypt-tpm2 '{"hash": "sha256", "key":"ecc", "pcr_bank":"sha256","pcr_ids":"0,1"}' | clevis-decrypt-tpm2) echo "+ Compare with the original string" [[ $result == $rand ]] + +unit: template +template-resource: tpm2_pcr_banks +template-unit: job +template-id: clevis-encrypt-tpm2/ecc_pcr_bank +id: clevis-encrypt-tpm2/ecc_{pcr_bank} +category_id: tpm2 +plugin: shell +depends: clevis-encrypt-tpm2/detect-ecc-capabilities +estimated_duration: 30 +_summary: clevis encrypt/decrypt key ecc using {pcr_bank} PCR bank and hash +user: root +command: + echo "+ Generate a random string" + rand=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c32768) + echo "+ Encrypt and decrypt the string using the TPM" + result=$(echo -n $rand | clevis-encrypt-tpm2 '{{"hash": "{pcr_bank}", "key":"ecc", "pcr_bank":"{pcr_bank}","pcr_ids":"0,1"}}' | clevis-decrypt-tpm2) + echo "+ Compare with the original string" + [[ $result == $rand ]] diff --git a/providers/tpm2/units/test-plan.pxu b/providers/tpm2/units/test-plan.pxu index adb5a14e8a..a5bc073580 100644 --- a/providers/tpm2/units/test-plan.pxu +++ b/providers/tpm2/units/test-plan.pxu @@ -88,14 +88,16 @@ _name: _description: Clevis encryption tests estimated_duration: 1m +mandatory_include: + com.canonical.plainbox::manifest +bootstrap_include: + tpm2_pcr_banks include: clevis-encrypt-tpm2/precheck certification-status=blocker clevis-encrypt-tpm2/detect-rsa-capabilities certification-status=blocker - clevis-encrypt-tpm2/rsa certification-status=blocker + clevis-encrypt-tpm2/rsa_pcr_bank certification-status=blocker clevis-encrypt-tpm2/detect-ecc-capabilities certification-status=blocker - clevis-encrypt-tpm2/ecc certification-status=blocker -mandatory_include: - com.canonical.plainbox::manifest + clevis-encrypt-tpm2/ecc_pcr_bank certification-status=blocker unit: test plan From 71693b70695fb6a73f5a58a7051318c7fbf65c13 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 13:44:53 +0200 Subject: [PATCH 05/16] Modify canary test plan --- providers/base/units/canary/test-plan.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/providers/base/units/canary/test-plan.yaml b/providers/base/units/canary/test-plan.yaml index 0c4d5ff61c..e4a4c76d55 100644 --- a/providers/base/units/canary/test-plan.yaml +++ b/providers/base/units/canary/test-plan.yaml @@ -20,6 +20,7 @@ bootstrap_include: - com.canonical.certification::dmi - com.canonical.certification::removable_partition - com.canonical.certification::serial_ports_static + - com.canonical.certification::tpm2_pcr_banks include: - com.canonical.plainbox::manifest - com.canonical.certification::executable @@ -87,9 +88,9 @@ include: - com.canonical.certification::socketcan/send_packet_local_fd_virtual - com.canonical.certification::clevis-encrypt-tpm2/precheck - com.canonical.certification::clevis-encrypt-tpm2/detect-rsa-capabilities - - com.canonical.certification::clevis-encrypt-tpm2/rsa + - com.canonical.certification::clevis-encrypt-tpm2/rsa_.* - com.canonical.certification::clevis-encrypt-tpm2/detect-ecc-capabilities - - com.canonical.certification::clevis-encrypt-tpm2/ecc + - com.canonical.certification::clevis-encrypt-tpm2/ecc_.* - com.canonical.certification::usb/storage-detect - com.canonical.certification::suspend/suspend_advanced_auto - com.canonical.certification::after-suspend-audio/detect-playback-devices From 1cce520289bb1e991eb7a0471b83ced2cce4f5c8 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 13:45:57 +0200 Subject: [PATCH 06/16] Remove old jobs They have been superseded by their template jobs equivalent and are not used anywhere in our generic providers. --- providers/tpm2/units/clevis.pxu | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/providers/tpm2/units/clevis.pxu b/providers/tpm2/units/clevis.pxu index e445cca1f1..9a3f7a8ebb 100644 --- a/providers/tpm2/units/clevis.pxu +++ b/providers/tpm2/units/clevis.pxu @@ -38,21 +38,6 @@ command: tpm2_capabilities.py pcr_banks sha256 && \ tpm2_capabilities.py assymetric rsa -id: clevis-encrypt-tpm2/rsa -category_id: tpm2 -plugin: shell -depends: clevis-encrypt-tpm2/detect-rsa-capabilities -estimated_duration: 30 -_summary: clevis encrypt/decrypt key rsa -user: root -command: - echo "+ Generate a random string" - rand=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c32768) - echo "+ Encrypt and decrypt the string using the TPM" - result=$(echo -n $rand | clevis-encrypt-tpm2 '{"hash": "sha256", "key":"rsa", "pcr_bank":"sha256","pcr_ids":"0,1"}' | clevis-decrypt-tpm2) - echo "+ Compare with the original string" - [[ $result == $rand ]] - unit: template template-resource: tpm2_pcr_banks template-unit: job @@ -83,21 +68,6 @@ command: tpm2_capabilities.py pcr_banks sha256 && \ tpm2_capabilities.py assymetric ecc -id: clevis-encrypt-tpm2/ecc -category_id: tpm2 -plugin: shell -depends: clevis-encrypt-tpm2/detect-ecc-capabilities -estimated_duration: 30 -_summary: clevis encrypt/decrypt key ecc -user: root -command: - echo "+ Generate a random string" - rand=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c32768) - echo "+ Encrypt and decrypt the string using the TPM" - result=$(echo -n $rand | clevis-encrypt-tpm2 '{"hash": "sha256", "key":"ecc", "pcr_bank":"sha256","pcr_ids":"0,1"}' | clevis-decrypt-tpm2) - echo "+ Compare with the original string" - [[ $result == $rand ]] - unit: template template-resource: tpm2_pcr_banks template-unit: job From f97a3580e68be1d1370ea3d3103c883f27485b0c Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 13:47:02 +0200 Subject: [PATCH 07/16] Add unit tests for tpm2_capabilities.py Add sample data from a desktop device with SHA256 PCR banks and hash, and use it in some unit tests. --- .../test_data/tpm2_getcap_algorithms.txt | 280 ++++++++++++++++++ .../tpm2/tests/test_data/tpm2_getcap_pcrs.txt | 4 + .../tpm2/tests/test_tpm2_capabilities.py | 247 +++++++++++++++ 3 files changed, 531 insertions(+) create mode 100644 providers/tpm2/tests/test_data/tpm2_getcap_algorithms.txt create mode 100644 providers/tpm2/tests/test_data/tpm2_getcap_pcrs.txt create mode 100644 providers/tpm2/tests/test_tpm2_capabilities.py diff --git a/providers/tpm2/tests/test_data/tpm2_getcap_algorithms.txt b/providers/tpm2/tests/test_data/tpm2_getcap_algorithms.txt new file mode 100644 index 0000000000..895632fc99 --- /dev/null +++ b/providers/tpm2/tests/test_data/tpm2_getcap_algorithms.txt @@ -0,0 +1,280 @@ +rsa: + value: 0x1 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 1 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +sha1: + value: 0x4 + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +hmac: + value: 0x5 + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 1 + encrypting: 0 + method: 0 +aes: + value: 0x6 + asymmetric: 0 + symmetric: 1 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +mgf1: + value: 0x7 + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 1 +keyedhash: + value: 0x8 + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 1 + reserved: 0x0 + signing: 1 + encrypting: 1 + method: 0 +xor: + value: 0xA + asymmetric: 0 + symmetric: 1 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +sha256: + value: 0xB + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +sha384: + value: 0xC + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +rsassa: + value: 0x14 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 1 + encrypting: 0 + method: 0 +rsaes: + value: 0x15 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 1 + method: 0 +rsapss: + value: 0x16 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 1 + encrypting: 0 + method: 0 +oaep: + value: 0x17 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 1 + method: 0 +ecdsa: + value: 0x18 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 1 + encrypting: 0 + method: 0 +ecdh: + value: 0x19 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 1 +ecdaa: + value: 0x1A + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 1 + encrypting: 0 + method: 0 +ecschnorr: + value: 0x1C + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 0 + reserved: 0x0 + signing: 1 + encrypting: 0 + method: 0 +kdf1_sp800_56a: + value: 0x20 + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 1 +kdf2: + value: 0x21 + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 1 +kdf1_sp800_108: + value: 0x22 + asymmetric: 0 + symmetric: 0 + hash: 1 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 1 +ecc: + value: 0x23 + asymmetric: 1 + symmetric: 0 + hash: 0 + object: 1 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +symcipher: + value: 0x25 + asymmetric: 0 + symmetric: 0 + hash: 0 + object: 1 + reserved: 0x0 + signing: 0 + encrypting: 0 + method: 0 +cmac: + value: 0x3F + asymmetric: 0 + symmetric: 1 + hash: 0 + object: 0 + reserved: 0x0 + signing: 1 + encrypting: 0 + method: 0 +ctr: + value: 0x40 + asymmetric: 0 + symmetric: 1 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 1 + method: 0 +ofb: + value: 0x41 + asymmetric: 0 + symmetric: 1 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 1 + method: 0 +cbc: + value: 0x42 + asymmetric: 0 + symmetric: 1 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 1 + method: 0 +cfb: + value: 0x43 + asymmetric: 0 + symmetric: 1 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 1 + method: 0 +ecb: + value: 0x44 + asymmetric: 0 + symmetric: 1 + hash: 0 + object: 0 + reserved: 0x0 + signing: 0 + encrypting: 1 + method: 0 diff --git a/providers/tpm2/tests/test_data/tpm2_getcap_pcrs.txt b/providers/tpm2/tests/test_data/tpm2_getcap_pcrs.txt new file mode 100644 index 0000000000..b9d4316392 --- /dev/null +++ b/providers/tpm2/tests/test_data/tpm2_getcap_pcrs.txt @@ -0,0 +1,4 @@ +selected-pcrs: + - sha1: [ ] + - sha256: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 ] + - sha384: [ ] diff --git a/providers/tpm2/tests/test_tpm2_capabilities.py b/providers/tpm2/tests/test_tpm2_capabilities.py new file mode 100644 index 0000000000..725edd52c7 --- /dev/null +++ b/providers/tpm2/tests/test_tpm2_capabilities.py @@ -0,0 +1,247 @@ +import io +import os +import subprocess +import unittest +from unittest.mock import patch + +import tpm2_capabilities + + +HERE = os.path.dirname(os.path.abspath(__file__)) +TEST_DATA = os.path.join(HERE, "test_data") + +with open(os.path.join(TEST_DATA, "tpm2_getcap_algorithms.txt"), "rb") as _f: + ALGS_SAMPLE = _f.read() +with open(os.path.join(TEST_DATA, "tpm2_getcap_pcrs.txt"), "rb") as _f: + PCRS_SAMPLE = _f.read() + + +def _fake_check_output(cmd, *args, **kwargs): + if cmd == ["tpm2_getcap", "algorithms"]: + return ALGS_SAMPLE + if cmd == ["tpm2_getcap", "pcrs"]: + return PCRS_SAMPLE + raise AssertionError("unexpected command: {}".format(cmd)) + + +def _fake_check_call_all_ok(cmd, *args, **kwargs): + # tpm2_testparms: pretend every requested variant is supported + return 0 + + +def _fake_check_call_all_fail(cmd, *args, **kwargs): + raise subprocess.CalledProcessError(1, cmd) + + +class TestBuildCapabilities(unittest.TestCase): + @patch( + "tpm2_capabilities.subprocess.check_call", + side_effect=_fake_check_call_all_ok, + ) + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=_fake_check_output, + ) + def test_classification_from_sample(self, _out, _call): + caps = tpm2_capabilities.build_capabilities() + + # Assymetric: rsa + ecc from sample, plus variants added by + # check_call succeeding for every tpm2_testparms probe. + self.assertIn("rsa", caps["assymetric"]) + self.assertIn("ecc", caps["assymetric"]) + self.assertIn("rsa2048", caps["assymetric"]) + self.assertIn("ecc256", caps["assymetric"]) + + # Symmetric: aes from sample; aes variants added. + self.assertIn("aes", caps["symmetric"]) + self.assertIn("aes128", caps["symmetric"]) + + # Hash algorithms in the sample. + self.assertEqual( + caps["hash"], {"sha1", "sha256", "sha384"} + ) + + # Keyed hash. + self.assertEqual( + caps["keyed_hash"], {"hmac", "xor", "keyedhash", "cmac"} + ) + + # Mask generation functions. + self.assertEqual(caps["mask_generation_functions"], {"mgf1"}) + + # Signature schemes present in sample. + self.assertEqual( + caps["signature_schemes"], + {"rsassa", "rsapss", "ecdsa", "ecdaa", "ecschnorr"}, + ) + + # Asymmetric encryption schemes. + self.assertEqual( + caps["assymetric_encryption_scheme"], + {"oaep", "rsaes", "ecdh"}, + ) + + # Key derivation functions. + self.assertEqual( + caps["key_derivation_functions"], + {"kdf1_sp800_56a", "kdf2", "kdf1_sp800_108"}, + ) + + # AES modes. + self.assertEqual( + caps["aes_modes"], {"ctr", "ofb", "cbc", "cfb", "ecb"} + ) + + # PCR banks: only sha256 has 0..23 in the sample. + self.assertEqual(caps["pcr_banks"], {"sha256"}) + + @patch( + "tpm2_capabilities.subprocess.check_call", + side_effect=_fake_check_call_all_fail, + ) + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=_fake_check_output, + ) + def test_variants_removed_when_testparms_fails(self, _out, _call): + caps = tpm2_capabilities.build_capabilities() + # When tpm2_testparms fails for every probed variant, none of the + # variants are added to the sets. The base entries themselves are + # also removed by the existing removal logic. + for variant in ("rsa", "rsa1024", "rsa2048", "rsa4096"): + self.assertNotIn(variant, caps["assymetric"]) + for variant in ("ecc", "ecc192", "ecc256", "ecc384"): + self.assertNotIn(variant, caps["assymetric"]) + for variant in ("aes", "aes128", "aes192", "aes256"): + self.assertNotIn(variant, caps["symmetric"]) + + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=FileNotFoundError, + ) + def test_missing_tpm2_tools_raises(self, _out): + with self.assertRaises(SystemExit): + tpm2_capabilities.build_capabilities() + + +class TestParseArgs(unittest.TestCase): + def test_resource_flag(self): + args = tpm2_capabilities.parse_args(["--resource"]) + self.assertTrue(args.resource) + self.assertFalse(args.resource_pcr_banks) + + def test_resource_pcr_banks_flag(self): + args = tpm2_capabilities.parse_args(["--resource-pcr-banks"]) + self.assertTrue(args.resource_pcr_banks) + + def test_positional_pair(self): + args = tpm2_capabilities.parse_args(["hash", "sha256"]) + self.assertEqual(args.capability, "hash") + self.assertEqual(args.value, "sha256") + + def test_no_args_errors(self): + with self.assertRaises(SystemExit): + tpm2_capabilities.parse_args([]) + + def test_single_positional_errors(self): + with self.assertRaises(SystemExit): + tpm2_capabilities.parse_args(["hash"]) + + def test_resource_with_positional_errors(self): + with self.assertRaises(SystemExit): + tpm2_capabilities.parse_args(["--resource", "hash", "sha256"]) + + def test_both_resource_flags_error(self): + with self.assertRaises(SystemExit): + tpm2_capabilities.parse_args( + ["--resource", "--resource-pcr-banks"] + ) + + +class TestMain(unittest.TestCase): + @patch( + "tpm2_capabilities.subprocess.check_call", + side_effect=_fake_check_call_all_ok, + ) + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=_fake_check_output, + ) + def test_resource_output_contains_all_keys(self, _out, _call): + buf = io.StringIO() + with patch("sys.stdout", buf): + tpm2_capabilities.main(["--resource"]) + output = buf.getvalue() + for key in ( + "assymetric", + "symmetric", + "hash", + "keyed_hash", + "mask_generation_functions", + "signature_schemes", + "assymetric_encryption_scheme", + "key_derivation_functions", + "aes_modes", + "pcr_banks", + ): + self.assertIn("{}:".format(key), output) + self.assertIn("pcr_banks: sha256", output) + + @patch( + "tpm2_capabilities.subprocess.check_call", + side_effect=_fake_check_call_all_ok, + ) + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=_fake_check_output, + ) + def test_resource_pcr_banks_output(self, _out, _call): + buf = io.StringIO() + with patch("sys.stdout", buf): + tpm2_capabilities.main(["--resource-pcr-banks"]) + # Only sha256 has 24 PCRs in the sample. + self.assertEqual(buf.getvalue().strip(), "pcr_bank: sha256") + + @patch( + "tpm2_capabilities.subprocess.check_call", + side_effect=_fake_check_call_all_ok, + ) + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=_fake_check_output, + ) + def test_check_supported_ok(self, _out, _call): + buf = io.StringIO() + with patch("sys.stdout", buf): + tpm2_capabilities.main(["hash", "sha256"]) + self.assertIn("hash supports sha256", buf.getvalue()) + + @patch( + "tpm2_capabilities.subprocess.check_call", + side_effect=_fake_check_call_all_ok, + ) + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=_fake_check_output, + ) + def test_check_unsupported_exits(self, _out, _call): + with self.assertRaises(SystemExit) as ctx: + tpm2_capabilities.main(["hash", "sha512"]) + self.assertIn("does not support", str(ctx.exception)) + + @patch( + "tpm2_capabilities.subprocess.check_call", + side_effect=_fake_check_call_all_ok, + ) + @patch( + "tpm2_capabilities.subprocess.check_output", + side_effect=_fake_check_output, + ) + def test_check_unknown_capability_exits(self, _out, _call): + with self.assertRaises(SystemExit) as ctx: + tpm2_capabilities.main(["nope", "sha256"]) + self.assertIn("Unknown capability", str(ctx.exception)) + + +if __name__ == "__main__": + unittest.main() From 129a67eaa16a8e17ab55d42bb05a4d1e65b88de3 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 14:34:10 +0200 Subject: [PATCH 08/16] Turn detect-[rsa|ecc]-capabilities into template jobs No more hardcoded sha256 expectations, and the capabilities tests are still valuable in case some devices in the future have a specific PCR bank that does not match any hashing algorithm. --- providers/base/units/canary/test-plan.yaml | 4 ++-- providers/tpm2/units/clevis.pxu | 28 ++++++++++++++-------- providers/tpm2/units/test-plan.pxu | 4 ++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/providers/base/units/canary/test-plan.yaml b/providers/base/units/canary/test-plan.yaml index e4a4c76d55..e89d92a010 100644 --- a/providers/base/units/canary/test-plan.yaml +++ b/providers/base/units/canary/test-plan.yaml @@ -87,9 +87,9 @@ include: - com.canonical.certification::socketcan/send_packet_local_eff_virtual - com.canonical.certification::socketcan/send_packet_local_fd_virtual - com.canonical.certification::clevis-encrypt-tpm2/precheck - - com.canonical.certification::clevis-encrypt-tpm2/detect-rsa-capabilities + - com.canonical.certification::clevis-encrypt-tpm2/detect-rsa-capabilities.* - com.canonical.certification::clevis-encrypt-tpm2/rsa_.* - - com.canonical.certification::clevis-encrypt-tpm2/detect-ecc-capabilities + - com.canonical.certification::clevis-encrypt-tpm2/detect-ecc-capabilities.* - com.canonical.certification::clevis-encrypt-tpm2/ecc_.* - com.canonical.certification::usb/storage-detect - com.canonical.certification::suspend/suspend_advanced_auto diff --git a/providers/tpm2/units/clevis.pxu b/providers/tpm2/units/clevis.pxu index 9a3f7a8ebb..4139899a0c 100644 --- a/providers/tpm2/units/clevis.pxu +++ b/providers/tpm2/units/clevis.pxu @@ -27,15 +27,19 @@ _summary: clevis encrypt/decrypt precheck flags: simple command: true -id: clevis-encrypt-tpm2/detect-rsa-capabilities +unit: template +template-resource: tpm2_pcr_banks +template-unit: job +template-id: clevis-encrypt-tpm2/detect-rsa-capabilities-pcr-bank +id: clevis-encrypt-tpm2/detect-rsa-capabilities-{pcr_bank} category_id: tpm2 plugin: shell depends: clevis-encrypt-tpm2/precheck user: root -_summary: Ensure the TPM has required capabilities for clevis RSA test +_summary: Ensure the TPM has required capabilities ({pcr_bank} PCR bank and hash) for clevis RSA command: - tpm2_capabilities.py hash sha256 && \ - tpm2_capabilities.py pcr_banks sha256 && \ + tpm2_capabilities.py hash {pcr_bank} && \ + tpm2_capabilities.py pcr_banks {pcr_bank} && \ tpm2_capabilities.py assymetric rsa unit: template @@ -45,7 +49,7 @@ template-id: clevis-encrypt-tpm2/rsa_pcr_bank id: clevis-encrypt-tpm2/rsa_{pcr_bank} category_id: tpm2 plugin: shell -depends: clevis-encrypt-tpm2/detect-rsa-capabilities +depends: clevis-encrypt-tpm2/detect-rsa-capabilities-{pcr_bank} estimated_duration: 30 _summary: clevis encrypt/decrypt key rsa using {pcr_bank} PCR bank and hash user: root @@ -57,15 +61,19 @@ command: echo "+ Compare with the original string" [[ $result == $rand ]] -id: clevis-encrypt-tpm2/detect-ecc-capabilities +unit: template +template-resource: tpm2_pcr_banks +template-unit: job +template-id: clevis-encrypt-tpm2/detect-ecc-capabilities-pcr-bank +id: clevis-encrypt-tpm2/detect-ecc-capabilities-{pcr_bank} category_id: tpm2 plugin: shell depends: clevis-encrypt-tpm2/precheck user: root -_summary: Ensure the TPM has required capabilities for clevis ECC test +_summary: Ensure the TPM has required capabilities ({pcr_bank} PCR bank and hash) for clevis ECC test command: - tpm2_capabilities.py hash sha256 && \ - tpm2_capabilities.py pcr_banks sha256 && \ + tpm2_capabilities.py hash {pcr_bank} && \ + tpm2_capabilities.py pcr_banks {pcr_bank} && \ tpm2_capabilities.py assymetric ecc unit: template @@ -75,7 +83,7 @@ template-id: clevis-encrypt-tpm2/ecc_pcr_bank id: clevis-encrypt-tpm2/ecc_{pcr_bank} category_id: tpm2 plugin: shell -depends: clevis-encrypt-tpm2/detect-ecc-capabilities +depends: clevis-encrypt-tpm2/detect-ecc-capabilities-{pcr_bank} estimated_duration: 30 _summary: clevis encrypt/decrypt key ecc using {pcr_bank} PCR bank and hash user: root diff --git a/providers/tpm2/units/test-plan.pxu b/providers/tpm2/units/test-plan.pxu index a5bc073580..f47cc1daeb 100644 --- a/providers/tpm2/units/test-plan.pxu +++ b/providers/tpm2/units/test-plan.pxu @@ -94,9 +94,9 @@ bootstrap_include: tpm2_pcr_banks include: clevis-encrypt-tpm2/precheck certification-status=blocker - clevis-encrypt-tpm2/detect-rsa-capabilities certification-status=blocker + clevis-encrypt-tpm2/detect-rsa-capabilities-pcr-bank certification-status=blocker clevis-encrypt-tpm2/rsa_pcr_bank certification-status=blocker - clevis-encrypt-tpm2/detect-ecc-capabilities certification-status=blocker + clevis-encrypt-tpm2/detect-ecc-capabilities-pcr-bank certification-status=blocker clevis-encrypt-tpm2/ecc_pcr_bank certification-status=blocker From 6fdb5ec7c7807418cb928b8389f3309fefa3cece Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 14:38:07 +0200 Subject: [PATCH 09/16] Add test run for TPM2 provider --- providers/tpm2/tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/providers/tpm2/tox.ini b/providers/tpm2/tox.ini index 7c34c69844..d7a61c6198 100644 --- a/providers/tpm2/tox.ini +++ b/providers/tpm2/tox.ini @@ -10,6 +10,9 @@ commands = {envpython} -m pip -q install ../../checkbox-support bash -c "for provider in ../*; do {envpython} $provider/manage.py develop -f; done" {envpython} manage.py validate + {envpython} -m coverage run manage.py test + {envpython} -m coverage report + {envpython} -m coverage xml setenv = PROVIDERPATH = {envdir} [testenv:py35] From a50e4bb36e5a179b273c0225aa4d162825e339ef Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 14:40:18 +0200 Subject: [PATCH 10/16] Only run unit tests for TPM2 providers This provider includes a lot of external Shell scripts that should not be tested, but it's still important to run the unit tests. --- providers/tpm2/tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/tpm2/tox.ini b/providers/tpm2/tox.ini index d7a61c6198..520859257f 100644 --- a/providers/tpm2/tox.ini +++ b/providers/tpm2/tox.ini @@ -10,7 +10,7 @@ commands = {envpython} -m pip -q install ../../checkbox-support bash -c "for provider in ../*; do {envpython} $provider/manage.py develop -f; done" {envpython} manage.py validate - {envpython} -m coverage run manage.py test + {envpython} -m coverage run manage.py test -u {envpython} -m coverage report {envpython} -m coverage xml setenv = PROVIDERPATH = {envdir} From b16518aea6aee2848403c7fd149108478b7b92db Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 14:45:09 +0200 Subject: [PATCH 11/16] black --- providers/tpm2/bin/tpm2_capabilities.py | 7 ++----- providers/tpm2/tests/test_tpm2_capabilities.py | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/providers/tpm2/bin/tpm2_capabilities.py b/providers/tpm2/bin/tpm2_capabilities.py index 737a083e81..170672d646 100755 --- a/providers/tpm2/bin/tpm2_capabilities.py +++ b/providers/tpm2/bin/tpm2_capabilities.py @@ -252,8 +252,7 @@ def parse_args(argv=None): "capability", nargs="?", help=( - "Capability group to query " - "(e.g. hash, assymetric, pcr_banks)" + "Capability group to query " "(e.g. hash, assymetric, pcr_banks)" ), ) parser.add_argument( @@ -296,9 +295,7 @@ def main(argv=None): if args.resource_pcr_banks: # print each PCR bank as a separate resource record banks = sorted(tpm2_cap["pcr_banks"]) - print( - "\n\n".join("pcr_bank: {}".format(bank) for bank in banks) - ) + print("\n\n".join("pcr_bank: {}".format(bank) for bank in banks)) return try: diff --git a/providers/tpm2/tests/test_tpm2_capabilities.py b/providers/tpm2/tests/test_tpm2_capabilities.py index 725edd52c7..662b7a7605 100644 --- a/providers/tpm2/tests/test_tpm2_capabilities.py +++ b/providers/tpm2/tests/test_tpm2_capabilities.py @@ -6,7 +6,6 @@ import tpm2_capabilities - HERE = os.path.dirname(os.path.abspath(__file__)) TEST_DATA = os.path.join(HERE, "test_data") @@ -57,9 +56,7 @@ def test_classification_from_sample(self, _out, _call): self.assertIn("aes128", caps["symmetric"]) # Hash algorithms in the sample. - self.assertEqual( - caps["hash"], {"sha1", "sha256", "sha384"} - ) + self.assertEqual(caps["hash"], {"sha1", "sha256", "sha384"}) # Keyed hash. self.assertEqual( From d4898a8cbe302c92dd18df6bd39b71a271b07ecc Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 14:57:22 +0200 Subject: [PATCH 12/16] Fix provider validation --- providers/tpm2/units/clevis.pxu | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/providers/tpm2/units/clevis.pxu b/providers/tpm2/units/clevis.pxu index 4139899a0c..ee5a026d4d 100644 --- a/providers/tpm2/units/clevis.pxu +++ b/providers/tpm2/units/clevis.pxu @@ -31,12 +31,13 @@ unit: template template-resource: tpm2_pcr_banks template-unit: job template-id: clevis-encrypt-tpm2/detect-rsa-capabilities-pcr-bank +template-summary: Ensure TPM has required capabilities for clevis RSA id: clevis-encrypt-tpm2/detect-rsa-capabilities-{pcr_bank} category_id: tpm2 plugin: shell depends: clevis-encrypt-tpm2/precheck user: root -_summary: Ensure the TPM has required capabilities ({pcr_bank} PCR bank and hash) for clevis RSA +_summary: Ensure TPM has required caps ({pcr_bank} PCR bank and hash) for clevis RSA command: tpm2_capabilities.py hash {pcr_bank} && \ tpm2_capabilities.py pcr_banks {pcr_bank} && \ @@ -46,6 +47,7 @@ unit: template template-resource: tpm2_pcr_banks template-unit: job template-id: clevis-encrypt-tpm2/rsa_pcr_bank +template-summary: clevis encrypt/decrypt key rsa id: clevis-encrypt-tpm2/rsa_{pcr_bank} category_id: tpm2 plugin: shell @@ -65,12 +67,13 @@ unit: template template-resource: tpm2_pcr_banks template-unit: job template-id: clevis-encrypt-tpm2/detect-ecc-capabilities-pcr-bank +template-summary: Ensure TPM has required capabilities for clevis ECC id: clevis-encrypt-tpm2/detect-ecc-capabilities-{pcr_bank} category_id: tpm2 plugin: shell depends: clevis-encrypt-tpm2/precheck user: root -_summary: Ensure the TPM has required capabilities ({pcr_bank} PCR bank and hash) for clevis ECC test +_summary: Ensure TPM has required caps ({pcr_bank} PCR bank and hash) for clevis ECC command: tpm2_capabilities.py hash {pcr_bank} && \ tpm2_capabilities.py pcr_banks {pcr_bank} && \ @@ -80,6 +83,7 @@ unit: template template-resource: tpm2_pcr_banks template-unit: job template-id: clevis-encrypt-tpm2/ecc_pcr_bank +template-summary: clevis encrypt/decrypt key ecc id: clevis-encrypt-tpm2/ecc_{pcr_bank} category_id: tpm2 plugin: shell From c89bd5722bddc2ee5929432deb7a0b079ce44bfe Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 15:06:46 +0200 Subject: [PATCH 13/16] Add coverage dependency to tox --- providers/tpm2/tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/providers/tpm2/tox.ini b/providers/tpm2/tox.ini index 520859257f..2fe039375f 100644 --- a/providers/tpm2/tox.ini +++ b/providers/tpm2/tox.ini @@ -21,6 +21,7 @@ deps = natsort == 4.0.3 requests == 2.9.1 urwid == 1.3.1 + coverage == 5.5 Jinja2 == 2.8 MarkupSafe == 0.23 XlsxWriter == 0.7.3 @@ -40,6 +41,7 @@ deps = natsort == 4.0.3 requests == 2.18.4 urwid == 2.0.1 + coverage == 5.5 Jinja2 == 2.10 MarkupSafe == 1.1.0 XlsxWriter == 0.9.6 @@ -54,6 +56,7 @@ deps = natsort == 7.0.1 requests == 2.22.0 urwid == 2.0.1 + coverage == 7.3.0 Jinja2 == 2.10.1 MarkupSafe == 1.1.0 XlsxWriter == 1.1.2 @@ -68,6 +71,7 @@ deps = natsort == 8.0.2 requests == 2.25.1 urwid == 2.1.2 + coverage == 7.3.0 Jinja2 == 3.0.3 MarkupSafe == 2.0.1 XlsxWriter == 3.0.2 @@ -82,6 +86,7 @@ deps = natsort == 8.0.2 requests == 2.31.0 urwid == 2.6.10 + coverage == 7.4.4 Jinja2 == 3.1.2 MarkupSafe == 2.1.5 XlsxWriter == 3.1.9 From 8a249fa9b79a4418689434ae4ed80f25790ccab4 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 15:32:14 +0200 Subject: [PATCH 14/16] Remove TPM2 tests from canary test plan These are still hosted in an external provider and it creates problems in our CI/CD. --- providers/base/units/canary/test-plan.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/providers/base/units/canary/test-plan.yaml b/providers/base/units/canary/test-plan.yaml index e89d92a010..d10c3a82d3 100644 --- a/providers/base/units/canary/test-plan.yaml +++ b/providers/base/units/canary/test-plan.yaml @@ -20,7 +20,6 @@ bootstrap_include: - com.canonical.certification::dmi - com.canonical.certification::removable_partition - com.canonical.certification::serial_ports_static - - com.canonical.certification::tpm2_pcr_banks include: - com.canonical.plainbox::manifest - com.canonical.certification::executable @@ -86,11 +85,6 @@ include: - com.canonical.certification::socketcan/send_packet_local_sff_virtual - com.canonical.certification::socketcan/send_packet_local_eff_virtual - com.canonical.certification::socketcan/send_packet_local_fd_virtual - - com.canonical.certification::clevis-encrypt-tpm2/precheck - - com.canonical.certification::clevis-encrypt-tpm2/detect-rsa-capabilities.* - - com.canonical.certification::clevis-encrypt-tpm2/rsa_.* - - com.canonical.certification::clevis-encrypt-tpm2/detect-ecc-capabilities.* - - com.canonical.certification::clevis-encrypt-tpm2/ecc_.* - com.canonical.certification::usb/storage-detect - com.canonical.certification::suspend/suspend_advanced_auto - com.canonical.certification::after-suspend-audio/detect-playback-devices From fdbc56b923f4786399c589d734474b70bb6c95b2 Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Tue, 28 Jul 2026 16:11:24 +0200 Subject: [PATCH 15/16] Fallback for yaml loader for older versions of Python --- providers/tpm2/bin/tpm2_capabilities.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/providers/tpm2/bin/tpm2_capabilities.py b/providers/tpm2/bin/tpm2_capabilities.py index 170672d646..74e162ba0f 100755 --- a/providers/tpm2/bin/tpm2_capabilities.py +++ b/providers/tpm2/bin/tpm2_capabilities.py @@ -95,8 +95,13 @@ def build_capabilities(): "Please make sure you have installed tpm-tools and tpm chip." ) - algs_list = yaml.load(algs_caps, Loader=yaml.FullLoader) - pcrs_list = yaml.load(pcrs_caps, Loader=yaml.FullLoader) + try: + Loader = yaml.FullLoader + except AttributeError: + Loader = yaml.SafeLoader # Fallback for older PyYAML / Python 3.6 + + algs_list = yaml.load(algs_caps, Loader=Loader) + pcrs_list = yaml.load(pcrs_caps, Loader=Loader) for alg, prop in algs_list.items(): # Assymetric From 82ee3196143824a5be1be22a7befdd11cc34f1be Mon Sep 17 00:00:00 2001 From: Pierre Equoy Date: Thu, 30 Jul 2026 09:11:25 +0200 Subject: [PATCH 16/16] Switch to SafeLoader There's no need for the FullLoader, since this is used only to parse some config-like strings. --- providers/tpm2/bin/tpm2_capabilities.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/providers/tpm2/bin/tpm2_capabilities.py b/providers/tpm2/bin/tpm2_capabilities.py index 74e162ba0f..e9fb9a7b2a 100755 --- a/providers/tpm2/bin/tpm2_capabilities.py +++ b/providers/tpm2/bin/tpm2_capabilities.py @@ -95,13 +95,8 @@ def build_capabilities(): "Please make sure you have installed tpm-tools and tpm chip." ) - try: - Loader = yaml.FullLoader - except AttributeError: - Loader = yaml.SafeLoader # Fallback for older PyYAML / Python 3.6 - - algs_list = yaml.load(algs_caps, Loader=Loader) - pcrs_list = yaml.load(pcrs_caps, Loader=Loader) + algs_list = yaml.load(algs_caps, Loader=yaml.SafeLoader) + pcrs_list = yaml.load(pcrs_caps, Loader=yaml.SafeLoader) for alg, prop in algs_list.items(): # Assymetric