diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index f415dabc..e8f7d56e 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -72,6 +72,8 @@ jobs: run: make push - name: "Install KubeVirt" run: make install-kubevirt + - name: "Pre-pull images" + run: make pre-pull-images - name: "Run integration tests" run: | eval $(ssh-agent -s) diff --git a/Cargo.lock b/Cargo.lock index 606d4225..4d6b9007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,16 +577,20 @@ dependencies = [ [[package]] name = "compute-pcrs-lib" version = "0.1.0" -source = "git+https://github.com/trusted-execution-clusters/compute-pcrs#1e7b9f74206e436d1426c335e30b2f1a6bd1681e" +source = "git+https://github.com/trusted-execution-clusters/compute-pcrs#12a1d01e437a166b29331ad817375167d7bbfb5e" dependencies = [ "anyhow", "glob", "hex", "hex-literal", + "itertools 0.14.0", "lief", + "log", "openssl", "serde", + "serde_with", "sha2 0.10.9", + "strum 0.27.2", "uuid", ] @@ -787,6 +791,16 @@ dependencies = [ "darling_macro 0.20.11", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling" version = "0.23.0" @@ -811,6 +825,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_core" version = "0.23.0" @@ -835,6 +863,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.23.0" @@ -864,6 +903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -2030,6 +2070,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -2146,7 +2195,7 @@ checksum = "d9c6922f6afe80418dd6019818af5d0d34584c371780ff09b9752370c25b4abb" dependencies = [ "base64 0.22.1", "jiff", - "schemars", + "schemars 1.1.0", "serde", "serde_json", ] @@ -2283,7 +2332,7 @@ dependencies = [ "http 1.4.2", "jiff", "k8s-openapi 0.27.1", - "schemars", + "schemars 1.1.0", "serde", "serde-value", "serde_json", @@ -2302,7 +2351,7 @@ dependencies = [ "jiff", "json-patch", "k8s-openapi 0.28.0", - "schemars", + "schemars 1.1.0", "serde", "serde-value", "serde_json", @@ -2393,9 +2442,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "lief" -version = "0.17.1" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18773f648622afc222330700726227739936a1fa0040b91f007c80605525c4ff" +checksum = "4a90e3c2a97875731396a088732fe6a3736c51462ff1d3a1d179f296d539a5f7" dependencies = [ "bitflags 2.10.0", "cxx", @@ -2408,9 +2457,9 @@ dependencies = [ [[package]] name = "lief-build" -version = "0.17.1" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3abce5340e56c7f9191d699302b9496445b0438033b48a9937b41c506f677731" +checksum = "d8093a7e3cb03708149414d595e3c0df57b79e5136fee630e3c415d06e6c876e" dependencies = [ "git-version", "miette", @@ -2421,9 +2470,9 @@ dependencies = [ [[package]] name = "lief-ffi" -version = "0.17.1" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba2ccbe972b26212f69f7b938fc198214f4eaa7f548a8d724f1a04968137f96" +checksum = "60589ba353bc6e4db8cc4273afbfac2ab5e089583174275450618f4ed1cf51c4" dependencies = [ "autocxx", "cxx", @@ -3228,9 +3277,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", @@ -3530,6 +3579,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.1.0" @@ -3761,6 +3822,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -3940,6 +4032,9 @@ name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] [[package]] name = "strum" @@ -4158,10 +4253,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -4170,6 +4267,16 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -4280,9 +4387,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ "indexmap 2.14.0", "serde_core", @@ -4313,9 +4420,9 @@ dependencies = [ [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -4453,6 +4560,7 @@ dependencies = [ "anyhow", "cfg-if", "compute-pcrs-lib", + "hex", "k8s-openapi 0.28.0", "kube 4.0.0", "regex", @@ -4584,9 +4692,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.4" +version = "1.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" dependencies = [ "getrandom 0.4.1", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 02db0a13..cae6e2ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ clevis-pin-trustee-lib = { git = "https://github.com/latchset/clevis-pin-trustee compute-pcrs-lib = { git = "https://github.com/trusted-execution-clusters/compute-pcrs" } env_logger = { version = "0.11.10", default-features = false } http = "1.4.2" +hex = "0.4.3" ignition-config = "0.6.1" k8s-openapi = { version = "0.28.0", features = ["v1_35", "schemars"] } kube = { version = "4.0.0", default-features = false, features = ["derive", "runtime", "openssl-tls"] } diff --git a/Makefile b/Makefile index e29e750b..314824d8 100644 --- a/Makefile +++ b/Makefile @@ -170,7 +170,7 @@ endif sed 's//$(NAMESPACE)/g' kind/kbs-forward.yaml | $(KUBECTL) apply -f -; \ fi $(KUBECTL) apply -f $(DEPLOY_PATH)/trusted_execution_cluster_cr.yaml - $(KUBECTL) apply -f $(DEPLOY_PATH)/approved_image_cr.yaml + $(KUBECTL) apply -f '$(DEPLOY_PATH)/approved_image_cr_*.yaml' install-kubevirt: scripts/install-kubevirt.sh diff --git a/api/trusted-cluster-gen.go b/api/trusted-cluster-gen.go index 5c9eba97..914d3885 100644 --- a/api/trusted-cluster-gen.go +++ b/api/trusted-cluster-gen.go @@ -21,6 +21,43 @@ import ( "sigs.k8s.io/yaml" ) +type stringSlice []string + +func (s *stringSlice) String() string { + return strings.Join(*s, ", ") +} + +func (s *stringSlice) Set(value string) error { + *s = append(*s, value) + return nil +} + +type approvedImage struct { + Name string + Reference string +} + +type approvedImageSlice []approvedImage + +func (i *approvedImageSlice) String() string { + var res []string + for _, image := range *i { + res = append(res, fmt.Sprintf("%s,%s", image.Name, image.Reference)) + } + + return strings.Join(res, " ") +} + +func (i *approvedImageSlice) Set(value string) error { + split := strings.SplitN(value, ",", 2) + if len(split) != 2 { + return fmt.Errorf("approved images are expected to be comma-separated ',' pairs") + } + + *i = append(*i, approvedImage{Name: split[0], Reference: split[1]}) + return nil +} + type Args struct { outputDir string image string @@ -29,7 +66,7 @@ type Args struct { pcrsComputeImage string registerServerImage string attestationKeyRegisterImage string - approvedImage string + approvedImages approvedImageSlice } func main() { @@ -41,7 +78,7 @@ func main() { flag.StringVar(&args.pcrsComputeImage, "pcrs-compute-image", "quay.io/trusted-execution-clusters/compute-pcrs:latest", "Container image with the Trusted Execution Clusters compute-pcrs binary") flag.StringVar(&args.registerServerImage, "register-server-image", "quay.io/trusted-execution-clusters/register-server:latest", "Register server image to use in the deployment") flag.StringVar(&args.attestationKeyRegisterImage, "attestation-key-register-image", "quay.io/trusted-execution-clusters/attestation-key-register:latest", "Attestation key register image to use in the deployment") - flag.StringVar(&args.approvedImage, "approved-image", "", "When set, defines an initial approved image. Must be a bootable container image with SHA reference.") + flag.Var(&args.approvedImages, "approved-image", "When set, defines an initial approved image. It must be a comma-separated name,image-ref pair. Must be a bootable container image with SHA reference. Can be set multiple times.") flag.Parse() log.SetFlags(log.LstdFlags) @@ -184,34 +221,33 @@ func generateTrustedExecutionClusterCR(args *Args) error { } func generateApprovedImageCR(args *Args) error { - if args.approvedImage == "" { - return nil - } + for i, approvedImage := range args.approvedImages { + approvedImage := &v1alpha1.ApprovedImage{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.GroupVersion.String(), + Kind: "ApprovedImage", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: approvedImage.Name, + Namespace: args.namespace, + }, + Spec: v1alpha1.ApprovedImageSpec{ + Reference: approvedImage.Reference, + }, + } - approvedImage := &v1alpha1.ApprovedImage{ - TypeMeta: metav1.TypeMeta{ - APIVersion: v1alpha1.GroupVersion.String(), - Kind: "ApprovedImage", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "coreos", - Namespace: args.namespace, - }, - Spec: v1alpha1.ApprovedImageSpec{ - Reference: args.approvedImage, - }, - } + approvedImageYAML, err := yaml.Marshal(approvedImage) + if err != nil { + return fmt.Errorf("failed to marshal ApprovedImage CR %d: %v", i, err) + } - approvedImageYAML, err := yaml.Marshal(approvedImage) - if err != nil { - return fmt.Errorf("failed to marshal ApprovedImage CR: %v", err) + outputPath := filepath.Join(args.outputDir, fmt.Sprintf("approved_image_cr_%d.yaml", i)) + if err := writeResources(outputPath, []string{string(approvedImageYAML)}); err != nil { + return fmt.Errorf("failed to write %s: %v", outputPath, err) + } + log.Printf("Generated ApprovedImage CR at %s", outputPath) } - outputPath := filepath.Join(args.outputDir, "approved_image_cr.yaml") - if err := writeResources(outputPath, []string{string(approvedImageYAML)}); err != nil { - return fmt.Errorf("failed to write %s: %v", outputPath, err) - } - log.Printf("Generated ApprovedImage CR at %s", outputPath) return nil } diff --git a/compute-pcrs/src/main.rs b/compute-pcrs/src/main.rs index 0c1bfacf..0241f37f 100644 --- a/compute-pcrs/src/main.rs +++ b/compute-pcrs/src/main.rs @@ -43,12 +43,11 @@ async fn main() -> Result<()> { let os_id = get_val("ID")?; let os_version_id = get_val("VERSION_ID")?; - let efivars = format!("/reference-values/efivars/qemu-ovmf/{os_id}-{os_version_id}"); let mokvars = format!("/reference-values/mok-variables/{os_id}-{os_version_id}"); let pcrs = vec![ compute_pcr4(&kernels, &esp, false, true), - compute_pcr7(Some(&efivars), &esp, true), + // TODO: compute pcr7 when platform EFI vars can be elucidated compute_pcr14(&mokvars), ]; diff --git a/docs/dev/test_approved_images.md b/docs/dev/test_approved_images.md new file mode 100644 index 00000000..0b601995 --- /dev/null +++ b/docs/dev/test_approved_images.md @@ -0,0 +1,130 @@ +# Approved images for integration tests + +There are two approved images that are used to run operator's +integration tests: The "primary", held by the `APPROVED_IMAGE` +environment variable in the [`Makefile`](/Makefile) and the "secondary", +defined in the `COMBINE_PCRS_UPDATE_TEST_IMAGE_REF` constant in +[`test_utils/src/constants.rs`](/tests/trusted_execution_cluster.rs). + +Integration tests assume those are the images that the VMs joining the +test cluster are booting. Based on that assumption, a set of PCRs are +defined as constants in +[`test_utils/src/constants.rs`](/test_utils/src/constants.rs). +Constant variable names try to be self-explanatory. + +In case any of the images need to be updated, the constant PCR values +that integration tests rely on might very likely need to be updated too. + +## Updating approved images for integration tests +Approved images used in integration tests are usually Fedora CoreOS +images that are pinned in the trusted-execution-clusters quay.io. That +is, basically pulled from a CoreOS mirror and pushed into this +organization's. This is done so images are not garbage collected in the +original mirror and the team is not kept busy dealing with test approved +image updates so frequently. + +Between the pull and the push, some image modifications shall be applied +to prepare the images to behave under the test cases. Mainly, to inject +the clevis-pin and a custom ignition to them. There is a helpful +[Containerfile][approved-image-containerfile] available in the investigations repository that does +exactly that. + +[approved-image-containerfile]: https://github.com/trusted-execution-clusters/investigations/blob/3321d58394131b8c56430cf11a29dc0076c00f37/coreos/Containerfile + +## Updating reference PCR values for integration tests + +### When to update reference PCR value constants +In the event of an approved image pointer update, integration tests will +very likely break. Updating the reference PCR values should be enough to +fix them back. + +In case the primary approved image was updated, `PRIMARY_*_HASH` consts +will need an update, as well as the `MIX_*_KERNEL_PCR4_HASH` consts. + +For secondary approved image updates, `SECONDARY_*_HASH` and +`MIX_*_KERNEL_PCR4_HASH` constants will need to be updated. + +In case both primary and secondary images are updated, all hashes will +need to be updated. + +### How to compute reference PCR value constants +The easiest way to compute the new PCR values is certainly to use the +compute-pcrs library against the new image. However, that might not be +the most desirable way to do it, as it would implicitly bypass some of +the purpose of the integration tests. + +The better way to do it is to boot the new image in a disposable VM and +check tpm event log. + +In this document section we are going to focus just in PCR4. + +First, a qcow2 image is needed. The bootable container image can be +"turned" into a qcow2 image. Another way, when it comes to CoreOS is to +find the image in the [CoreOS builds browser][coreos-build-browser] and +obtain its already built qcow2 relative. + +To boot the VM, the investigations [`install_vm.sh`][installvm] script +might come in handy. + +#### Boot stack event hashes +Once booted, log into the VM and run: +```bash +$ sudo tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements | \ + yq '.events[] | + select(.PCRIndex == 4 and + .EventType == "EV_EFI_BOOT_SERVICES_APPLICATION") | + .Digests[] | + select(.AlgorithmId == "sha256") | + .Digest' +``` +This should print 3 sha256 hash values. The ones relative to (in order +of appearance) shim, grub and the vmlinuz hashes. + +If 3 values are not printed, it might be that secure boot was not +enabled in the VM, and the hash values should be just shim and grub's. +Make sure secure boot is enabled and try again. + +Take those values and update the values of +`{PRIMARY|SECONDARY}_{SHIM,GRUB,KERNEL}_HASH` constants accordingly. + +#### PCR4 hashes +Then again, run the following command in the VM: +```bash +$ sudo tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements | \ + yq '.pcrs.sha256.4' | \ + sed 's/^0x//' +``` +This will print the resulting PCR4 hash of the new approved image. +Update the `PRIMARY_PCR4_HASH` or `SECONDARY_PCR4_HASH` value. + +#### Combined PCR4 hashes +The tricky part comes with the combination (or mix) PCR4 hashes. These +model possible intermediate stages that the VM goes through during an +update. That includes the VM booting a new kernel through the old +bootloader, or booting the old kernel with a new bootloader in case of a +rollback being needed. + +Set the shim and grub hashes of the primary image and the kernel hash of +the secondary image and run the following bash script: +```bash +hashes=( + "3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba" + "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119" + "${SHIM_HASH}" + "${GRUB_HASH}" + "${KERNEL_HASH}" +) +current_pcr="0000000000000000000000000000000000000000000000000000000000000000" +for event_hash in "${hashes[@]}"; do + current_pcr=$(echo -n "${current_pcr}${event_hash}" | xxd -r -p | sha256sum | awk '{print $1}') +done +echo $current_pcr +``` + +The output is `MIX_PRIMARY_BOOT_SECONDARY_KERNEL_PCR4_HASH`. Then repeat +again with shim and grub hashes from the secondary image and the kernel +hash of the primary image to obtain +`MIX_SECONDARY_BOOT_PRIMARY_KERNEL_PCR4_HASH`. + +[coreos-build-browser]: https://builds.coreos.fedoraproject.org/browser?stream=stable&arch=x86_64 +[installvm]: https://github.com/trusted-execution-clusters/investigations/blob/f3bcaa95fff7c39092fae5cc63f9b31a2aacb221/scripts/install_vm.sh diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 1496094a..83e5451a 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -17,7 +17,7 @@ trusted-cluster-operator-lib = { path = "../lib" } compute-pcrs-lib.workspace = true env_logger.workspace = true futures-util = "0.3.32" -hex = "0.4.3" +hex.workspace = true json-patch = "4.2.0" jsonptr = "0.7.1" k8s-openapi.workspace = true diff --git a/operator/src/test_utils.rs b/operator/src/test_utils.rs index af4ed28c..f21fecb7 100644 --- a/operator/src/test_utils.rs +++ b/operator/src/test_utils.rs @@ -3,12 +3,18 @@ // SPDX-License-Identifier: MIT use compute_pcrs_lib::Pcr; +use compute_pcrs_lib::tpmevents::{TPMEvent, TPMEventID}; use k8s_openapi::{api::core::v1::ConfigMap, jiff::Timestamp}; use std::collections::BTreeMap; use crate::trustee; use trusted_cluster_operator_lib::reference_values::{ImagePcr, ImagePcrs, PCR_CONFIG_FILE}; +pub const DUMMY_PCR_4_VALUE: &str = + "3f263b96ccbc33bb53d808771f9ab1e02d4dec8854f9530f749cde853a723273"; +pub const DUMMY_PCR_7_VALUE: &str = + "e58ada1ba75f2e4722b539824598ad5e10c55f2e4aeab2033f3b0a8ee3f3eca6"; + pub fn dummy_pcrs() -> ImagePcrs { ImagePcrs(BTreeMap::from([( "cos".to_string(), @@ -16,17 +22,33 @@ pub fn dummy_pcrs() -> ImagePcrs { first_seen: Timestamp::now(), pcrs: vec![ Pcr { - id: 0, - value: "pcr0_val".to_string(), - parts: vec![], + id: 4, + value: hex::decode(DUMMY_PCR_4_VALUE).unwrap(), + events: vec![TPMEvent { + name: "EV_EFI_ACTION".into(), + pcr: 4, + hash: hex::decode( + "3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba", + ) + .unwrap(), + id: TPMEventID::Pcr4EfiCall, + }], }, Pcr { - id: 1, - value: "pcr1_val".to_string(), - parts: vec![], + id: 7, + value: hex::decode(DUMMY_PCR_7_VALUE).unwrap(), + events: vec![TPMEvent { + name: "EV_EFI_VARIABLE_DRIVER_CONFIG".into(), + pcr: 7, + hash: hex::decode( + "ccfc4bb32888a345bc8aeadaba552b627d99348c767681ab3141f5b01e40a40e", + ) + .unwrap(), + id: TPMEventID::Pcr7SecureBoot, + }], }, ], - reference: "ref".to_string(), + reference: "".to_string(), }, )])) } diff --git a/operator/src/trustee.rs b/operator/src/trustee.rs index d8d63d16..1000af65 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -9,6 +9,8 @@ use anyhow::{Context, Result}; use base64::{Engine as _, engine::general_purpose}; use chrono::{DateTime, Utc}; use clevis_pin_trustee_lib::Key as ClevisKey; +use compute_pcrs_lib::tpmevents::TPMEvent; +use compute_pcrs_lib::tpmevents::combine::combine_images; use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec}; use k8s_openapi::api::core::v1::{ ConfigMap, ConfigMapVolumeSource, Container, ContainerPort, EmptyDirVolumeSource, EnvVar, @@ -28,7 +30,7 @@ use log::info; use operator::{TLS_DIR, create_or_info_if_exists, read_certificate}; use serde::{Serialize, Serializer}; use serde_json::{Value::String as JsonString, json}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use trusted_cluster_operator_lib::endpoints::*; use trusted_cluster_operator_lib::reference_values::*; @@ -72,14 +74,20 @@ pub fn get_image_pcrs(image_pcrs_map: ConfigMap) -> Result { } fn recompute_reference_values(image_pcrs: ImagePcrs) -> Vec { - // TODO many grub+shim:many OS image recompute once supported let mut reference_values_in = - BTreeMap::from([("svn".to_string(), vec![JsonString("1".to_string())])]); - for pcr in image_pcrs.0.values().flat_map(|v| &v.pcrs) { + BTreeMap::from([("svn".to_string(), BTreeSet::from(["1".to_string()]))]); + let tpm_events: Vec> = image_pcrs + .0 + .values() + .map(|v| v.pcrs.iter().flat_map(|p| p.events.clone()).collect()) + .collect(); + + let pcr_combinations = combine_images(&tpm_events); + for pcr in pcr_combinations.iter().flatten() { reference_values_in .entry(format!("pcr{}", pcr.id)) .or_default() - .push(JsonString(pcr.value.clone())); + .insert(hex::encode(&pcr.value)); } reference_values_in .iter() @@ -87,7 +95,7 @@ fn recompute_reference_values(image_pcrs: ImagePcrs) -> Vec { version: "0.1.0".to_string(), name: format!("tpm_{name}"), expiration: Utc::now() + chrono::Duration::days(365), - value: serde_json::Value::Array(values.to_vec()), + value: serde_json::Value::Array(values.iter().map(|v| JsonString(v.clone())).collect()), }) .collect() } @@ -589,17 +597,38 @@ pub async fn generate_kbs_deployment( mod tests { use super::*; use crate::test_utils::*; + use compute_pcrs_lib::Pcr; + use compute_pcrs_lib::tpmevents::TPMEventID; use http::{Method, Request, StatusCode}; + use k8s_openapi::jiff::Timestamp; use kube::client::Body; + use trusted_cluster_operator_test_utils::constants::*; use trusted_cluster_operator_test_utils::mock_client::*; use trusted_cluster_operator_test_utils::test_error_method; + use trusted_cluster_operator_test_utils::*; + + fn reference_values_from(reference_values: &[ReferenceValue], rv_name: &str) -> Vec { + let rv = reference_values + .iter() + .find(|rv| rv.name == rv_name) + .unwrap(); + let val_arr = rv.value.as_array().unwrap(); + val_arr.iter().map(|v| v.as_str().unwrap().into()).collect() + } #[test] fn test_get_image_pcrs_success() { let config_map = dummy_pcrs_map(); let image_pcrs = get_image_pcrs(config_map).unwrap(); assert_eq!(image_pcrs.0["cos"].pcrs.len(), 2); - assert_eq!(image_pcrs.0["cos"].pcrs[0].value, "pcr0_val"); + assert_eq!( + hex::encode(&image_pcrs.0["cos"].pcrs[0].value), + DUMMY_PCR_4_VALUE + ); + assert_eq!( + hex::encode(&image_pcrs.0["cos"].pcrs[1].value), + "e58ada1ba75f2e4722b539824598ad5e10c55f2e4aeab2033f3b0a8ee3f3eca6" + ); } #[test] @@ -633,10 +662,10 @@ mod tests { fn test_recompute_reference_values() { let result = recompute_reference_values(dummy_pcrs()); assert_eq!(result.len(), 3); - let rv = result.iter().find(|rv| rv.name == "tpm_pcr0").unwrap(); - let val_arr = rv.value.as_array().unwrap(); - let vals: Vec<_> = val_arr.iter().map(|v| v.as_str().unwrap()).collect(); - assert_eq!(vals, vec!["pcr0_val".to_string()]); + let vals = reference_values_from(&result, "tpm_pcr4"); + assert_eq!(vals, vec![DUMMY_PCR_4_VALUE,]); + let vals = reference_values_from(&result, "tpm_pcr7"); + assert_eq!(vals, vec![DUMMY_PCR_7_VALUE,]); } #[tokio::test] @@ -893,4 +922,71 @@ mod tests { let clos = |client| generate_kbs_deployment(client, Default::default(), "image", &None); test_error_method!(clos, Method::POST); } + + #[test] + fn test_recompute_reference_values_pcr4() { + let cos2_pcr4_hash = "c7fc63ec604348d8258993a9e344ba72041afd1473ad291a3171199b551aedbd"; + let image_pcrs = ImagePcrs(BTreeMap::from([ + ( + "cos1".to_string(), + ImagePcr { + first_seen: Timestamp::now(), + pcrs: vec![primary_pcr4!(), expected_pcr7!()], + reference: "".to_string(), + }, + ), + ( + "cos2".to_string(), + ImagePcr { + first_seen: Timestamp::now(), + pcrs: vec![Pcr { + id: 4, + value: hex::decode(cos2_pcr4_hash).unwrap(), + events: vec![ + pcr4_ev_efi_action_event!(), + pcr_separator_event!(4, TPMEventID::Pcr4Separator), + TPMEvent { + name: "EV_EFI_BOOT_SERVICES_APPLICATION".into(), + pcr: 4, + hash: hex::decode("1fed6fad5ca735adc80615d2a7e795e2f17f84e407b07979498c9edb1e04383f") + .unwrap(), + id: TPMEventID::Pcr4Shim, + }, + TPMEvent { + name: "EV_EFI_BOOT_SERVICES_APPLICATION".into(), + pcr: 4, + hash: hex::decode("8f3adc6b42da2defa6d5ef3202badc39a5a22ceec068f106760592163a505a0e") + .unwrap(), + id: TPMEventID::Pcr4Grub, + }, + TPMEvent { + name: "EV_EFI_BOOT_SERVICES_APPLICATION".into(), + pcr: 4, + hash: hex::decode("772c3a90820e4a76944d3715e6f700bc41e846b0049b7817f9feb3289a56d3f8") + .unwrap(), + id: TPMEventID::Pcr4Vmlinuz, + }, + ], + }, + expected_pcr7!()], + reference: "".to_string(), + }, + ), + ])); + + let result = recompute_reference_values(image_pcrs); + assert_eq!(result.len(), 3); + let vals_pcr4 = reference_values_from(&result, "tpm_pcr4"); + assert_eq!( + vals_pcr4, + vec![ + "514259b499f88d74cce9ff4763bb95d5c4e9a6703df48467a99dbcae02c3d974", + cos2_pcr4_hash, + "c9c3add791efc98f59977c89e673a34ad0b357872e9eb2c43d14607488e5d9e2", + PRIMARY_PCR4_HASH + ] + ); + let vals_pcr7 = reference_values_from(&result, "tpm_pcr7"); + assert_eq!(vals_pcr7, vec![PCR7_HASH]); + } } diff --git a/scripts/pre-pull-images.sh b/scripts/pre-pull-images.sh index 5c002c9f..91209f0f 100755 --- a/scripts/pre-pull-images.sh +++ b/scripts/pre-pull-images.sh @@ -4,15 +4,27 @@ # # SPDX-License-Identifier: CC0-1.0 +secondary_approved_image() { + sed -nE 's/pub const COMBINE_PCRS_UPDATE_TEST_IMAGE_REF: &str = "(.*)";/\1/p' test_utils/src/constants.rs +} ## Special handling for the approved image because of the kind issue with ## loading images used by image volumes. See: ## https://github.com/kubernetes-sigs/kind/issues/4099 -LOCAL_APPROVED_IMAGE=localhost:5000/approved-image:latest -docker pull $APPROVED_IMAGE -docker tag $APPROVED_IMAGE $LOCAL_APPROVED_IMAGE -docker push $LOCAL_APPROVED_IMAGE -docker exec -ti kind-control-plane crictl pull $LOCAL_APPROVED_IMAGE +pull_approved_image() { + local image=$1 + local index=$2 + local local_image="localhost:5000/approved-image-${index}:latest" + echo "Pulling approved image: $image" + docker pull "$image" + docker tag "$image" "$local_image" + docker push "$local_image" + docker exec -ti kind-control-plane crictl pull "$local_image" + echo "-------------------------------" +} + +pull_approved_image "$APPROVED_IMAGE" 0 +pull_approved_image "$(secondary_approved_image)" 1 KV_VERSION=v1.7.0 IMAGES=( diff --git a/test_utils/src/constants.rs b/test_utils/src/constants.rs new file mode 100644 index 00000000..4ac2daea --- /dev/null +++ b/test_utils/src/constants.rs @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: BeƱat Gartzia Arruabarrena +// +// SPDX-License-Identifier: MIT + +pub const TEC_NAME: &str = "trusted-execution-cluster"; +pub const TRUSTEE_CONFIG_MAP: &str = "trustee-data"; +pub const RV_JSON_KEY: &str = "reference-values.json"; + +pub const APPROVED_IMAGE_NAME: &str = "coreos-approved-primary"; +pub const COMBINE_PCRS_UPDATE_TEST_IMAGE_NAME: &str = "coreos-test-update-combination"; +// TODO: This image does not contain a custom ignition, neither a clevis pin. +// It is just right to check that the operator fills the expected trustee +// reference values in the integration test covering PCR combination. +// However, it is not an image that supports attestation and so it will +// always boot, even if reference values or policies were not configured +// properly. +pub const COMBINE_PCRS_UPDATE_TEST_IMAGE_REF: &str = "quay.io/trusted-execution-clusters/fedora-coreos@sha256:372a5db90a8695fafc2869d438bacd7f0ef7fd84f63746a450bfcd4b8b64ae83"; + +pub const PRIMARY_PCR4_HASH: &str = + "ff2b357be4a4bc66be796d4e7b2f1f27077dc89b96220aae60b443bcf4672525"; +pub const SECONDARY_PCR4_HASH: &str = + "37517a1f76c4d5cf615f4690921c732ad31359aac55f3aaf66d65a8ed38655a9"; +pub const MIX_PRIMARY_BOOT_SECONDARY_KERNEL_PCR4_HASH: &str = + "0c4e52c0bc5d2fedbf83b2fee82664dbe5347a79cfb2cbcb9a37f64211add6e8"; +pub const MIX_SECONDARY_BOOT_PRIMARY_KERNEL_PCR4_HASH: &str = + "cc5a5360e64b25718be370ca2056645a9ba9e9bae33df08308d6b8e05b8ebb87"; +// PCR7 is not used for attestation yet. This constant is only consumed by +// unit tests. +pub const PCR7_HASH: &str = "b3a56a06c03a65277d0a787fcabc1e293eaa5d6dd79398f2dda741f7b874c65d"; +pub const PCR14_HASH: &str = "17cdefd9548f4383b67a37a901673bf3c8ded6f619d36c8007562de1d93c81cc"; + +pub const SEPARATOR_HASH: &str = "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"; +pub const EFICALL_HASH: &str = "3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba"; +pub const PRIMARY_SHIM_HASH: &str = + "94896c17d49fc8c8df0cc2836611586edab1615ce7cb58cf13fc5798de56b367"; +pub const PRIMARY_GRUB_HASH: &str = + "bc6844fc7b59b4f0c7da70a307fc578465411d7a2c34b0f4dc2cc154c873b644"; +pub const PRIMARY_KERNEL_HASH: &str = + "72c613f1b4d60dcf51f82f3458cca246580d23150130ec6751ac6fa62c867364"; +pub const SECONDARY_SHIM_HASH: &str = PRIMARY_SHIM_HASH; +pub const SECONDARY_GRUB_HASH: &str = + "f45c2c974192366a5391e077c3cbf91e735e86eba2037fd86a1f1501818f73f4"; +pub const SECONDARY_KERNEL_HASH: &str = + "f31e645e5e9ed131eea5dca0a18893a21e5625b4a56314fa39587ddc33a7fa91"; + +#[macro_export] +macro_rules! pcr4_ev_efi_action_event { + () => {{ + TPMEvent { + pcr: 4, + name: "EV_EFI_ACTION".to_string(), + hash: hex::decode(EFICALL_HASH).unwrap(), + id: TPMEventID::Pcr4EfiCall, + } + }}; +} + +#[macro_export] +macro_rules! pcr_separator_event { + ($pcr:expr, $event_id:expr) => {{ + TPMEvent { + pcr: $pcr, + name: "EV_SEPARATOR".to_string(), + hash: hex::decode(SEPARATOR_HASH).unwrap(), + id: $event_id, + } + }}; +} + +#[macro_export] +macro_rules! expected_pcr7 { + // PCR7 is not used for attestation yet. This constant is only consumed by + // unit tests. + () => {{ + Pcr { + id: 7, + value: hex::decode(PCR7_HASH).unwrap(), + events: vec![ + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), + hash: hex::decode( + "ccfc4bb32888a345bc8aeadaba552b627d99348c767681ab3141f5b01e40a40e", + ) + .unwrap(), + id: TPMEventID::Pcr7SecureBoot, + }, + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), + hash: hex::decode( + "adb6fc232943e39c374bf4782b6c697f43c39fca1f4b51dfceda21164e19a893", + ) + .unwrap(), + id: TPMEventID::Pcr7Pk, + }, + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), + hash: hex::decode( + "b5432fe20c624811cb0296391bfdf948ebd02f0705ab8229bea09774023f0ebf", + ) + .unwrap(), + id: TPMEventID::Pcr7Kek, + }, + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), + hash: hex::decode( + "4313e43de720194a0eabf4d6415d42b5a03a34fdc47bb1fc924cc4e665e6893d", + ) + .unwrap(), + id: TPMEventID::Pcr7Db, + }, + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), + hash: hex::decode( + "001004ba58a184f09be6c1f4ec75a246cc2eefa9637b48ee428b6aa9bce48c55", + ) + .unwrap(), + id: TPMEventID::Pcr7Dbx, + }, + pcr_separator_event!(7, TPMEventID::Pcr7Separator), + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_AUTHORITY".to_string(), + hash: hex::decode( + "4d4a8e2c74133bbdc01a16eaf2dbb5d575afeb36f5d8dfcf609ae043909e2ee9", + ) + .unwrap(), + id: TPMEventID::Pcr7ShimCert, + }, + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_AUTHORITY".to_string(), + hash: hex::decode( + "e8e9578f5951ef16b1c1aa18ef02944b8375ec45ed4b5d8cdb30428db4a31016", + ) + .unwrap(), + id: TPMEventID::Pcr7SbatLevel, + }, + TPMEvent { + pcr: 7, + name: "EV_EFI_VARIABLE_AUTHORITY".to_string(), + hash: hex::decode( + "ad5901fd581e6640c742c488083b9ac2c48255bd28a16c106c6f9df52702ee3f", + ) + .unwrap(), + id: TPMEventID::Pcr7GrubMokListCert, + }, + ], + } + }}; +} + +#[macro_export] +macro_rules! expected_pcr14 { + () => {{ + Pcr { + id: 14, + value: hex::decode(PCR14_HASH).unwrap(), + events: vec![ + TPMEvent { + pcr: 14, + name: "EV_IPL".to_string(), + hash: hex::decode( + "e8e48e3ad10bc243341b4663c0057aef0ec7894ccc9ecb0598f0830fa57f7220", + ) + .unwrap(), + id: TPMEventID::Pcr14MokList, + }, + TPMEvent { + pcr: 14, + name: "EV_IPL".to_string(), + hash: hex::decode( + "8d8a3aae50d5d25838c95c034aadce7b548c9a952eb7925e366eda537c59c3b0", + ) + .unwrap(), + id: TPMEventID::Pcr14MokListX, + }, + TPMEvent { + pcr: 14, + name: "EV_IPL".to_string(), + hash: hex::decode( + "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a", + ) + .unwrap(), + id: TPMEventID::Pcr14MokListTrusted, + }, + ], + } + }}; +} + +#[macro_export] +macro_rules! pcr4_shim_event { + ($hash:expr) => {{ + TPMEvent { + pcr: 4, + name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), + hash: hex::decode($hash).unwrap(), + id: TPMEventID::Pcr4Shim, + } + }}; +} + +#[macro_export] +macro_rules! pcr4_grub_event { + ($hash:expr) => {{ + TPMEvent { + pcr: 4, + name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), + hash: hex::decode($hash).unwrap(), + id: TPMEventID::Pcr4Grub, + } + }}; +} + +#[macro_export] +macro_rules! pcr4_kernel_event { + ($hash:expr) => {{ + TPMEvent { + pcr: 4, + name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), + hash: hex::decode($hash).unwrap(), + id: TPMEventID::Pcr4Vmlinuz, + } + }}; +} + +#[macro_export] +macro_rules! pcr4 { + ($value:expr, $shim:expr, $grub:expr, $kernel:expr) => {{ + Pcr { + id: 4, + value: hex::decode($value).unwrap(), + events: vec![ + pcr4_ev_efi_action_event!(), + pcr_separator_event!(4, TPMEventID::Pcr4Separator), + pcr4_shim_event!($shim), + pcr4_grub_event!($grub), + pcr4_kernel_event!($kernel), + ], + } + }}; +} + +#[macro_export] +macro_rules! primary_pcr4 { + () => {{ + pcr4!( + PRIMARY_PCR4_HASH, + PRIMARY_SHIM_HASH, + PRIMARY_GRUB_HASH, + PRIMARY_KERNEL_HASH + ) + }}; +} + +// TODO: This should also contain expected pcr7, as soon as it can be +// predicted and used for attestation +#[macro_export] +macro_rules! pcrs { + ($pcr4:expr) => {{ [$pcr4, expected_pcr14!()] }}; +} + +#[macro_export] +macro_rules! primary_pcrs { + () => {{ pcrs!(primary_pcr4!()) }}; +} + +#[macro_export] +macro_rules! secondary_pcrs { + () => {{ + pcrs!(pcr4!( + SECONDARY_PCR4_HASH, + SECONDARY_SHIM_HASH, + SECONDARY_GRUB_HASH, + SECONDARY_KERNEL_HASH + )) + }}; +} diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index a9899fd2..3bffd2da 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -5,6 +5,7 @@ use anyhow::{Context, Result, anyhow}; use fs_extra::dir; +use glob::glob; use k8s_openapi::api::apps::v1::{Deployment, DeploymentCondition, DeploymentStatus}; use k8s_openapi::api::core::v1::{ ConfigMap, LoadBalancerStatus, Namespace, Secret, Service, ServicePort, ServiceSpec, @@ -24,12 +25,14 @@ use trusted_cluster_operator_lib::certificates::{ }; use trusted_cluster_operator_lib::conditions::COMMITTED_CONDITION; use trusted_cluster_operator_lib::issuers::{Issuer, IssuerCa, IssuerSpec}; +use trusted_cluster_operator_lib::reference_values::ImagePcrs; use trusted_cluster_operator_lib::{ApprovedImage, ApprovedImageStatus, AttestationKey, Machine}; use trusted_cluster_operator_lib::{TrustedExecutionCluster, endpoints::*, images::*}; pub mod timer; pub use timer::Poller; +pub mod constants; pub mod mock_client; #[cfg(feature = "virtualization")] @@ -57,7 +60,6 @@ const ATT_REG_SECRET: &str = "att-reg-secret"; const REG_CERT: &str = "reg-srv-cert"; const TRUSTEE_CERT: &str = "trustee-cert"; const ATT_REG_CERT: &str = "att-reg-cert"; -pub const APPROVED_IMAGE_NAME: &str = "coreos"; pub fn compare_pcrs(actual: &[Pcr], expected: &[Pcr]) -> bool { if actual.len() != expected.len() { @@ -461,7 +463,11 @@ pub struct TestContext { } impl TestContext { - pub async fn new(test_name: &str, delayed_approved_image: bool) -> Result { + pub async fn new( + test_name: &str, + delayed_approved_image: bool, + approved_images: &[(&str, &str)], + ) -> Result { INIT.call_once(|| { let _ = env_logger::builder().is_test(true).try_init(); }); @@ -482,7 +488,7 @@ impl TestContext { ctx.manifests_dir = manifests_dir; ctx.create_namespace().await?; - ctx.apply_operator_manifests().await?; + ctx.apply_operator_manifests(approved_images).await?; test_info!( &ctx.test_name, @@ -731,7 +737,11 @@ impl TestContext { Ok(()) } - async fn generate_manifests(&self, workspace_root: &PathBuf) -> Result<(PathBuf, PathBuf)> { + async fn generate_manifests( + &self, + workspace_root: &PathBuf, + approved_images: &[(&str, &str)], + ) -> Result<(PathBuf, PathBuf)> { let ns = self.test_namespace.clone(); let controller_gen_pattern = workspace_root.join("bin/controller-gen-*"); let pattern = controller_gen_pattern.to_str().unwrap(); @@ -796,7 +806,15 @@ impl TestContext { args.extend(&["-trustee-image", &trustee_image]); args.extend(&["-register-server-image", ®_srv_img]); args.extend(&["-attestation-key-register-image", &att_reg_img]); - args.extend(&["-approved-image", &approved_image]); + let primary_approved_arg = format!("{},{approved_image}", constants::APPROVED_IMAGE_NAME); + args.extend(&["-approved-image", &primary_approved_arg]); + let approved_args: Vec = approved_images + .iter() + .map(|&(n, r)| format!("{n},{r}")) + .collect(); + for arg in &approved_args { + args.extend(&["-approved-image", arg]); + } let manifest_gen = Command::new(&trusted_cluster_gen_path).args(args).output(); let manifest_gen_output = manifest_gen.await?; if !manifest_gen_output.status.success() { @@ -806,11 +824,13 @@ impl TestContext { Ok((crd_temp_dir, rbac_temp_dir)) } - async fn apply_operator_manifests(&self) -> Result<()> { + async fn apply_operator_manifests(&self, approved_images: &[(&str, &str)]) -> Result<()> { let manifests_dir = &self.manifests_dir; test_info!(&self.test_name, "Generating manifests in {manifests_dir}"); let workspace_root = env::current_dir()?.join(".."); - let (crd_temp_dir, rbac_temp_dir) = self.generate_manifests(&workspace_root).await?; + let (crd_temp_dir, rbac_temp_dir) = self + .generate_manifests(&workspace_root, approved_images) + .await?; test_info!(&self.test_name, "Manifests generated successfully"); self.set_certificates().await?; @@ -955,13 +975,20 @@ impl TestContext { if self.delayed_approved_image { tokio::time::sleep(Duration::from_secs(5)).await; } - let approved_image_path = manifests_path.join("approved_image_cr.yaml"); - let approved_image_str = approved_image_path.to_str().unwrap(); - kube_apply!( - approved_image_str, - &self.test_name, - "Applying ApprovedImage manifest" - ); + let approved_image_paths = glob( + manifests_path + .join("approved_image_cr_*.yaml") + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid ApprovedImage manifest path"))?, + )?; + for approved_image_path in approved_image_paths.filter_map(Result::ok) { + let approved_image_str = approved_image_path.to_str().unwrap(); + kube_apply!( + approved_image_str, + &self.test_name, + "Applying ApprovedImage manifest" + ); + } let depl_ready = |depl: Option<&Deployment>| { let chk_cond = |c: &DeploymentCondition| c.type_ == "Available" && c.status == "True"; @@ -1026,7 +1053,10 @@ impl TestContext { let configmap_api: Api = Api::namespaced(self.client.clone(), ns); wait_for_resource_created(&configmap_api, "image-pcrs", scaled_timeout(60)).await?; - let info = format!("Waiting for ApprovedImage {APPROVED_IMAGE_NAME} to be Committed"); + let info = format!( + "Waiting for ApprovedImage {} to be Committed", + constants::APPROVED_IMAGE_NAME + ); test_info!(&self.test_name, "{info}"); let images: Api = Api::namespaced(self.client.clone(), ns); let image_ready = |img: Option<&ApprovedImage>| { @@ -1036,11 +1066,40 @@ impl TestContext { let chk = |img: &ApprovedImage| img.status.as_ref().and_then(chk_status); img.and_then(chk).unwrap_or(false) }; - let done = await_condition(images.clone(), APPROVED_IMAGE_NAME, image_ready); - let ctx = format!("waiting for ApprovedImage {APPROVED_IMAGE_NAME} to be Committed"); + let done = await_condition(images.clone(), constants::APPROVED_IMAGE_NAME, image_ready); + let ctx = format!( + "waiting for ApprovedImage {} to be Committed", + constants::APPROVED_IMAGE_NAME + ); timeout(scaled_duration(300), done).await.context(ctx)??; Ok(()) } + + pub async fn verify_expected_pcrs(&self, expected_pcrs: &[&[Pcr]]) -> anyhow::Result<()> { + let client = self.client(); + let namespace = self.namespace(); + + let configmap_api: Api = Api::namespaced(client.clone(), namespace); + let populated = |cm: Option<&ConfigMap>| { + let data = cm.and_then(|cm| cm.data.as_ref()); + let json = data.and_then(|data| data.get("image-pcrs.json")); + let pcrs = json.and_then(|json| serde_json::from_str::(json).ok()); + pcrs.map(|pcrs| { + pcrs.0.len() == expected_pcrs.len() + && pcrs.0.values().all(|image_data| { + expected_pcrs + .iter() + .any(|exp| compare_pcrs(&image_data.pcrs, exp)) + }) + }) + .unwrap_or(false) + }; + let done = await_condition(configmap_api.clone(), "image-pcrs", populated); + let ctx = "waiting for ConfigMap image-pcrs to be populated with expected PCR values"; + timeout(scaled_duration(180), done).await.context(ctx)??; + + Ok(()) + } } #[macro_export] @@ -1069,8 +1128,9 @@ macro_rules! virt_test { #[macro_export] macro_rules! setup { - () => {{ $crate::TestContext::new(TEST_NAME, false) }}; - (delayed_approved_image) => {{ $crate::TestContext::new(TEST_NAME, true) }}; + () => {{ $crate::TestContext::new(TEST_NAME, false, &[]) }}; + (delayed_approved_image) => {{ $crate::TestContext::new(TEST_NAME, true, &[]) }}; + ($images:expr) => {{ $crate::TestContext::new(TEST_NAME, false, &$images) }}; } async fn setup_test_client() -> Result { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 01d434c0..64dd9087 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -16,6 +16,7 @@ virtualization = [] anyhow.workspace = true cfg-if = "1.0.4" compute-pcrs-lib.workspace = true +hex.workspace = true k8s-openapi.workspace = true kube = { workspace = true } regex = "1" diff --git a/tests/trusted_execution_cluster.rs b/tests/trusted_execution_cluster.rs index fb36dcef..62584f58 100644 --- a/tests/trusted_execution_cluster.rs +++ b/tests/trusted_execution_cluster.rs @@ -4,7 +4,8 @@ // SPDX-License-Identifier: MIT use anyhow::Context; -use compute_pcrs_lib::{Part, Pcr}; +use compute_pcrs_lib::Pcr; +use compute_pcrs_lib::tpmevents::{TPMEvent, TPMEventID}; use k8s_openapi::api::apps::v1::Deployment; use k8s_openapi::api::core::v1::{ConfigMap, Secret}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, OwnerReference}; @@ -15,17 +16,12 @@ use std::time::Duration; use tokio::time::timeout; use trusted_cluster_operator_lib::conditions::NOT_COMMITTED_REASON_PENDING; use trusted_cluster_operator_lib::endpoints::{REGISTER_SERVER_DEPLOYMENT, TRUSTEE_DEPLOYMENT}; -use trusted_cluster_operator_lib::reference_values::ImagePcrs; use trusted_cluster_operator_lib::{ ApprovedImage, AttestationKey, Machine, TrustedExecutionCluster, generate_owner_reference, }; +use trusted_cluster_operator_test_utils::constants::*; use trusted_cluster_operator_test_utils::*; -const EXPECTED_PCR4: &str = "ff2b357be4a4bc66be796d4e7b2f1f27077dc89b96220aae60b443bcf4672525"; -const TEC_NAME: &str = "trusted-execution-cluster"; -const TRUSTEE_CONFIG_MAP: &str = "trustee-data"; -const RV_JSON_KEY: &str = "reference-values.json"; - fn ak_approved(ak: Option<&AttestationKey>) -> bool { let is_approved = |c: &Condition| c.type_ == "Approved" && c.status == "True"; let cs = ak.and_then(|ak| ak.status.as_ref().and_then(|s| s.conditions.as_ref())); @@ -127,88 +123,8 @@ named_test!( named_test! { async fn test_image_pcrs_configmap_updates() -> anyhow::Result<()> { let test_ctx = setup!().await?; - let client = test_ctx.client(); - let namespace = test_ctx.namespace(); - - let configmap_api: Api = Api::namespaced(client.clone(), namespace); - let populated = |cm: Option<&ConfigMap>| { - let data = cm.and_then(|cm| cm.data.as_ref()); - let json = data.and_then(|data| data.get("image-pcrs.json")); - let pcrs = json.and_then(|json| serde_json::from_str::(json).ok()); - pcrs.map(|pcrs| !pcrs.0.is_empty()).unwrap_or(false) - }; - let done = await_condition(configmap_api.clone(), "image-pcrs", populated); - let ctx = "waiting for ConfigMap image-pcrs to be populated"; - timeout(scaled_duration(180), done).await.context(ctx)??; - - let image_pcrs_cm = configmap_api.get("image-pcrs").await?; - assert_eq!(image_pcrs_cm.metadata.name.as_deref(), Some("image-pcrs")); - - let data = image_pcrs_cm.data.as_ref() - .expect("image-pcrs ConfigMap should have data field"); - - assert!(!data.is_empty(), "image-pcrs ConfigMap should have data"); - - let image_pcrs_json = data.get("image-pcrs.json") - .expect("image-pcrs ConfigMap should have image-pcrs.json key"); - - assert!(!image_pcrs_json.is_empty(), "image-pcrs.json should not be empty"); - - // Parse the image-pcrs.json using the ImagePcrs structure - let image_pcrs: ImagePcrs = serde_json::from_str(image_pcrs_json) - .expect("image-pcrs.json should be valid ImagePcrs JSON"); - - assert!(!image_pcrs.0.is_empty(), "image-pcrs.json should contain at least one image entry"); - - let expected_pcrs = vec![ - Pcr { - id: 4, - value: EXPECTED_PCR4.to_string(), - parts: vec![ - Part { name: "EV_EFI_ACTION".to_string(), hash: "3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba".to_string() }, - Part { name: "EV_SEPARATOR".to_string(), hash: "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119".to_string() }, - Part { name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), hash: "94896c17d49fc8c8df0cc2836611586edab1615ce7cb58cf13fc5798de56b367".to_string() }, - Part { name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), hash: "bc6844fc7b59b4f0c7da70a307fc578465411d7a2c34b0f4dc2cc154c873b644".to_string() }, - Part { name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), hash: "72c613f1b4d60dcf51f82f3458cca246580d23150130ec6751ac6fa62c867364".to_string() }, - ], - }, - Pcr { - id: 7, - value: "b3a56a06c03a65277d0a787fcabc1e293eaa5d6dd79398f2dda741f7b874c65d".to_string(), - parts: vec![ - Part { name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), hash: "ccfc4bb32888a345bc8aeadaba552b627d99348c767681ab3141f5b01e40a40e".to_string() }, - Part { name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), hash: "adb6fc232943e39c374bf4782b6c697f43c39fca1f4b51dfceda21164e19a893".to_string() }, - Part { name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), hash: "b5432fe20c624811cb0296391bfdf948ebd02f0705ab8229bea09774023f0ebf".to_string() }, - Part { name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), hash: "4313e43de720194a0eabf4d6415d42b5a03a34fdc47bb1fc924cc4e665e6893d".to_string() }, - Part { name: "EV_EFI_VARIABLE_DRIVER_CONFIG".to_string(), hash: "001004ba58a184f09be6c1f4ec75a246cc2eefa9637b48ee428b6aa9bce48c55".to_string() }, - Part { name: "EV_SEPARATOR".to_string(), hash: "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119".to_string() }, - Part { name: "EV_EFI_VARIABLE_AUTHORITY".to_string(), hash: "4d4a8e2c74133bbdc01a16eaf2dbb5d575afeb36f5d8dfcf609ae043909e2ee9".to_string() }, - Part { name: "EV_EFI_VARIABLE_AUTHORITY".to_string(), hash: "e8e9578f5951ef16b1c1aa18ef02944b8375ec45ed4b5d8cdb30428db4a31016".to_string() }, - Part { name: "EV_EFI_VARIABLE_AUTHORITY".to_string(), hash: "ad5901fd581e6640c742c488083b9ac2c48255bd28a16c106c6f9df52702ee3f".to_string() }, - ], - }, - Pcr { - id: 14, - value: "17cdefd9548f4383b67a37a901673bf3c8ded6f619d36c8007562de1d93c81cc".to_string(), - parts: vec![ - Part { name: "EV_IPL".to_string(), hash: "e8e48e3ad10bc243341b4663c0057aef0ec7894ccc9ecb0598f0830fa57f7220".to_string() }, - Part { name: "EV_IPL".to_string(), hash: "8d8a3aae50d5d25838c95c034aadce7b548c9a952eb7925e366eda537c59c3b0".to_string() }, - Part { name: "EV_IPL".to_string(), hash: "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a".to_string() }, - ], - }, - ]; - - let mut found_expected_pcrs = false; - for (_image_ref, image_data) in image_pcrs.0.iter() { - if compare_pcrs(&image_data.pcrs, &expected_pcrs) { - found_expected_pcrs = true; - break; - } - } - - assert!(found_expected_pcrs, - "At least one image should have the expected PCR values"); + test_ctx.verify_expected_pcrs(&[&primary_pcrs!()]).await?; test_ctx.cleanup().await?; Ok(()) @@ -228,7 +144,7 @@ async fn test_image_disallow() -> anyhow::Result<()> { let chk_removed = |cm: Option<&ConfigMap>| { let data = cm.and_then(|cm| cm.data.as_ref()); let json = data.and_then(|data| data.get(RV_JSON_KEY)); - json.map(|json| !json.contains(EXPECTED_PCR4)).unwrap_or(false) + json.map(|json| !json.contains(PRIMARY_PCR4_HASH)).unwrap_or(false) }; let rv_removed = await_condition(configmap_api, TRUSTEE_CONFIG_MAP, chk_removed); let ctx = format!("waiting for ConfigMap {TRUSTEE_CONFIG_MAP} to not contain PCR value"); @@ -425,7 +341,7 @@ async fn test_approved_image_readoption() -> anyhow::Result<()> { let chk_added = |cm: Option<&ConfigMap>| { let data = cm.and_then(|cm| cm.data.as_ref()); let json = data.and_then(|data| data.get(RV_JSON_KEY)); - json.map(|json| json.contains(EXPECTED_PCR4)).unwrap_or(false) + json.map(|json| json.contains(PRIMARY_PCR4_HASH)).unwrap_or(false) }; let rv_added = await_condition(configmaps, TRUSTEE_CONFIG_MAP, chk_added); let ctx = format!("waiting for ConfigMap {TRUSTEE_CONFIG_MAP} to contain PCR value"); @@ -436,3 +352,46 @@ async fn test_approved_image_readoption() -> anyhow::Result<()> { Ok(()) } } + +named_test! { +async fn test_combined_image_pcrs_configmap_updates() -> anyhow::Result<()> { + let test_ctx = setup!([(COMBINE_PCRS_UPDATE_TEST_IMAGE_NAME, COMBINE_PCRS_UPDATE_TEST_IMAGE_REF)]).await?; + let client = test_ctx.client(); + let namespace = test_ctx.namespace(); + + // In practical terms it emulates a grub + kernel upgrade + test_ctx.verify_expected_pcrs(&[&primary_pcrs!(), &secondary_pcrs!()]).await?; + + let expected_ref_values = [ + // PCR4 + PRIMARY_PCR4_HASH, + MIX_PRIMARY_BOOT_SECONDARY_KERNEL_PCR4_HASH, + MIX_SECONDARY_BOOT_PRIMARY_KERNEL_PCR4_HASH, + SECONDARY_PCR4_HASH, + // PCR14 + PCR14_HASH, + ]; + + let configmaps: Api = Api::namespaced(client.clone(), namespace); + let all_expected_pcrs = |cm: Option<&ConfigMap>| { + let data = cm.and_then(|cm| cm.data.as_ref()); + let rv_json = data.and_then(|data| data.get("reference-values.json")); + if let Some(reference_values) = rv_json { + for value in expected_ref_values { + if !reference_values.contains(value) { + return false; + } + } + } else { + return false; + } + true + }; + let done = await_condition(configmaps, "trustee-data", all_expected_pcrs); + let ctx = "waiting for ConfigMap trustee-data to contain all expected pcrs"; + timeout(scaled_duration(180), done).await.context(ctx)??; + + test_ctx.cleanup().await?; + Ok(()) +} +}