diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 50261314..cfc8b744 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,7 @@ concurrency: env: CARGO_TERM_COLOR: always # Pinned toolchain for linting - ACTIONS_LINTS_TOOLCHAIN: 1.85.0 + ACTIONS_LINTS_TOOLCHAIN: 1.88.0 jobs: linting: diff --git a/Cargo.lock b/Cargo.lock index 295482fd..e4d7df6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,16 +445,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#4c77196e852265dc2016e0e1cbd8ecf52b44f077" dependencies = [ "anyhow", "glob", "hex", "hex-literal", + "itertools 0.14.0", "lief", + "log", "openssl", "serde", + "serde_with", "sha2", + "strum", "uuid", ] @@ -677,6 +681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -881,7 +886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1652,6 +1657,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -1687,7 +1694,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1720,6 +1727,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" @@ -1818,7 +1834,7 @@ checksum = "06d9e5e61dd037cdc51da0d7e2b2be10f497478ea7e120d85dad632adb99882b" dependencies = [ "base64 0.22.1", "chrono", - "schemars", + "schemars 1.1.0", "serde", "serde_json", ] @@ -1884,7 +1900,7 @@ dependencies = [ "http 1.4.0", "json-patch", "k8s-openapi", - "schemars", + "schemars 1.1.0", "serde", "serde-value", "serde_json", @@ -1955,9 +1971,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "lief" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18773f648622afc222330700726227739936a1fa0040b91f007c80605525c4ff" +checksum = "763b6760e243b6b2083e3abb6f84916050114a944c88d5e649356c81f2699804" dependencies = [ "bitflags 2.10.0", "cxx", @@ -1970,9 +1986,9 @@ dependencies = [ [[package]] name = "lief-build" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3abce5340e56c7f9191d699302b9496445b0438033b48a9937b41c506f677731" +checksum = "282503de62c5a17299c7406f90716ce7fc7977b4c5c60a33e60f762432510851" dependencies = [ "git-version", "miette", @@ -1983,9 +1999,9 @@ dependencies = [ [[package]] name = "lief-ffi" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba2ccbe972b26212f69f7b938fc198214f4eaa7f548a8d724f1a04968137f96" +checksum = "d85732d1f35911b3d4c88ea8a2a8bfd82dd0fa9bd79679e01aa6f020e85aa899" dependencies = [ "autocxx", "cxx", @@ -2961,7 +2977,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3025,6 +3041,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" @@ -3210,6 +3238,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -3389,6 +3448,9 @@ name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -3515,7 +3577,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3586,10 +3648,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]] @@ -3598,6 +3662,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" @@ -3800,6 +3874,7 @@ dependencies = [ "clevis-pin-trustee-lib", "compute-pcrs-lib", "env_logger", + "glob", "http 1.4.0", "ignition-config", "k8s-openapi", @@ -3823,6 +3898,7 @@ version = "0.1.0" dependencies = [ "anyhow", "compute-pcrs-lib", + "hex", "k8s-openapi", "kube", "regex", diff --git a/Cargo.toml b/Cargo.toml index 87d9752a..7fd62eea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ resolver = "3" [workspace.package] edition = "2024" -rust-version = "1.85" +rust-version = "1.88" [workspace.dependencies] anyhow = "1.0.100" @@ -18,6 +18,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 = "0.11.8" http = "1.4.0" +hex = "0.4.3" ignition-config = "0.5.0" k8s-openapi = { version = "0.26.1", features = ["v1_33", "schemars"] } kube = { version = "2.0.1", default-features = false, features = ["derive", "runtime", "openssl-tls"] } diff --git a/api/trusted-cluster-gen.go b/api/trusted-cluster-gen.go index 2b590b51..4569f882 100644 --- a/api/trusted-cluster-gen.go +++ b/api/trusted-cluster-gen.go @@ -21,6 +21,17 @@ 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 Args struct { outputDir string image string @@ -29,7 +40,7 @@ type Args struct { pcrsComputeImage string registerServerImage string attestationKeyRegisterImage string - approvedImage string + approvedImages stringSlice } func main() { @@ -41,7 +52,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. Must be a bootable container image with SHA reference.") flag.Parse() log.SetFlags(log.LstdFlags) @@ -166,34 +177,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: fmt.Sprintf("coreos-%d", i), + Namespace: args.namespace, + }, + Spec: v1alpha1.ApprovedImageSpec{ + Reference: approvedImage, + }, + } - 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/attestation-key-register/src/main.rs b/attestation-key-register/src/main.rs index 165d686e..8a764973 100644 --- a/attestation-key-register/src/main.rs +++ b/attestation-key-register/src/main.rs @@ -40,7 +40,7 @@ async fn handle_registration( client: Client, addr: Option, ) -> Result { - info!("Received registration request: {:?}", registration); + info!("Received registration request: {registration:?}"); let api: Api = Api::default_namespaced(client); @@ -50,8 +50,7 @@ async fn handle_registration( if key.spec.public_key == registration.public_key { let existing_name = key.metadata.name.unwrap_or_default(); error!( - "Duplicate public key detected: already exists in AttestationKey '{}'", - existing_name + "Duplicate public key detected: already exists in AttestationKey '{existing_name}'" ); return Ok(reply::with_status( reply::json(&serde_json::json!({ @@ -64,11 +63,11 @@ async fn handle_registration( } } Err(e) => { - error!("Failed to list AttestationKeys: {}", e); + error!("Failed to list AttestationKeys: {e}"); return Ok(reply::with_status( reply::json(&serde_json::json!({ "status": "error", - "message": format!("Failed to check for existing keys: {}", e), + "message": format!("Failed to check for existing keys: {e}"), })), StatusCode::INTERNAL_SERVER_ERROR, )); @@ -106,11 +105,11 @@ async fn handle_registration( )) } Err(e) => { - error!("Failed to create AttestationKey: {}", e); + error!("Failed to create AttestationKey: {e}"); Ok(reply::with_status( reply::json(&serde_json::json!({ "status": "error", - "message": format!("Failed to create AttestationKey: {}", e), + "message": format!("Failed to create AttestationKey: {e}"), })), StatusCode::INTERNAL_SERVER_ERROR, )) @@ -145,7 +144,7 @@ async fn main() -> anyhow::Result<()> { .and_then(handle_registration); let addr = SocketAddr::from(([0, 0, 0, 0], args.port)); - info!("Listening on {}", addr); + info!("Listening on {addr}"); warp::serve(register).run(addr).await; diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 0db4174e..03a04272 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -16,7 +16,7 @@ trusted-cluster-operator-lib = { path = "../lib" } compute-pcrs-lib.workspace = true env_logger.workspace = true futures-util = "0.3.31" -hex = "0.4.3" +hex.workspace = true json-patch = "4.1.0" jsonptr = "0.7.1" k8s-openapi.workspace = true diff --git a/operator/src/attestation_key_register.rs b/operator/src/attestation_key_register.rs index 5f56fa54..653a104e 100644 --- a/operator/src/attestation_key_register.rs +++ b/operator/src/attestation_key_register.rs @@ -135,13 +135,13 @@ async fn ak_reconcile( client: Arc, ) -> Result { let ak_name = ak.metadata.name.clone().unwrap_or_default(); - info!("Attestation Key reconciliation for: {}", ak_name); + info!("Attestation Key reconciliation for: {ak_name}"); let client = Arc::unwrap_or_clone(client); let machines: Api = Api::default_namespaced(client.clone()); let lp = ListParams::default(); let machine_list: ObjectList = machines.list(&lp).await.map_err(|e| { - eprintln!("Error fetching machine list: {}", e); + eprintln!("Error fetching machine list: {e}"); ControllerError::Anyhow(e.into()) })?; for machine in &machine_list.items { @@ -182,7 +182,7 @@ async fn machine_reconcile( let aks: Api = Api::default_namespaced(client.clone()); let lp = ListParams::default(); let ak_list: ObjectList = aks.list(&lp).await.map_err(|e| { - eprintln!("Error fetching attestation key list: {}", e); + eprintln!("Error fetching attestation key list: {e}"); ControllerError::Anyhow(e.into()) })?; for ak in ak_list.items { @@ -315,10 +315,7 @@ async fn secret_reconcile( return Ok(Action::await_change()); } - info!( - "Secret reconciliation for AttestationKey secret: {}", - secret_name - ); + info!("Secret reconciliation for AttestationKey secret: {secret_name}"); let secrets: Api = Api::default_namespaced(Arc::unwrap_or_clone(client.clone())); finalizer(&secrets, ATTESTATION_KEY_SECRET_FINALIZER, secret, |ev| async move { @@ -330,15 +327,14 @@ async fn secret_reconcile( .await .map(|_| Action::await_change()) .map_err(|e| { - eprintln!("Error updating attestation key volumes on secret apply: {}", e); + eprintln!("Error updating attestation key volumes on secret apply: {e}"); finalizer::Error::::ApplyFailed(e.into()) }) } Event::Cleanup(secret) => { let secret_name = secret.metadata.name.clone().unwrap_or_default(); info!( - "AttestationKey secret {} is being deleted, updating trustee deployment volumes", - secret_name + "AttestationKey secret {secret_name} is being deleted, updating trustee deployment volumes" ); let client = Arc::unwrap_or_clone(client); // Update trustee deployment - secrets with deletion_timestamp will be filtered out @@ -347,8 +343,7 @@ async fn secret_reconcile( .map(|_| Action::await_change()) .map_err(|e| { eprintln!( - "Error updating attestation key volumes during secret deletion: {}", - e + "Error updating attestation key volumes during secret deletion: {e}" ); finalizer::Error::::CleanupFailed(e.into()) }) diff --git a/operator/src/test_utils.rs b/operator/src/test_utils.rs index 6d8e5717..c1aaf143 100644 --- a/operator/src/test_utils.rs +++ b/operator/src/test_utils.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT use compute_pcrs_lib::Pcr; +use compute_pcrs_lib::tpmevents::{TPMEvent, TPMEventID}; use k8s_openapi::{api::core::v1::ConfigMap, chrono::Utc}; use kube::Client; use operator::RvContextData; @@ -18,17 +19,39 @@ pub fn dummy_pcrs() -> ImagePcrs { first_seen: Utc::now(), pcrs: vec![ Pcr { - id: 0, - value: "pcr0_val".to_string(), - parts: vec![], + id: 4, + value: hex::decode( + "3f263b96ccbc33bb53d808771f9ab1e02d4dec8854f9530f749cde853a723273", + ) + .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( + "e58ada1ba75f2e4722b539824598ad5e10c55f2e4aeab2033f3b0a8ee3f3eca6", + ) + .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 74ad65d2..1bbc2f24 100644 --- a/operator/src/trustee.rs +++ b/operator/src/trustee.rs @@ -7,6 +7,8 @@ use anyhow::{Context, Result}; use base64::{Engine as _, engine::general_purpose}; 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, @@ -70,14 +72,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) { + 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())); + .push(JsonString(hex::encode(pcr.value.clone()))); } reference_values_in .iter() @@ -254,7 +262,7 @@ pub async fn update_attestation_keys(client: Client) -> Result<()> { name: secret_name.to_string(), items: Some(vec![KeyToPath { key: "public_key".to_string(), - path: format!("{}.pub", secret_name), + path: format!("{secret_name}.pub"), ..Default::default() }]), ..Default::default() @@ -543,16 +551,34 @@ 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 kube::client::Body; use trusted_cluster_operator_test_utils::mock_client::*; + 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.clone()), + "3f263b96ccbc33bb53d808771f9ab1e02d4dec8854f9530f749cde853a723273" + ); + assert_eq!( + hex::encode(image_pcrs.0["cos"].pcrs[1].value.clone()), + "e58ada1ba75f2e4722b539824598ad5e10c55f2e4aeab2033f3b0a8ee3f3eca6" + ); } #[test] @@ -586,10 +612,16 @@ 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!["3f263b96ccbc33bb53d808771f9ab1e02d4dec8854f9530f749cde853a723273",] + ); + let vals = reference_values_from(&result, "tpm_pcr7"); + assert_eq!( + vals, + vec!["e58ada1ba75f2e4722b539824598ad5e10c55f2e4aeab2033f3b0a8ee3f3eca6",] + ); } #[tokio::test] @@ -850,4 +882,119 @@ mod tests { let clos = |client| generate_kbs_deployment(client, Default::default(), "image"); test_create_error(clos).await; } + + #[test] + fn test_recompute_reference_values_pcr4() { + let image_pcrs = ImagePcrs(BTreeMap::from([ + ( + "cos1".to_string(), + ImagePcr { + first_seen: Utc::now(), + pcrs: vec![Pcr { + id: 4, + value: hex::decode("852718920421131081032051205110114719423559841238794129122376912159784392212168").unwrap(), + events: vec![ + TPMEvent { + name: "EV_EFI_ACTION".into(), + pcr: 4, + hash: hex::decode("3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba") + .unwrap(), + id: TPMEventID::Pcr4EfiCall, + }, + TPMEvent { + name: "EV_SEPARATOR".into(), + pcr: 4, + hash: hex::decode("df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119") + .unwrap(), + id: TPMEventID::Pcr4Separator, + }, + TPMEvent { + name: "EV_EFI_BOOT_SERVICES_APPLICATION".into(), + pcr: 4, + hash: hex::decode("94896c17d49fc8c8df0cc2836611586edab1615ce7cb58cf13fc5798de56b367") + .unwrap(), + id: TPMEventID::Pcr4Shim, + }, + TPMEvent { + name: "EV_EFI_BOOT_SERVICES_APPLICATION".into(), + pcr: 4, + hash: hex::decode("bc6844fc7b59b4f0c7da70a307fc578465411d7a2c34b0f4dc2cc154c873b644") + .unwrap(), + id: TPMEventID::Pcr4Grub, + }, + TPMEvent { + name: "EV_EFI_BOOT_SERVICES_APPLICATION".into(), + pcr: 4, + hash: hex::decode("2b1dc59bc61dbbc3db11a6f3b0708c948efd46cceb7f6c8ea2024b8d1b8c829a") + .unwrap(), + id: TPMEventID::Pcr4Vmlinuz, + }, + ], + }], + reference: "".to_string(), + }, + ), + ( + "cos2".to_string(), + ImagePcr { + first_seen: Utc::now(), + pcrs: vec![Pcr { + id: 4, + value: hex::decode("19925299236966772216371371471692276818611442625320115173412649113251558526237189").unwrap(), + events: vec![ + TPMEvent { + name: "EV_EFI_ACTION".into(), + pcr: 4, + hash: hex::decode("3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba") + .unwrap(), + id: TPMEventID::Pcr4EfiCall, + }, + TPMEvent { + name: "EV_SEPARATOR".into(), + pcr: 4, + hash: hex::decode("df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119") + .unwrap(), + id: 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, + }, + ], + }], + reference: "".to_string(), + }, + ), + ])); + + let result = recompute_reference_values(image_pcrs); + assert_eq!(result.len(), 2); + let vals = reference_values_from(&result, "tpm_pcr4"); + assert_eq!( + vals, + vec![ + "551bbd142a716c67cd78336593c2eb3b547b575e810ced4501d761082b5cd4a8", + "c9c3add791efc98f59977c89e673a34ad0b357872e9eb2c43d14607488e5d9e2", + "47b742b3a2244cc7249ff3221ec640198044aac533a95abafded7921237508c1", + "c7fc63ec604348d8258993a9e344ba72041afd1473ad291a3171199b551aedbd", + ] + ); + } } diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml index 8aaf4164..183816c7 100644 --- a/test_utils/Cargo.toml +++ b/test_utils/Cargo.toml @@ -31,3 +31,4 @@ tokio = { workspace = true, features = ["process"] } tower = { version = "0.5.2", features = ["full"] } uuid.workspace = true which = "8.0" +glob = "0.3.3" diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 1c62c4bc..d5ba84a5 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: MIT +use glob::glob; use k8s_openapi::api::apps::v1::Deployment; use k8s_openapi::api::core::v1::{ConfigMap, Namespace}; use kube::api::DeleteParams; @@ -11,6 +12,7 @@ use std::path::Path; use std::sync::Once; use std::time::Duration; use tokio::process::Command; +use trusted_cluster_operator_lib::reference_values::ImagePcrs; pub mod timer; pub use timer::Poller; @@ -21,6 +23,8 @@ pub mod virt; use compute_pcrs_lib::Pcr; +pub const DEFAULT_TEST_FCOS_IMAGE: &str = "quay.io/fedora/fedora-coreos@sha256:8f11c87187dfe83145001e9571948f9ab466e9f4a8b1e092a4798e5db1030dc3"; + pub fn compare_pcrs(actual: &[Pcr], expected: &[Pcr]) -> bool { if actual.len() != expected.len() { return false; @@ -82,7 +86,7 @@ pub struct TestContext { } impl TestContext { - pub async fn new(test_name: &str) -> anyhow::Result { + pub async fn new(test_name: &str, approved_images: &[&str]) -> anyhow::Result { INIT.call_once(|| { let _ = env_logger::builder().is_test(true).try_init(); }); @@ -102,7 +106,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, @@ -242,7 +246,7 @@ impl TestContext { .await } - async fn apply_operator_manifests(&self) -> anyhow::Result<()> { + async fn apply_operator_manifests(&self, approved_images: &[&str]) -> anyhow::Result<()> { test_info!( &self.test_name, "Generating manifests in {}", @@ -301,25 +305,31 @@ impl TestContext { )); } + let mut trusted_cluster_gen_args = vec![ + "-namespace", + &ns, + "-output-dir", + &self.manifests_dir, + "-image", + "localhost:5000/trusted-execution-clusters/trusted-cluster-operator:latest", + "-pcrs-compute-image", + "localhost:5000/trusted-execution-clusters/compute-pcrs:latest", + "-trustee-image", + "quay.io/trusted-execution-clusters/key-broker-service:20260106", + "-register-server-image", + "localhost:5000/trusted-execution-clusters/registration-server:latest", + "-attestation-key-register-image", + "localhost:5000/trusted-execution-clusters/attestation-key-register:latest", + ]; + + trusted_cluster_gen_args.extend( + approved_images + .iter() + .flat_map(|&i| vec!["-approved-image", i]), + ); + let manifest_gen_output = Command::new(&trusted_cluster_gen_path) - .args([ - "-namespace", - &ns, - "-output-dir", - &self.manifests_dir, - "-image", - "localhost:5000/trusted-execution-clusters/trusted-cluster-operator:latest", - "-pcrs-compute-image", - "localhost:5000/trusted-execution-clusters/compute-pcrs:latest", - "-trustee-image", - "quay.io/trusted-execution-clusters/key-broker-service:20260106", - "-register-server-image", - "localhost:5000/trusted-execution-clusters/registration-server:latest", - "-attestation-key-register-image", - "localhost:5000/trusted-execution-clusters/attestation-key-register:latest", - "-approved-image", - "quay.io/fedora/fedora-coreos@sha256:8f11c87187dfe83145001e9571948f9ab466e9f4a8b1e092a4798e5db1030dc3" - ]) + .args(trusted_cluster_gen_args) .output() .await?; @@ -357,14 +367,14 @@ impl TestContext { let sa_src = workspace_root.join("config/rbac/base/service_account.yaml"); let sa_content = std::fs::read_to_string(&sa_src)? - .replace("namespace: system", &format!("namespace: {}", ns)); + .replace("namespace: system", &format!("namespace: {ns}")); let sa_dst = rbac_temp_dir.join("service_account.yaml"); std::fs::write(&sa_dst, sa_content)?; let role_path = rbac_temp_dir.join("role.yaml"); let role_content = std::fs::read_to_string(&role_path)?.replace( "name: trusted-cluster-operator-role", - &format!("name: {}-trusted-cluster-operator-role", ns), + &format!("name: {ns}-trusted-cluster-operator-role"), ); std::fs::write(&role_path, role_content)?; @@ -372,25 +382,25 @@ impl TestContext { let rb_content = std::fs::read_to_string(&rb_src)? .replace( "name: manager-rolebinding", - &format!("name: {}-manager-rolebinding", ns), + &format!("name: {ns}-manager-rolebinding"), ) .replace( "name: trusted-cluster-operator-role", - &format!("name: {}-trusted-cluster-operator-role", ns), + &format!("name: {ns}-trusted-cluster-operator-role"), ) - .replace("namespace: system", &format!("namespace: {}", ns)); + .replace("namespace: system", &format!("namespace: {ns}")); let rb_dst = rbac_temp_dir.join("role_binding.yaml"); std::fs::write(&rb_dst, rb_content)?; let le_role_src = workspace_root.join("config/rbac/base/leader_election_role.yaml"); let le_role_content = std::fs::read_to_string(&le_role_src)? - .replace("namespace: system", &format!("namespace: {}", ns)); + .replace("namespace: system", &format!("namespace: {ns}")); let le_role_dst = rbac_temp_dir.join("leader_election_role.yaml"); std::fs::write(&le_role_dst, le_role_content)?; let le_rb_src = workspace_root.join("config/rbac/base/leader_election_role_binding.yaml"); let le_rb_content = std::fs::read_to_string(&le_rb_src)? - .replace("namespace: system", &format!("namespace: {}", ns)); + .replace("namespace: system", &format!("namespace: {ns}")); let le_rb_dst = rbac_temp_dir.join("leader_election_role_binding.yaml"); std::fs::write(&le_rb_dst, le_rb_content)?; @@ -399,7 +409,7 @@ impl TestContext { r#"# SPDX-FileCopyrightText: Generated for testing # SPDX-License-Identifier: CC0-1.0 -namespace: {} +namespace: {ns} resources: - service_account.yaml @@ -407,8 +417,7 @@ resources: - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml -"#, - ns +"# ); let temp_kustomization_path = rbac_temp_dir.join("kustomization.yaml"); @@ -436,7 +445,7 @@ resources: &self.test_name, "Updating CR manifest with publicTrusteeAddr" ); - let trustee_addr = format!("kbs-service.{}.svc.cluster.local:8080", ns); + let trustee_addr = format!("kbs-service.{ns}.svc.cluster.local:8080"); let cr_manifest_path = manifests_path.join("trusted_execution_cluster_cr.yaml"); let cr_content = std::fs::read_to_string(&cr_manifest_path)?; @@ -465,15 +474,22 @@ resources: .ok_or_else(|| anyhow::anyhow!("Invalid CR manifest path"))?; kube_apply!(cr_manifest_str, &self.test_name, "Applying CR manifest"); - let approved_image_path = manifests_path.join("approved_image_cr.yaml"); - let approved_image_str = approved_image_path - .to_str() - .ok_or_else(|| anyhow::anyhow!("Invalid ApprovedImage manifest path"))?; - 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() + .ok_or_else(|| anyhow::anyhow!("Invalid ApprovedImage manifest path"))?; + kube_apply!( + approved_image_str, + &self.test_name, + "Applying ApprovedImage manifest" + ); + } let deployments_api: Api = Api::namespaced(self.client.clone(), &ns); @@ -494,8 +510,7 @@ resources: .with_timeout(Duration::from_secs(60)) .with_interval(Duration::from_secs(5)) .with_error_message(format!( - "image-pcrs ConfigMap in the namespace {} not found", - ns + "image-pcrs ConfigMap in the namespace {ns} not found" )); let test_name_owned = self.test_name.clone(); @@ -515,6 +530,99 @@ resources: 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 poller = Poller::new() + .with_timeout(Duration::from_secs(180)) + .with_interval(Duration::from_secs(5)) + .with_error_message("image-pcrs ConfigMap not populated with data".to_string()); + + poller + .poll_async(|| { + let api = configmap_api.clone(); + async move { + let cm = api.get("image-pcrs").await?; + + if let Some(data) = &cm.data { + if let Some(image_pcrs_json) = data.get("image-pcrs.json") { + if let Ok(image_pcrs) = + serde_json::from_str::(image_pcrs_json) + { + if image_pcrs.0.len() == expected_pcrs.len() { + return Ok(()); + } + } + } + } + + Err(anyhow::anyhow!( + "image-pcrs ConfigMap not yet populated with image-pcrs.json data" + )) + } + }) + .await?; + + 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" + ); + + test_info!( + &self.test_name, + "Checking into {} image results:", + image_pcrs.0.len() + ); + let mut found_expected_pcrs = false; + + assert_eq!( + image_pcrs.0.len(), + expected_pcrs.len(), + "image-pcrs.json should contain {} image entries", + expected_pcrs.len() + ); + + for (i, (_image_ref, image_data)) in image_pcrs.0.iter().enumerate() { + if compare_pcrs(&image_data.pcrs, expected_pcrs[i]) { + found_expected_pcrs = true; + break; + } + } + + assert!( + found_expected_pcrs, + "At least one image should have the expected PCR values" + ); + + Ok(()) + } } #[macro_export] @@ -543,7 +651,9 @@ macro_rules! virt_test { #[macro_export] macro_rules! setup { - () => {{ $crate::TestContext::new(TEST_NAME) }}; + () => {{ $crate::TestContext::new(TEST_NAME, &[DEFAULT_TEST_FCOS_IMAGE]) }}; + + ($images:expr) => {{ $crate::TestContext::new(TEST_NAME, &$images) }}; } async fn setup_test_client() -> anyhow::Result { diff --git a/test_utils/src/virt.rs b/test_utils/src/virt.rs index 5a76574d..221fc49c 100644 --- a/test_utils/src/virt.rs +++ b/test_utils/src/virt.rs @@ -49,10 +49,7 @@ pub fn generate_ssh_key_pair() -> anyhow::Result<(String, String, std::path::Pat let stderr = String::from_utf8_lossy(&ssh_add_output.stderr); // Clean up the key file if ssh-add fails let _ = fs::remove_file(&key_path); - return Err(anyhow::anyhow!( - "Failed to add SSH key to agent: {}", - stderr - )); + return Err(anyhow::anyhow!("Failed to add SSH key to agent: {stderr}")); } Ok((private_key_str, public_key_str, key_path)) @@ -138,10 +135,8 @@ pub fn generate_ignition_config( serde_json::to_value(&config).expect("Failed to serialize ignition config"); // Add attestation key registration field - let attestation_url = format!( - "http://attestation-key-register.{}.svc.cluster.local:8001/register-ak", - namespace - ); + let attestation_url = + format!("http://attestation-key-register.{namespace}.svc.cluster.local:8001/register-ak"); if let Some(obj) = ignition_json.as_object_mut() { obj.insert( @@ -268,8 +263,7 @@ pub async fn wait_for_vm_running( .with_timeout(Duration::from_secs(timeout_secs)) .with_interval(Duration::from_secs(5)) .with_error_message(format!( - "VirtualMachine {} did not reach Running phase after {} seconds", - vm_name, timeout_secs + "VirtualMachine {vm_name} did not reach Running phase after {timeout_secs} seconds" )); poller @@ -289,8 +283,7 @@ pub async fn wait_for_vm_running( } Err(anyhow::anyhow!( - "VirtualMachine {} is not in Running phase yet", - name + "VirtualMachine {name} is not in Running phase yet" )) } }) @@ -309,7 +302,7 @@ pub async fn virtctl_ssh_exec( )); } - let _vm_target = format!("core@vmi/{}/{}", vm_name, namespace); + let _vm_target = format!("core@vmi/{vm_name}/{namespace}"); let full_cmd = format!( "virtctl ssh -i {} core@vmi/{}/{} -t '-o IdentitiesOnly=yes' -t '-o StrictHostKeyChecking=no' --known-hosts /dev/null -c '{}'", key_path.display(), @@ -321,7 +314,7 @@ pub async fn virtctl_ssh_exec( let output = Command::new("sh").arg("-c").arg(full_cmd).output().await?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow::anyhow!("virtctl ssh command failed: {}", stderr)); + return Err(anyhow::anyhow!("virtctl ssh command failed: {stderr}")); } Ok(String::from_utf8_lossy(&output.stdout).to_string()) @@ -357,8 +350,7 @@ async fn wait_for_vm_ssh( .with_timeout(Duration::from_secs(timeout_secs)) .with_interval(Duration::from_secs(10)) .with_error_message(format!( - "SSH access to VM {}/{} did not become {}available after {} seconds", - namespace, vm_name, avail_prefix, timeout_secs + "SSH access to VM {namespace}/{vm_name} did not become {avail_prefix}available after {timeout_secs} seconds", )); poller diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 8d6bd795..251918c4 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -17,6 +17,7 @@ anyhow.workspace = true trusted-cluster-operator-lib = { path = "../lib" } trusted-cluster-operator-test-utils = { path = "../test_utils" } compute-pcrs-lib.workspace = true +hex.workspace = true k8s-openapi.workspace = true kube = { workspace = true } regex = "1" diff --git a/tests/attestation.rs b/tests/attestation.rs index fecf6aef..94899dae 100644 --- a/tests/attestation.rs +++ b/tests/attestation.rs @@ -46,17 +46,15 @@ impl SingleAttestationContext { let (_private_key, public_key, key_path) = virt::generate_ssh_key_pair()?; test_ctx.info(format!( - "Generated SSH key pair and added to ssh-agent: {:?}", - key_path + "Generated SSH key pair and added to ssh-agent: {key_path:?}" )); let register_server_url = format!( - "http://register-server.{}.svc.cluster.local:8000/ignition-clevis-pin-trustee", - namespace + "http://register-server.{namespace}.svc.cluster.local:8000/ignition-clevis-pin-trustee" ); let image = "quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:2026-14-01"; - test_ctx.info(format!("Creating VM: {}", vm_name)); + test_ctx.info(format!("Creating VM: {vm_name}")); virt::create_kubevirt_vm( client, namespace, @@ -67,11 +65,11 @@ impl SingleAttestationContext { ) .await?; - test_ctx.info(format!("Waiting for VM {} to reach Running state", vm_name)); + test_ctx.info(format!("Waiting for VM {vm_name} to reach Running state")); virt::wait_for_vm_running(client, namespace, vm_name, 300).await?; - test_ctx.info(format!("VM {} is Running", vm_name)); + test_ctx.info(format!("VM {vm_name} is Running")); - test_ctx.info(format!("Waiting for SSH access to VM {}", vm_name)); + test_ctx.info(format!("Waiting for SSH access to VM {vm_name}")); virt::wait_for_vm_ssh_ready(namespace, vm_name, &key_path, 600).await?; test_ctx.info("SSH access is ready"); @@ -124,8 +122,7 @@ async fn test_parallel_vm_attestation() -> anyhow::Result<()> { test_ctx.info("Generated SSH key pairs for both VMs"); let register_server_url = format!( - "http://register-server.{}.svc.cluster.local:8000/ignition-clevis-pin-trustee", - namespace + "http://register-server.{namespace}.svc.cluster.local:8000/ignition-clevis-pin-trustee" ); let image = "quay.io/trusted-execution-clusters/fedora-coreos-kubevirt:2026-14-01"; @@ -233,7 +230,7 @@ async fn test_vm_reboot_attestation() -> anyhow::Result<()> { // Perform multiple reboots let num_reboots = 3; for i in 1..=num_reboots { - test_ctx.info(format!("Performing reboot {} of {}", i, num_reboots)); + test_ctx.info(format!("Performing reboot {i} of {num_reboots}")); // Reboot the VM via SSH let _reboot_result = virt::virtctl_ssh_exec( @@ -244,27 +241,25 @@ async fn test_vm_reboot_attestation() -> anyhow::Result<()> { ) .await; - test_ctx.info(format!("Waiting for lack of SSH access after reboot {}", i)); + test_ctx.info(format!("Waiting for lack of SSH access after reboot {i}")); virt::wait_for_vm_ssh_unavail(namespace, vm_name, &att_ctx.key_path, 30).await?; - test_ctx.info(format!("Waiting for SSH access after reboot {}", i)); + test_ctx.info(format!("Waiting for SSH access after reboot {i}")); virt::wait_for_vm_ssh_ready(namespace, vm_name, &att_ctx.key_path, 300).await?; // Verify encrypted root is still present after reboot - test_ctx.info(format!("Verifying encrypted root after reboot {}", i)); + test_ctx.info(format!("Verifying encrypted root after reboot {i}")); let has_encrypted_root = virt::verify_encrypted_root(namespace, vm_name, &att_ctx.key_path, &att_ctx.root_key).await?; assert!( has_encrypted_root, - "VM should have encrypted root device after reboot {}", - i + "VM should have encrypted root device after reboot {i}" ); - test_ctx.info(format!("Reboot {}: attestation successful", i)); + test_ctx.info(format!("Reboot {i}: attestation successful")); } test_ctx.info(format!( - "VM successfully rebooted {} times with encrypted root device maintained", - num_reboots + "VM successfully rebooted {num_reboots} times with encrypted root device maintained" )); test_ctx.cleanup().await?; diff --git a/tests/trusted_execution_cluster.rs b/tests/trusted_execution_cluster.rs index 6a60cd49..ee9fc710 100644 --- a/tests/trusted_execution_cluster.rs +++ b/tests/trusted_execution_cluster.rs @@ -2,17 +2,201 @@ // // SPDX-License-Identifier: MIT -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; use kube::{Api, api::DeleteParams}; use std::time::Duration; -use trusted_cluster_operator_lib::reference_values::ImagePcrs; use trusted_cluster_operator_lib::{ApprovedImage, TrustedExecutionCluster}; use trusted_cluster_operator_test_utils::*; const EXPECTED_PCR4: &str = "ff2b357be4a4bc66be796d4e7b2f1f27077dc89b96220aae60b443bcf4672525"; +macro_rules! expected_base_pcrs { + () => {{ + [ + Pcr { + id: 4, + value: hex::decode(EXPECTED_PCR4).unwrap(), + events: vec![ + TPMEvent { + pcr: 4, + name: "EV_EFI_ACTION".to_string(), + hash: hex::decode( + "3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba", + ) + .unwrap(), + id: TPMEventID::Pcr4EfiCall, + }, + TPMEvent { + pcr: 4, + name: "EV_SEPARATOR".to_string(), + hash: hex::decode( + "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119", + ) + .unwrap(), + id: TPMEventID::Pcr4Separator, + }, + TPMEvent { + pcr: 4, + name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), + hash: hex::decode( + "94896c17d49fc8c8df0cc2836611586edab1615ce7cb58cf13fc5798de56b367", + ) + .unwrap(), + id: TPMEventID::Pcr4Shim, + }, + TPMEvent { + pcr: 4, + name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), + hash: hex::decode( + "bc6844fc7b59b4f0c7da70a307fc578465411d7a2c34b0f4dc2cc154c873b644", + ) + .unwrap(), + id: TPMEventID::Pcr4Grub, + }, + TPMEvent { + pcr: 4, + name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), + hash: hex::decode( + "2b1dc59bc61dbbc3db11a6f3b0708c948efd46cceb7f6c8ea2024b8d1b8c829a", + ) + .unwrap(), + id: TPMEventID::Pcr4Vmlinuz, + }, + ], + }, + Pcr { + id: 7, + value: hex::decode( + "b3a56a06c03a65277d0a787fcabc1e293eaa5d6dd79398f2dda741f7b874c65d", + ) + .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, + }, + TPMEvent { + pcr: 7, + name: "EV_SEPARATOR".to_string(), + hash: hex::decode( + "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119", + ) + .unwrap(), + id: 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, + }, + ], + }, + Pcr { + id: 14, + value: hex::decode( + "17cdefd9548f4383b67a37a901673bf3c8ded6f619d36c8007562de1d93c81cc", + ) + .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, + }, + ], + }, + ] + }}; +} + named_test!( async fn test_trusted_execution_cluster_uninstall() -> anyhow::Result<()> { let test_ctx = setup!().await?; @@ -44,140 +228,135 @@ named_test!( named_test! { async fn test_image_pcrs_configmap_updates() -> anyhow::Result<()> { let test_ctx = setup!().await?; + + test_ctx.verify_expected_pcrs(&[&expected_base_pcrs!()]).await?; + + test_ctx.cleanup().await?; + + Ok(()) +} +} + +named_test! { +async fn test_image_disallow() -> 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 images: Api = Api::namespaced(client.clone(), namespace); + images.delete("coreos-0", &DeleteParams::default()).await?; + let configmap_api: Api = Api::namespaced(client.clone(), namespace); let poller = Poller::new() .with_timeout(Duration::from_secs(180)) .with_interval(Duration::from_secs(5)) - .with_error_message("image-pcrs ConfigMap not populated with data".to_string()); - - poller - .poll_async(|| { - let api = configmap_api.clone(); - async move { - let cm = api.get("image-pcrs").await?; - - if let Some(data) = &cm.data { - if let Some(image_pcrs_json) = data.get("image-pcrs.json") { - if let Ok(image_pcrs) = serde_json::from_str::(image_pcrs_json) { - if !image_pcrs.0.is_empty() { - return Ok(()); - } - } + .with_error_message("Reference value not removed".to_string()); + poller.poll_async(|| { + let api = configmap_api.clone(); + async move { + let cm = api.get("trustee-data").await?; + if let Some(data) = &cm.data { + if let Some(reference_values_json) = data.get("reference-values.json") { + if !reference_values_json.contains(EXPECTED_PCR4) { + return Ok(()); } } - - Err(anyhow::anyhow!("image-pcrs ConfigMap not yet populated with image-pcrs.json data")) } - }) - .await?; - - 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; + Err(anyhow::anyhow!("Reference value not yet removed")) } - } - - assert!(found_expected_pcrs, - "At least one image should have the expected PCR values"); - - test_ctx.cleanup().await?; + }).await?; Ok(()) } } named_test! { -async fn test_image_disallow() -> anyhow::Result<()> { - let test_ctx = setup!().await?; +async fn test_combined_image_pcrs_configmap_updates() -> anyhow::Result<()> { + let test_ctx = setup!([ + DEFAULT_TEST_FCOS_IMAGE, + "quay.io/trusted-execution-clusters/fedora-coreos@sha256:372a5db90a8695fafc2869d438bacd7f0ef7fd84f63746a450bfcd4b8b64ae83", + ]).await?; let client = test_ctx.client(); let namespace = test_ctx.namespace(); - let images: Api = Api::namespaced(client.clone(), namespace); - images.delete("coreos", &DeleteParams::default()).await?; + test_ctx.verify_expected_pcrs( + &[&expected_base_pcrs!(), + // In practical terms it emulates a grub + kernel upgrade + &[ + Pcr { + id: 4, + value: hex::decode("37517a1f76c4d5cf615f4690921c732ad31359aac55f3aaf66d65a8ed38655a9").unwrap(), + events: vec![ + TPMEvent { pcr: 4, name: "EV_EFI_ACTION".to_string(), hash: hex::decode("3d6772b4f84ed47595d72a2c4c5ffd15f5bb72c7507fe26f2aaee2c69d5633ba").unwrap(), id: TPMEventID::Pcr4EfiCall }, + TPMEvent { pcr: 4, name: "EV_SEPARATOR".to_string(), hash: hex::decode("df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119").unwrap(), id: TPMEventID::Pcr4Separator }, + TPMEvent { pcr: 4, name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), hash: hex::decode("94896c17d49fc8c8df0cc2836611586edab1615ce7cb58cf13fc5798de56b367").unwrap(), id: TPMEventID::Pcr4Shim }, + TPMEvent { pcr: 4, name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), hash: hex::decode("f45c2c974192366a5391e077c3cbf91e735e86eba2037fd86a1f1501818f73f4").unwrap(), id: TPMEventID::Pcr4Grub }, + TPMEvent { pcr: 4, name: "EV_EFI_BOOT_SERVICES_APPLICATION".to_string(), hash: hex::decode("f31e645e5e9ed131eea5dca0a18893a21e5625b4a56314fa39587ddc33a7fa91").unwrap(), id: TPMEventID::Pcr4Vmlinuz }, + ], + }, + Pcr { + id: 7, + value: hex::decode("b3a56a06c03a65277d0a787fcabc1e293eaa5d6dd79398f2dda741f7b874c65d").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 }, + TPMEvent { pcr: 7, name: "EV_SEPARATOR".to_string(), hash: hex::decode("df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119").unwrap(), id: 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 }, + ], + }, + Pcr { + id: 14, + value: hex::decode("17cdefd9548f4383b67a37a901673bf3c8ded6f619d36c8007562de1d93c81cc").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 }, + ], + }, + ]] + ).await?; + + let expected_ref_values = [ + // PCR4 + "ff2b357be4a4bc66be796d4e7b2f1f27077dc89b96220aae60b443bcf4672525", + "0c4e52c0bc5d2fedbf83b2fee82664dbe5347a79cfb2cbcb9a37f64211add6e8", + "cc5a5360e64b25718be370ca2056645a9ba9e9bae33df08308d6b8e05b8ebb87", + "37517a1f76c4d5cf615f4690921c732ad31359aac55f3aaf66d65a8ed38655a9", + // PCR7 + "b3a56a06c03a65277d0a787fcabc1e293eaa5d6dd79398f2dda741f7b874c65d", + // PCR14 + "cdff7d9348467f3bc23fe4b299873255985a3a5c9acf9ae96c984c6176ffc136", + ]; let configmap_api: Api = Api::namespaced(client.clone(), namespace); let poller = Poller::new() .with_timeout(Duration::from_secs(180)) .with_interval(Duration::from_secs(5)) - .with_error_message("Reference value not removed".to_string()); + .with_error_message("Reference value expectations not met".to_string()); poller.poll_async(|| { let api = configmap_api.clone(); async move { let cm = api.get("trustee-data").await?; if let Some(data) = &cm.data { if let Some(reference_values_json) = data.get("reference-values.json") { - if !reference_values_json.contains(EXPECTED_PCR4) { - return Ok(()); + for value in expected_ref_values { + if !reference_values_json.contains(value) { + return Err(anyhow::anyhow!("Reference value expectations not met")); + } } } } - Err(anyhow::anyhow!("Reference value not yet removed")) + Ok(()) } }).await?; + test_ctx.cleanup().await?; + Ok(()) } }