diff --git a/cli/cmd/generate.go b/cli/cmd/generate.go index ba1c3d5d5ef..622500e141f 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, coordinatorCPU, 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,26 @@ func runGenerate(cmd *cobra.Command, args []string) error { } } + var filteredSNP []manifest.SNPReferenceValues + for _, snp := range mnf.ReferenceValues.SNP { + 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 + if coordinatorCPU > 0 { + snp.TrustedMeasurement = filteredMeasurements[strconv.FormatUint(coordinatorCPU, 10)] + } + 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 +704,42 @@ func runtimeClassesFromUnstructured(fileMap map[string][]*unstructured.Unstructu return runtimeClasses, nil } +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, 0, err + } + if !isCCWorkload(applyConfig) { + continue + } + + isCoord := isCoordinator(applyConfig) + kuberesource.MapPodSpec(applyConfig, func(spec *applycorev1.PodSpecApplyConfiguration) *applycorev1.PodSpecApplyConfiguration { + if spec == nil { + return spec + } + + totalCPUs := getPodCPUCount(spec) + used[totalCPUs] = struct{}{} + if isCoord { + coordinatorCPU = totalCPUs + } + return spec + }) + } + } + + var out []uint64 + for k := range used { + out = append(out, k) + } + return out, coordinatorCPU, 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 +795,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 +834,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 diff --git a/cli/main.go b/cli/main.go index 08a76349068..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,8 +80,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()) + 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) @@ -157,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/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/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..35bca42c159 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,14 @@ 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]. +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/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 64e3d337ff0..384894f056c 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -130,77 +130,84 @@ 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") + measurements := refVal.TrustedMeasurements + if len(measurements) == 0 && refVal.TrustedMeasurement != "" { + measurements = map[string]HexString{"coordinator": refVal.TrustedMeasurement} } - trustedMeasurement, err := refVal.TrustedMeasurement.Bytes() - if err != nil { - return nil, fmt.Errorf("failed to convert TrustedMeasurement from manifest to byte slices: %w", err) + if len(measurements) == 0 { + return nil, errors.New("trusted measurements cannot be empty") } - 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 cpus, tm := range measurements { + trustedMeasurement, err := tm.Bytes() + if err != nil { + return nil, fmt.Errorf("failed to convert TrustedMeasurements for %s vCPUs from manifest to byte slices: %w", cpus, 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..acfa65826de 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, }, @@ -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 { @@ -311,7 +326,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) @@ -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 @@ -450,7 +488,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..bc3c76f5143 100644 --- a/internal/manifest/referencevalues.go +++ b/internal/manifest/referencevalues.go @@ -194,16 +194,13 @@ func (r *ReferenceValues) Patch(patches ReferenceValuePatches) error { type SNPReferenceValues struct { Platform string ProductName ProductName + TrustedMeasurements map[string]HexString TrustedMeasurement 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 +227,18 @@ 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 && r.TrustedMeasurement == "" { + 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)) + } + } + 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") diff --git a/packages/by-name/contrast/reference-values/package.nix b/packages/by-name/contrast/reference-values/package.nix index c071d2119eb..f7f6dcd7e44 100644 --- a/packages/by-name/contrast/reference-values/package.nix +++ b/packages/by-name/contrast/reference-values/package.nix @@ -25,29 +25,39 @@ let platformInfo = { SMTEnabled = true; }; - vcpuCounts = lib.range 1 8; + vcpuCounts = lib.range 1 220; products = [ "Milan" "Genoa" ]; 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/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; 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, 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