From 30db1e0b374ab9b64a94f9e3154ce820ed3924fc Mon Sep 17 00:00:00 2001 From: Charlotte Hartmann Paludo Date: Tue, 7 Apr 2026 11:00:50 +0200 Subject: [PATCH 1/5] cli: only add required CPU counts to manifest --- cli/cmd/generate.go | 86 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/cli/cmd/generate.go b/cli/cmd/generate.go index ba1c3d5d5ef..249da0cc2d2 100644 --- a/cli/cmd/generate.go +++ b/cli/cmd/generate.go @@ -145,6 +145,11 @@ func runGenerate(cmd *cobra.Command, args []string) error { usedPlatforms.Add(flags.referenceValuesPlatform) } + usedCPUs, err := usedCPUsFromUnstructured(fileMap) + if err != nil { + return fmt.Errorf("determining cpu counts used in deployment: %w", err) + } + // generate a manifest by checking if a manifest exists and using that, // or otherwise using a default. var mnf *manifest.Manifest @@ -176,6 +181,14 @@ func runGenerate(cmd *cobra.Command, args []string) error { } } + var filteredSNP []manifest.SNPReferenceValues + for _, snp := range mnf.ReferenceValues.SNP { + if snp.CPUs == 0 || slices.Contains(usedCPUs, snp.CPUs) { + filteredSNP = append(filteredSNP, snp) + } + } + mnf.ReferenceValues.SNP = filteredSNP + var runtimeHandler string if flags.referenceValuesPlatform == platforms.Unknown { // Due to the pre generate verifiers, this code path should only be reachable when all resources have an explicit runtime class set. @@ -679,6 +692,37 @@ func runtimeClassesFromUnstructured(fileMap map[string][]*unstructured.Unstructu return runtimeClasses, nil } +func usedCPUsFromUnstructured(fileMap map[string][]*unstructured.Unstructured) ([]uint64, error) { + used := make(map[uint64]struct{}) + for _, resources := range fileMap { + for _, r := range resources { + applyConfig, err := kuberesource.UnstructuredToApplyConfiguration(r) + if err != nil { + return nil, err + } + if !isCCWorkload(applyConfig) { + continue + } + + kuberesource.MapPodSpec(applyConfig, func(spec *applycorev1.PodSpecApplyConfiguration) *applycorev1.PodSpecApplyConfiguration { + if spec == nil { + return spec + } + + totalCPUs := getPodCPUCount(spec) + used[totalCPUs] = struct{}{} + return spec + }) + } + } + + var out []uint64 + for k := range used { + out = append(out, k) + } + return out, nil +} + func patchRuntimeClassName(defaultRuntimeHandler string) func(*applycorev1.PodSpecApplyConfiguration) (*applycorev1.PodSpecApplyConfiguration, error) { return func(spec *applycorev1.PodSpecApplyConfiguration) (*applycorev1.PodSpecApplyConfiguration, error) { if spec == nil || spec.RuntimeClassName == nil { @@ -734,24 +778,8 @@ func patchIDBlockAnnotation(res any) error { return meta, spec, nil } - var regularContainersCPU int64 - for _, container := range spec.Containers { - regularContainersCPU += getCPUCount(container.Resources) - } - var initContainersCPU int64 - for _, container := range spec.InitContainers { - cpuCount := getCPUCount(container.Resources) - initContainersCPU += cpuCount - // Sidecar containers remain running alongside the actual application, consuming CPU resources - if container.RestartPolicy != nil && *container.RestartPolicy == corev1.ContainerRestartPolicyAlways { - regularContainersCPU += cpuCount - } - } - podLevelCPU := getCPUCount(spec.Resources) - - // Convert milliCPUs to number of CPUs (rounding up), and add 1 for hypervisor overhead - totalMilliCPUs := max(regularContainersCPU, initContainersCPU, podLevelCPU) - cpuCount := strconv.FormatInt((totalMilliCPUs+999)/1000+1, 10) + totalCPUs := getPodCPUCount(spec) + cpuCount := strconv.FormatUint(totalCPUs, 10) platform := strings.ToLower(targetPlatform.String()) @@ -789,6 +817,28 @@ func getCPUCount(resources *applycorev1.ResourceRequirementsApplyConfiguration) return 0 } +func getPodCPUCount(spec *applycorev1.PodSpecApplyConfiguration) uint64 { + var regularContainersCPU int64 + for _, container := range spec.Containers { + regularContainersCPU += getCPUCount(container.Resources) + } + var initContainersCPU int64 + for _, container := range spec.InitContainers { + cpuCount := getCPUCount(container.Resources) + initContainersCPU += cpuCount + // Sidecar containers remain running alongside the actual application, consuming CPU resources + if container.RestartPolicy != nil && *container.RestartPolicy == corev1.ContainerRestartPolicyAlways { + regularContainersCPU += cpuCount + } + } + podLevelCPU := getCPUCount(spec.Resources) + + // Convert milliCPUs to number of CPUs (rounding up), and add 1 for hypervisor overhead + totalMilliCPUs := max(regularContainersCPU, initContainersCPU, podLevelCPU) + totalCPUs := (totalMilliCPUs+999)/1000 + 1 + return uint64(totalCPUs) +} + type generateFlags struct { policyPath string settingsPath string From 85d97cc69b9a825789bc8e022742a8e8810769b5 Mon Sep 17 00:00:00 2001 From: Charlotte Hartmann Paludo Date: Tue, 7 Apr 2026 13:27:20 +0200 Subject: [PATCH 2/5] treewide: restructure `TrustedMeasurements` manifest entries for multi-CPU --- cli/cmd/generate.go | 11 +- cli/main.go | 6 +- .../internal/peerrecovery/recovery_test.go | 2 +- .../internal/stateguard/stateguard_test.go | 2 +- coordinator/internal/userapi/userapi_test.go | 2 +- internal/manifest/manifest.go | 130 +++++++++--------- internal/manifest/manifest_test.go | 10 +- internal/manifest/referencevalues.go | 15 +- .../contrast/reference-values/package.nix | 28 ++-- sdk/verify_test.go | 2 +- 10 files changed, 116 insertions(+), 92 deletions(-) diff --git a/cli/cmd/generate.go b/cli/cmd/generate.go index 249da0cc2d2..68fabeac414 100644 --- a/cli/cmd/generate.go +++ b/cli/cmd/generate.go @@ -183,7 +183,16 @@ func runGenerate(cmd *cobra.Command, args []string) error { var filteredSNP []manifest.SNPReferenceValues for _, snp := range mnf.ReferenceValues.SNP { - if snp.CPUs == 0 || slices.Contains(usedCPUs, snp.CPUs) { + filteredMeasurements := make(map[string]manifest.HexString) + for cpuStr, meas := range snp.TrustedMeasurements { + cpu, _ := strconv.ParseUint(cpuStr, 10, 64) + if slices.Contains(usedCPUs, cpu) { + filteredMeasurements[cpuStr] = meas + } + } + + if len(filteredMeasurements) > 0 { + snp.TrustedMeasurements = filteredMeasurements filteredSNP = append(filteredSNP, snp) } } diff --git a/cli/main.go b/cli/main.go index 08a76349068..dfd0f243085 100644 --- a/cli/main.go +++ b/cli/main.go @@ -78,8 +78,10 @@ func buildVersionString() (string, error) { } for _, snp := range values.SNP { fmt.Fprintf(versionsWriter, "\t- product name:\t%s\n", snp.ProductName) - fmt.Fprintf(versionsWriter, "\t vCPUs:\t%d\n", snp.CPUs) - fmt.Fprintf(versionsWriter, "\t launch digest:\t%s\n", snp.TrustedMeasurement.String()) + for cpu, meas := range snp.TrustedMeasurements { + fmt.Fprintf(versionsWriter, "\t vCPUs:\t%s\n", cpu) + fmt.Fprintf(versionsWriter, "\t launch digest:\t%s\n", meas.String()) + } fmt.Fprint(versionsWriter, "\t default SNP TCB:\t\n") printOptionalSVN("bootloader", snp.MinimumTCB.BootloaderVersion) printOptionalSVN("tee", snp.MinimumTCB.TEEVersion) diff --git a/coordinator/internal/peerrecovery/recovery_test.go b/coordinator/internal/peerrecovery/recovery_test.go index 56fccf3ef64..73459865073 100644 --- a/coordinator/internal/peerrecovery/recovery_test.go +++ b/coordinator/internal/peerrecovery/recovery_test.go @@ -268,7 +268,7 @@ func newManifest(t *testing.T) (*manifest.Manifest, []byte) { SNPVersion: &svn0, MicrocodeVersion: &svn0, }, - TrustedMeasurement: manifest.NewHexString(measurement[:]), + TrustedMeasurements: map[string]manifest.HexString{"1": manifest.NewHexString(measurement[:])}, GuestPolicy: abi.SnpPolicy{ SMT: true, }, diff --git a/coordinator/internal/stateguard/stateguard_test.go b/coordinator/internal/stateguard/stateguard_test.go index 8a2ef5430e4..64458d19d30 100644 --- a/coordinator/internal/stateguard/stateguard_test.go +++ b/coordinator/internal/stateguard/stateguard_test.go @@ -532,7 +532,7 @@ func newManifest(t *testing.T) (*manifest.Manifest, []byte, [][]byte) { SNPVersion: &svn0, MicrocodeVersion: &svn0, }, - TrustedMeasurement: manifest.NewHexString(measurement[:]), + TrustedMeasurements: map[string]manifest.HexString{"1": manifest.NewHexString(measurement[:])}, GuestPolicy: abi.SnpPolicy{ SMT: true, }, diff --git a/coordinator/internal/userapi/userapi_test.go b/coordinator/internal/userapi/userapi_test.go index 47d2d965fda..1822ef69d3c 100644 --- a/coordinator/internal/userapi/userapi_test.go +++ b/coordinator/internal/userapi/userapi_test.go @@ -865,7 +865,7 @@ func newManifestWithSeedshareOwner(t *testing.T) ([]byte, [][]byte) { SNPVersion: &svn0, MicrocodeVersion: &svn0, }, - TrustedMeasurement: manifest.NewHexString(measurement[:]), + TrustedMeasurements: map[string]manifest.HexString{"1": manifest.NewHexString(measurement[:])}, GuestPolicy: abi.SnpPolicy{ SMT: true, }, diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 64e3d337ff0..fb8780c5c16 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -130,77 +130,79 @@ func (m *Manifest) SNPValidateOpts(kdsGetter *certcache.CachedHTTPSGetter) ([]SN var out []SNPValidatorOptions for _, refVal := range m.ReferenceValues.SNP { - if len(refVal.TrustedMeasurement) == 0 { - return nil, errors.New("trusted measurement cannot be empty") + if len(refVal.TrustedMeasurements) == 0 { + return nil, errors.New("trusted measurements cannot be empty") } - trustedMeasurement, err := refVal.TrustedMeasurement.Bytes() - if err != nil { - return nil, fmt.Errorf("failed to convert TrustedMeasurement from manifest to byte slices: %w", err) - } - - verifyOpts := snpverify.DefaultOptions() - // Setting the productLine explicitly, because of full dependence of trustedMeasurements and derivation of trustedRoots on productLine. - verifyOpts.Product, err = kds.ParseProductLine(string(refVal.ProductName)) - if err != nil { - return nil, fmt.Errorf("SNP reference values: %w", err) - } - verifyOpts.TrustedRoots, err = amdTrustedRootCerts(refVal.ProductName) - if err != nil { - return nil, fmt.Errorf("determine trusted roots: %w", err) - } - verifyOpts.CheckRevocations = true - verifyOpts.Getter = kdsGetter.SNPGetter() + for _, tm := range refVal.TrustedMeasurements { + trustedMeasurement, err := tm.Bytes() + if err != nil { + return nil, fmt.Errorf("failed to convert TrustedMeasurements from manifest to byte slices: %w", err) + } - // Generate static public IDKey based on the launch digest and guest policy. - _, authBlk, err := idblock.IDBlocksFromLaunchDigest([48]byte(trustedMeasurement), refVal.GuestPolicy) - if err != nil { - return nil, fmt.Errorf("failed to generate ID blocks: %w", err) - } - idKeyBytes, err := authBlk.IDKey.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal IDKey: %w", err) - } - idKeyHash := sha512.Sum384(idKeyBytes) - - validateOpts := &snpvalidate.Options{ - Measurement: trustedMeasurement, - PlatformInfo: &refVal.PlatformInfo, - GuestPolicy: refVal.GuestPolicy, - VMPL: new(int), // VMPL0 - MinimumTCB: kds.TCBParts{ - BlSpl: refVal.MinimumTCB.BootloaderVersion.UInt8(), - TeeSpl: refVal.MinimumTCB.TEEVersion.UInt8(), - SnpSpl: refVal.MinimumTCB.SNPVersion.UInt8(), - UcodeSpl: refVal.MinimumTCB.MicrocodeVersion.UInt8(), - }, - MinimumLaunchTCB: kds.TCBParts{ - BlSpl: refVal.MinimumTCB.BootloaderVersion.UInt8(), - TeeSpl: refVal.MinimumTCB.TEEVersion.UInt8(), - SnpSpl: refVal.MinimumTCB.SNPVersion.UInt8(), - UcodeSpl: refVal.MinimumTCB.MicrocodeVersion.UInt8(), - }, - PermitProvisionalFirmware: true, - RequireIDBlock: true, - TrustedIDKeyHashes: [][]byte{idKeyHash[:]}, - MinimumLaunchMitigationVector: refVal.MinimumMitigationVector, - MinimumCurrentMitigationVector: refVal.MinimumMitigationVector, - } + verifyOpts := snpverify.DefaultOptions() + // Setting the productLine explicitly, because of full dependence of trustedMeasurements and derivation of trustedRoots on productLine. + verifyOpts.Product, err = kds.ParseProductLine(string(refVal.ProductName)) + if err != nil { + return nil, fmt.Errorf("SNP reference values: %w", err) + } + verifyOpts.TrustedRoots, err = amdTrustedRootCerts(refVal.ProductName) + if err != nil { + return nil, fmt.Errorf("determine trusted roots: %w", err) + } + verifyOpts.CheckRevocations = true + verifyOpts.Getter = kdsGetter.SNPGetter() - var allowedChipIDs [][]byte - for _, chipIDHex := range refVal.AllowedChipIDs { - chipID, err := chipIDHex.Bytes() + // Generate static public IDKey based on the launch digest and guest policy. + _, authBlk, err := idblock.IDBlocksFromLaunchDigest([48]byte(trustedMeasurement), refVal.GuestPolicy) if err != nil { - return nil, fmt.Errorf("failed to convert AllowedChipID from manifest to byte slices: %w", err) + return nil, fmt.Errorf("failed to generate ID blocks: %w", err) + } + idKeyBytes, err := authBlk.IDKey.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal IDKey: %w", err) + } + idKeyHash := sha512.Sum384(idKeyBytes) + + validateOpts := &snpvalidate.Options{ + Measurement: trustedMeasurement, + PlatformInfo: &refVal.PlatformInfo, + GuestPolicy: refVal.GuestPolicy, + VMPL: new(int), // VMPL0 + MinimumTCB: kds.TCBParts{ + BlSpl: refVal.MinimumTCB.BootloaderVersion.UInt8(), + TeeSpl: refVal.MinimumTCB.TEEVersion.UInt8(), + SnpSpl: refVal.MinimumTCB.SNPVersion.UInt8(), + UcodeSpl: refVal.MinimumTCB.MicrocodeVersion.UInt8(), + }, + MinimumLaunchTCB: kds.TCBParts{ + BlSpl: refVal.MinimumTCB.BootloaderVersion.UInt8(), + TeeSpl: refVal.MinimumTCB.TEEVersion.UInt8(), + SnpSpl: refVal.MinimumTCB.SNPVersion.UInt8(), + UcodeSpl: refVal.MinimumTCB.MicrocodeVersion.UInt8(), + }, + PermitProvisionalFirmware: true, + RequireIDBlock: true, + TrustedIDKeyHashes: [][]byte{idKeyHash[:]}, + MinimumLaunchMitigationVector: refVal.MinimumMitigationVector, + MinimumCurrentMitigationVector: refVal.MinimumMitigationVector, } - allowedChipIDs = append(allowedChipIDs, chipID) - } - out = append(out, SNPValidatorOptions{ - VerifyOpts: verifyOpts, - ValidateOpts: validateOpts, - AllowedChipIDs: allowedChipIDs, - }) + var allowedChipIDs [][]byte + for _, chipIDHex := range refVal.AllowedChipIDs { + chipID, err := chipIDHex.Bytes() + if err != nil { + return nil, fmt.Errorf("failed to convert AllowedChipID from manifest to byte slices: %w", err) + } + allowedChipIDs = append(allowedChipIDs, chipID) + } + + out = append(out, SNPValidatorOptions{ + VerifyOpts: verifyOpts, + ValidateOpts: validateOpts, + AllowedChipIDs: allowedChipIDs, + }) + } } return out, nil diff --git a/internal/manifest/manifest_test.go b/internal/manifest/manifest_test.go index 2274a4d0371..757731a82ba 100644 --- a/internal/manifest/manifest_test.go +++ b/internal/manifest/manifest_test.go @@ -34,8 +34,8 @@ func newTestManifestSNP() *Manifest { SNPVersion: toPtr(SVN(2)), MicrocodeVersion: toPtr(SVN(2)), }, - ProductName: "Milan", - TrustedMeasurement: HexString("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"), + ProductName: "Milan", + TrustedMeasurements: map[string]HexString{"1": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}, GuestPolicy: abi.SnpPolicy{ SMT: true, }, @@ -109,7 +109,7 @@ func TestValidate(t *testing.T) { "trusted measurement empty": { m: newTestManifestSNP(), mutate: func(m *Manifest) { - m.ReferenceValues.SNP[0].TrustedMeasurement = HexString("") + m.ReferenceValues.SNP[0].TrustedMeasurements = nil }, wantErr: true, }, @@ -311,7 +311,7 @@ func TestSNPValidateOpts(t *testing.T) { assert.NotNil(tcb.SNPVersion) assert.NotNil(tcb.MicrocodeVersion) - trustedMeasurement, err := m.ReferenceValues.SNP[0].TrustedMeasurement.Bytes() + trustedMeasurement, err := m.ReferenceValues.SNP[0].TrustedMeasurements["1"].Bytes() assert.NoError(err) assert.Equal(trustedMeasurement, opts[0].ValidateOpts.Measurement) @@ -450,7 +450,7 @@ func TestExpectedMissingReferenceValues(t *testing.T) { "snp with unexpected validation errors": { m: func() *Manifest { m := newTestManifestSNP() - m.ReferenceValues.SNP[0].TrustedMeasurement = "" + m.ReferenceValues.SNP[0].TrustedMeasurements = nil return m }(), want: false, diff --git a/internal/manifest/referencevalues.go b/internal/manifest/referencevalues.go index 715cbae7d5b..4e74c73213e 100644 --- a/internal/manifest/referencevalues.go +++ b/internal/manifest/referencevalues.go @@ -194,16 +194,12 @@ func (r *ReferenceValues) Patch(patches ReferenceValuePatches) error { type SNPReferenceValues struct { Platform string ProductName ProductName - TrustedMeasurement HexString + TrustedMeasurements map[string]HexString MinimumTCB SNPTCB GuestPolicy abi.SnpPolicy PlatformInfo abi.SnpPlatformInfo MinimumMitigationVector uint64 AllowedChipIDs []HexString - // CPUs is the number of vCPUs assigned to the VM. - // This field is purely informative as [SNPReferenceValues.TrustedMeasurement] - // already implicitly contains the number of vCPUs - CPUs uint64 } // Validate checks the validity of all fields in the AKS reference values. @@ -230,8 +226,13 @@ func (r SNPReferenceValues) Validate() error { errs = append(errs, newValidationError("ProductName", fmt.Errorf("unknown product name: %s", r.ProductName))) } - if err := validateHexString(r.TrustedMeasurement, abi.MeasurementSize); err != nil { - errs = append(errs, newValidationError("TrustedMeasurement", err)) + if len(r.TrustedMeasurements) == 0 { + errs = append(errs, newValidationError("TrustedMeasurements", fmt.Errorf("field cannot be empty"))) + } + for cpu, tm := range r.TrustedMeasurements { + if err := validateHexString(tm, abi.MeasurementSize); err != nil { + errs = append(errs, newValidationError(fmt.Sprintf("TrustedMeasurements[%s]", cpu), err)) + } } noModificationPermittedErr := errors.New("modifying this field is not permitted") diff --git a/packages/by-name/contrast/reference-values/package.nix b/packages/by-name/contrast/reference-values/package.nix index c071d2119eb..c2ac45d4708 100644 --- a/packages/by-name/contrast/reference-values/package.nix +++ b/packages/by-name/contrast/reference-values/package.nix @@ -32,22 +32,32 @@ let ]; generateRefVal = - vcpus: product: + product: let - launch-digest = kata.calculateSnpLaunchDigest { - inherit os-image vcpus; - inherit (node-installer-image) withDebug; - }; filename = "${lib.toLower product}.hex"; + getMeasurement = + vcpus: + let + launch-digest = kata.calculateSnpLaunchDigest { + inherit os-image vcpus; + inherit (node-installer-image) withDebug; + }; + in + builtins.readFile "${launch-digest}/${filename}"; + + TrustedMeasurements = builtins.listToAttrs ( + map (vcpus: { + name = toString vcpus; + value = getMeasurement vcpus; + }) vcpuCounts + ); in { - inherit guestPolicy platformInfo; - trustedMeasurement = builtins.readFile "${launch-digest}/${filename}"; + inherit guestPolicy platformInfo TrustedMeasurements; productName = product; - cpus = vcpus; }; in - builtins.concatLists (map (vcpus: map (product: generateRefVal vcpus product) products) vcpuCounts); + map generateRefVal products; }; snpRefVals = snpRefValsWith node-installer-image.os-image; diff --git a/sdk/verify_test.go b/sdk/verify_test.go index e4f602a18ad..3b4f3c1f571 100644 --- a/sdk/verify_test.go +++ b/sdk/verify_test.go @@ -195,7 +195,7 @@ var testManifest = []byte(` "MicrocodeVersion": 213 }, "ProductName": "Milan", - "TrustedMeasurement": "05c504736ca974b9ac0c84b5099f957907507c09e4844bd0672d0b647205f35837bd479ae35567b22b69ce636666c286", + "TrustedMeasurements": { "1": "05c504736ca974b9ac0c84b5099f957907507c09e4844bd0672d0b647205f35837bd479ae35567b22b69ce636666c286" }, "GuestPolicy": { "ABIMinor": 0, "ABIMajor": 0, From ed3d885e6a4504aae83302c2898999bf23c617c5 Mon Sep 17 00:00:00 2001 From: Charlotte Hartmann Paludo Date: Fri, 10 Apr 2026 13:43:03 +0200 Subject: [PATCH 3/5] cli: re-add `TrustedMeasurement` for forward- and backward-compatibility --- cli/cmd/generate.go | 16 +++++++++--- internal/kuberesource/sets.go | 6 ++++- internal/manifest/manifest.go | 11 +++++--- internal/manifest/manifest_test.go | 38 ++++++++++++++++++++++++++++ internal/manifest/referencevalues.go | 8 +++++- 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/cli/cmd/generate.go b/cli/cmd/generate.go index 68fabeac414..622500e141f 100644 --- a/cli/cmd/generate.go +++ b/cli/cmd/generate.go @@ -145,7 +145,7 @@ func runGenerate(cmd *cobra.Command, args []string) error { usedPlatforms.Add(flags.referenceValuesPlatform) } - usedCPUs, err := usedCPUsFromUnstructured(fileMap) + usedCPUs, coordinatorCPU, err := usedCPUsFromUnstructured(fileMap) if err != nil { return fmt.Errorf("determining cpu counts used in deployment: %w", err) } @@ -193,6 +193,9 @@ func runGenerate(cmd *cobra.Command, args []string) error { if len(filteredMeasurements) > 0 { snp.TrustedMeasurements = filteredMeasurements + if coordinatorCPU > 0 { + snp.TrustedMeasurement = filteredMeasurements[strconv.FormatUint(coordinatorCPU, 10)] + } filteredSNP = append(filteredSNP, snp) } } @@ -701,18 +704,20 @@ func runtimeClassesFromUnstructured(fileMap map[string][]*unstructured.Unstructu return runtimeClasses, nil } -func usedCPUsFromUnstructured(fileMap map[string][]*unstructured.Unstructured) ([]uint64, error) { +func usedCPUsFromUnstructured(fileMap map[string][]*unstructured.Unstructured) ([]uint64, uint64, error) { used := make(map[uint64]struct{}) + var coordinatorCPU uint64 for _, resources := range fileMap { for _, r := range resources { applyConfig, err := kuberesource.UnstructuredToApplyConfiguration(r) if err != nil { - return nil, err + return nil, 0, err } if !isCCWorkload(applyConfig) { continue } + isCoord := isCoordinator(applyConfig) kuberesource.MapPodSpec(applyConfig, func(spec *applycorev1.PodSpecApplyConfiguration) *applycorev1.PodSpecApplyConfiguration { if spec == nil { return spec @@ -720,6 +725,9 @@ func usedCPUsFromUnstructured(fileMap map[string][]*unstructured.Unstructured) ( totalCPUs := getPodCPUCount(spec) used[totalCPUs] = struct{}{} + if isCoord { + coordinatorCPU = totalCPUs + } return spec }) } @@ -729,7 +737,7 @@ func usedCPUsFromUnstructured(fileMap map[string][]*unstructured.Unstructured) ( for k := range used { out = append(out, k) } - return out, nil + return out, coordinatorCPU, nil } func patchRuntimeClassName(defaultRuntimeHandler string) func(*applycorev1.PodSpecApplyConfiguration) (*applycorev1.PodSpecApplyConfiguration, error) { diff --git a/internal/kuberesource/sets.go b/internal/kuberesource/sets.go index c2a6d9ba894..6cd1cf33f6a 100644 --- a/internal/kuberesource/sets.go +++ b/internal/kuberesource/sets.go @@ -80,7 +80,11 @@ func OpenSSL() []any { WithContainerPort(443), ). WithResources(ResourceRequirements(). - WithMemoryLimitAndRequest(250), + WithMemoryLimitAndRequest(250). + WithLimits(corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("250Mi"), + corev1.ResourceCPU: resource.MustParse("2"), + }), ). WithReadinessProbe(Probe(). WithInitialDelaySeconds(1). diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index fb8780c5c16..384894f056c 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -130,14 +130,19 @@ func (m *Manifest) SNPValidateOpts(kdsGetter *certcache.CachedHTTPSGetter) ([]SN var out []SNPValidatorOptions for _, refVal := range m.ReferenceValues.SNP { - if len(refVal.TrustedMeasurements) == 0 { + measurements := refVal.TrustedMeasurements + if len(measurements) == 0 && refVal.TrustedMeasurement != "" { + measurements = map[string]HexString{"coordinator": refVal.TrustedMeasurement} + } + + if len(measurements) == 0 { return nil, errors.New("trusted measurements cannot be empty") } - for _, tm := range refVal.TrustedMeasurements { + for cpus, tm := range measurements { trustedMeasurement, err := tm.Bytes() if err != nil { - return nil, fmt.Errorf("failed to convert TrustedMeasurements from manifest to byte slices: %w", err) + return nil, fmt.Errorf("failed to convert TrustedMeasurements for %s vCPUs from manifest to byte slices: %w", cpus, err) } verifyOpts := snpverify.DefaultOptions() diff --git a/internal/manifest/manifest_test.go b/internal/manifest/manifest_test.go index 757731a82ba..acfa65826de 100644 --- a/internal/manifest/manifest_test.go +++ b/internal/manifest/manifest_test.go @@ -244,6 +244,21 @@ func TestValidate(t *testing.T) { }, wantErr: true, }, + "fallback to trusted measurement": { + m: newTestManifestSNP(), + mutate: func(m *Manifest) { + m.ReferenceValues.SNP[0].TrustedMeasurement = m.ReferenceValues.SNP[0].TrustedMeasurements["1"] + m.ReferenceValues.SNP[0].TrustedMeasurements = nil + }, + }, + "trusted measurements empty and trusted measurement empty": { + m: newTestManifestSNP(), + mutate: func(m *Manifest) { + m.ReferenceValues.SNP[0].TrustedMeasurements = nil + m.ReferenceValues.SNP[0].TrustedMeasurement = "" + }, + wantErr: true, + }, } for name, tc := range testCases { @@ -326,6 +341,29 @@ func TestSNPValidateOpts(t *testing.T) { assert.Equal(tcbParts, opts[0].ValidateOpts.MinimumLaunchTCB) } +func TestSNPValidateOptsFallback(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + m := newTestManifestSNP() + m.ReferenceValues.SNP[0].TrustedMeasurement = m.ReferenceValues.SNP[0].TrustedMeasurements["1"] + m.ReferenceValues.SNP[0].TrustedMeasurements = nil + + m.Policies = map[HexString]PolicyEntry{ + HexString("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"): { + Role: RoleCoordinator, + }, + } + + opts, err := m.SNPValidateOpts(nil) + require.NoError(err) + require.Len(opts, 1) + + trustedMeasurement, err := m.ReferenceValues.SNP[0].TrustedMeasurement.Bytes() + assert.NoError(err) + assert.Equal(trustedMeasurement, opts[0].ValidateOpts.Measurement) +} + func TestHexString(t *testing.T) { testCases := []struct { b []byte diff --git a/internal/manifest/referencevalues.go b/internal/manifest/referencevalues.go index 4e74c73213e..bc3c76f5143 100644 --- a/internal/manifest/referencevalues.go +++ b/internal/manifest/referencevalues.go @@ -195,6 +195,7 @@ type SNPReferenceValues struct { Platform string ProductName ProductName TrustedMeasurements map[string]HexString + TrustedMeasurement HexString MinimumTCB SNPTCB GuestPolicy abi.SnpPolicy PlatformInfo abi.SnpPlatformInfo @@ -226,7 +227,7 @@ func (r SNPReferenceValues) Validate() error { errs = append(errs, newValidationError("ProductName", fmt.Errorf("unknown product name: %s", r.ProductName))) } - if len(r.TrustedMeasurements) == 0 { + if len(r.TrustedMeasurements) == 0 && r.TrustedMeasurement == "" { errs = append(errs, newValidationError("TrustedMeasurements", fmt.Errorf("field cannot be empty"))) } for cpu, tm := range r.TrustedMeasurements { @@ -234,6 +235,11 @@ func (r SNPReferenceValues) Validate() error { errs = append(errs, newValidationError(fmt.Sprintf("TrustedMeasurements[%s]", cpu), err)) } } + if r.TrustedMeasurement != "" { + if err := validateHexString(r.TrustedMeasurement, abi.MeasurementSize); err != nil { + errs = append(errs, newValidationError("TrustedMeasurement", err)) + } + } noModificationPermittedErr := errors.New("modifying this field is not permitted") var guestPolicyErrs []error From eefeba8922e6f8f5f838a142845321ec2b1e073d Mon Sep 17 00:00:00 2001 From: Charlotte Hartmann Paludo Date: Tue, 7 Apr 2026 13:39:35 +0200 Subject: [PATCH 4/5] docs: reflect changes to `TrsutedMeasurements` --- dev-docs/frozen/security-overview.md | 7 +++++-- docs/docs/architecture/components/manifest.md | 15 +++++++++++---- .../config/vocabularies/edgeless/accept.txt | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/dev-docs/frozen/security-overview.md b/dev-docs/frozen/security-overview.md index 23461a947ba..ad26cbe05bb 100644 --- a/dev-docs/frozen/security-overview.md +++ b/dev-docs/frozen/security-overview.md @@ -91,7 +91,7 @@ The report includes the following information: The manifest, enforced by the Contrast coordinator, contains reference values used to verify all application pods. It can be seen as the trusted reference state of the deployment. The manifest includes: - **Policies:** One cryptographic hash per pod, representing its enforced runtime policy. -- **ReferenceValues**: The launch digests of the CVMs, based on AMD SEV-SNP. This doesn't include any application code but tracks the setup of the CVM. Confidential Pods on the same CPU have the same reference values. +- **ReferenceValues**: The launch digests of the CVMs, based on AMD SEV-SNP. This doesn't include any application code but tracks the setup of the CVM. Reference values are grouped by CPU count, as the measurement depends on the number of vCPUs assigned to the VM. - **WorkloadOwnerKeyDigests**: A public key digest used to authenticate subsequent manifest updates. - **SeedshareOwnerPubKeys**: Used for coordinator recovery. For details, see [later sections](#secret-recovery). @@ -127,7 +127,10 @@ Here is an example manifest: "MicrocodeVersion": 72 }, "ProductName": "Genoa", - "TrustedMeasurement": "92a34339f1e1ec94b911830cafa875082d4f51b9805f3c2638ce468c6fda038be5acaca52fea5cb767e18cc1edfb1f7c" + "TrustedMeasurements": { + "1": "92a34339f1e1ec94b911830cafa875082d4f51b9805f3c2638ce468c6fda038be5acaca52fea5cb767e18cc1edfb1f7c", + "2": "..." + } } ] }, diff --git a/docs/docs/architecture/components/manifest.md b/docs/docs/architecture/components/manifest.md index b1e97696879..7785d8ddc7e 100644 --- a/docs/docs/architecture/components/manifest.md +++ b/docs/docs/architecture/components/manifest.md @@ -19,7 +19,11 @@ The manifest has the following higher level structure: "snp": [ { "ProductName": "", - "TrustedMeasurement": "", + "TrustedMeasurements": { + "1": "", + "2": "", + ... + }, "MinimumTCB": { }, "GuestPolicy": { }, "PlatformInfo": { } @@ -125,13 +129,16 @@ The Coordinator will accept a workload if its attestation report matches _any_ o The product name of your platform. `Milan` and `Genoa` are supported by Contrast. -### `ReferenceValues.snp.*.TrustedMeasurement` {#snp-trusted-measurement} +### `ReferenceValues.snp.*.TrustedMeasurements` {#snp-trusted-measurements} -The `TrustedMeasurement` is a hash over the initial memory contents and state of the confidential VM. +The `TrustedMeasurements` is a map of vCPU counts to their respective launch measurements. +A launch measurement is a hash over the initial memory contents and state of the confidential VM. It covers the guest firmware, the initrd and kernel as well as the kernel command line. The kernel command line contains the dm-verity hash of the root filesystem, which contains all Contrast components that run inside the guest. -It's the (launch) `MEASUREMENT` from the SNP `ATTESTATION_REPORT`, according to Table 23 in the [SEV ABI Spec]. +Contrast currently supports 1 to 8 vCPUs per pod. The manifest must contain the measurement for the specific vCPU count used by the workload. + +The values are (launch) `MEASUREMENT`s from the SNP `ATTESTATION_REPORT`, according to Table 23 in the [SEV ABI Spec]. ### `ReferenceValues.snp.*.MinimumTCB` {#snp-minimum-tcb} diff --git a/tools/vale/styles/config/vocabularies/edgeless/accept.txt b/tools/vale/styles/config/vocabularies/edgeless/accept.txt index 6f9c3fb4f95..4570a59dc3c 100644 --- a/tools/vale/styles/config/vocabularies/edgeless/accept.txt +++ b/tools/vale/styles/config/vocabularies/edgeless/accept.txt @@ -186,7 +186,7 @@ URIs? userland UUID validators? -vCPU +vCPUs? vendored virsh virtualized From 7a33a15da7557b7799520d495324f61acbf5a2d8 Mon Sep 17 00:00:00 2001 From: Charlotte Hartmann Paludo Date: Tue, 7 Apr 2026 15:26:58 +0200 Subject: [PATCH 5/5] treewide: bump supported vCPU count to kernel maximum of 220 --- cli/main.go | 30 ++++++++++++++++--- docs/docs/architecture/components/manifest.md | 2 -- .../deployment-file-preparation.md | 2 +- .../contrast/reference-values/package.nix | 2 +- .../contrast/snp-id-blocks/package.nix | 2 +- 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/cli/main.go b/cli/main.go index dfd0f243085..50bc0af780c 100644 --- a/cli/main.go +++ b/cli/main.go @@ -8,6 +8,8 @@ import ( "fmt" "os" "os/signal" + "sort" + "strconv" "strings" "text/tabwriter" @@ -78,10 +80,10 @@ func buildVersionString() (string, error) { } for _, snp := range values.SNP { fmt.Fprintf(versionsWriter, "\t- product name:\t%s\n", snp.ProductName) - for cpu, meas := range snp.TrustedMeasurements { - fmt.Fprintf(versionsWriter, "\t vCPUs:\t%s\n", cpu) - fmt.Fprintf(versionsWriter, "\t launch digest:\t%s\n", meas.String()) - } + fmt.Fprintf(versionsWriter, "\t launch digest per vCPU count:\n") + forSortedMeasurement(snp.TrustedMeasurements, func(cpu string, meas manifest.HexString) { + fmt.Fprintf(versionsWriter, "\t %3s: %s\n", cpu, meas.String()) + }) fmt.Fprint(versionsWriter, "\t default SNP TCB:\t\n") printOptionalSVN("bootloader", snp.MinimumTCB.BootloaderVersion) printOptionalSVN("tee", snp.MinimumTCB.TEEVersion) @@ -159,3 +161,23 @@ func signalContext(ctx context.Context, sig os.Signal) (context.Context, context func preRunRoot(cmd *cobra.Command, _ []string) { cmd.SilenceUsage = true } + +func forSortedMeasurement( + m map[string]manifest.HexString, + fn func(cpu string, meas manifest.HexString), +) { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + sort.Slice(keys, func(i, j int) bool { + ai, _ := strconv.Atoi(keys[i]) + bi, _ := strconv.Atoi(keys[j]) + return ai < bi + }) + + for _, k := range keys { + fn(k, m[k]) + } +} diff --git a/docs/docs/architecture/components/manifest.md b/docs/docs/architecture/components/manifest.md index 7785d8ddc7e..35bca42c159 100644 --- a/docs/docs/architecture/components/manifest.md +++ b/docs/docs/architecture/components/manifest.md @@ -136,8 +136,6 @@ A launch measurement is a hash over the initial memory contents and state of the It covers the guest firmware, the initrd and kernel as well as the kernel command line. The kernel command line contains the dm-verity hash of the root filesystem, which contains all Contrast components that run inside the guest. -Contrast currently supports 1 to 8 vCPUs per pod. The manifest must contain the measurement for the specific vCPU count used by the workload. - The values are (launch) `MEASUREMENT`s from the SNP `ATTESTATION_REPORT`, according to Table 23 in the [SEV ABI Spec]. ### `ReferenceValues.snp.*.MinimumTCB` {#snp-minimum-tcb} diff --git a/docs/docs/howto/workload-deployment/deployment-file-preparation.md b/docs/docs/howto/workload-deployment/deployment-file-preparation.md index 1d5025ea931..8a45984a43b 100644 --- a/docs/docs/howto/workload-deployment/deployment-file-preparation.md +++ b/docs/docs/howto/workload-deployment/deployment-file-preparation.md @@ -196,7 +196,7 @@ Altogether, setting the limit to 10x the compressed image size should be suffici :::warning Each Contrast pod requires one CPU by default. -Containers may request additional CPUs, up to a total of 8 CPUs per pod. +Containers may request additional CPUs, up to a total of 220 CPUs per pod. Please note that fractional CPU requests are always rounded up to the nearest whole number, meaning a Pod with one container requesting 0.2 CPUs will require 2 CPUs in total. ::: diff --git a/packages/by-name/contrast/reference-values/package.nix b/packages/by-name/contrast/reference-values/package.nix index c2ac45d4708..f7f6dcd7e44 100644 --- a/packages/by-name/contrast/reference-values/package.nix +++ b/packages/by-name/contrast/reference-values/package.nix @@ -25,7 +25,7 @@ let platformInfo = { SMTEnabled = true; }; - vcpuCounts = lib.range 1 8; + vcpuCounts = lib.range 1 220; products = [ "Milan" "Genoa" diff --git a/packages/by-name/contrast/snp-id-blocks/package.nix b/packages/by-name/contrast/snp-id-blocks/package.nix index 1a5f18cb7d8..528d48ae493 100644 --- a/packages/by-name/contrast/snp-id-blocks/package.nix +++ b/packages/by-name/contrast/snp-id-blocks/package.nix @@ -38,7 +38,7 @@ let }; }; - vcpuCounts = lib.range 1 8; + vcpuCounts = lib.range 1 220; allVcpuBlocks = builtins.listToAttrs ( map (vcpus: { name = toString vcpus;