From 39dd4d83d68fe87f3b3a4820d9c54797e307c52f Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:27:45 +0200 Subject: [PATCH] cli: embed multiple reference values This adds support for embedding a more versatile format of reference values (i.e. a structured type) into the Contrast binaries. This will allow us to embed all reference values at build-time from a single source (the Nix build file) rather than having SVNs in Go code and inserting trusted measurements via the go build commandline. It will now embed a JSON file containing the reference values, which is unmarshaled at first default manifest generation. --- cli/cmd/generate.go | 82 ++++----- cli/cmd/verify.go | 2 +- cli/main.go | 3 +- coordinator/internal/authority/authority.go | 2 +- .../internal/authority/authority_test.go | 3 +- .../internal/authority/userapi_test.go | 9 +- .../manifest/assets/reference-values.json | 1 + internal/manifest/constants.go | 45 +++-- internal/manifest/manifest.go | 143 +++++++--------- internal/manifest/manifest_test.go | 155 ++++-------------- internal/manifest/referencevalues.go | 100 +++++++++++ internal/manifest/referencevalues_test.go | 115 +++++++++++++ packages/by-name/contrast/package.nix | 38 +++-- 13 files changed, 410 insertions(+), 288 deletions(-) create mode 100755 internal/manifest/assets/reference-values.json create mode 100644 internal/manifest/referencevalues.go create mode 100644 internal/manifest/referencevalues_test.go diff --git a/cli/cmd/generate.go b/cli/cmd/generate.go index e9184b7f5b..5797e30c13 100644 --- a/cli/cmd/generate.go +++ b/cli/cmd/generate.go @@ -88,7 +88,7 @@ subcommands.`, func runGenerate(cmd *cobra.Command, args []string) error { flags, err := parseGenerateFlags(cmd) if err != nil { - return fmt.Errorf("failed to parse flags: %w", err) + return fmt.Errorf("parse flags: %w", err) } log, err := newCLILogger(cmd) @@ -101,23 +101,44 @@ func runGenerate(cmd *cobra.Command, args []string) error { return err } - if err := patchTargets(paths, flags.imageReplacementsFile, flags.skipInitializer, log); err != nil { - return fmt.Errorf("failed to patch targets: %w", err) + // generate manifest + defaultManifest := manifest.Default(flags.referenceValuesPlatform) + + defaultManifestData, err := json.MarshalIndent(&defaultManifest, "", " ") + if err != nil { + return fmt.Errorf("marshaling default manifest: %w", err) + } + manifestData, err := readFileOrDefault(flags.manifestPath, defaultManifestData) + if err != nil { + return fmt.Errorf("read manifest file: %w", err) + } + var manifest *manifest.Manifest + if err := json.Unmarshal(manifestData, &manifest); err != nil { + return fmt.Errorf("unmarshal manifest: %w", err) + } + + runtimeHandler, err := manifest.RuntimeHandler(flags.referenceValuesPlatform) + if err != nil { + return fmt.Errorf("get runtime handler: %w", err) + } + + if err := patchTargets(paths, flags.imageReplacementsFile, runtimeHandler, flags.skipInitializer, log); err != nil { + return fmt.Errorf("patch targets: %w", err) } fmt.Fprintln(cmd.OutOrStdout(), "✔️ Patched targets") if err := generatePolicies(cmd.Context(), flags.policyPath, flags.settingsPath, flags.genpolicyCachePath, paths, log); err != nil { - return fmt.Errorf("failed to generate policies: %w", err) + return fmt.Errorf("generate policies: %w", err) } fmt.Fprintln(cmd.OutOrStdout(), "✔️ Generated workload policy annotations") policies, err := policiesFromKubeResources(paths) if err != nil { - return fmt.Errorf("failed to find kube resources with policy: %w", err) + return fmt.Errorf("find kube resources with policy: %w", err) } policyMap, err := manifestPolicyMapFromPolicies(policies) if err != nil { - return fmt.Errorf("failed to create policy map: %w", err) + return fmt.Errorf("create policy map: %w", err) } if err := generateWorkloadOwnerKey(flags); err != nil { @@ -127,24 +148,6 @@ func runGenerate(cmd *cobra.Command, args []string) error { return fmt.Errorf("generating seedshare owner key: %w", err) } - defaultManifest := manifest.Default() - switch flags.referenceValuesPlatform { - case platforms.AKSCloudHypervisorSNP: - defaultManifest = manifest.DefaultAKS() - } - - defaultManifestData, err := json.MarshalIndent(&defaultManifest, "", " ") - if err != nil { - return fmt.Errorf("marshaling default manifest: %w", err) - } - manifestData, err := readFileOrDefault(flags.manifestPath, defaultManifestData) - if err != nil { - return fmt.Errorf("failed to read manifest file: %w", err) - } - var manifest *manifest.Manifest - if err := json.Unmarshal(manifestData, &manifest); err != nil { - return fmt.Errorf("failed to unmarshal manifest: %w", err) - } manifest.Policies = policyMap if err := manifest.Validate(); err != nil { return fmt.Errorf("validating manifest: %w", err) @@ -170,10 +173,10 @@ func runGenerate(cmd *cobra.Command, args []string) error { manifestData, err = json.MarshalIndent(manifest, "", " ") if err != nil { - return fmt.Errorf("failed to marshal manifest: %w", err) + return fmt.Errorf("marshal manifest: %w", err) } if err := os.WriteFile(flags.manifestPath, append(manifestData, '\n'), 0o644); err != nil { - return fmt.Errorf("failed to write manifest: %w", err) + return fmt.Errorf("write manifest: %w", err) } fmt.Fprintf(cmd.OutOrStdout(), "✔️ Updated manifest %s\n", flags.manifestPath) @@ -181,7 +184,7 @@ func runGenerate(cmd *cobra.Command, args []string) error { if hash := getCoordinatorPolicyHash(policies, log); hash != "" { coordHashPath := filepath.Join(flags.workspaceDir, coordHashFilename) if err := os.WriteFile(coordHashPath, []byte(hash), 0o644); err != nil { - return fmt.Errorf("failed to write coordinator policy hash: %w", err) + return fmt.Errorf("write coordinator policy hash: %w", err) } } @@ -207,7 +210,7 @@ func findGenerateTargets(args []string, logger *slog.Logger) ([]string, error) { return nil }) if err != nil { - return nil, fmt.Errorf("failed to walk %s: %w", path, err) + return nil, fmt.Errorf("walk %s: %w", path, err) } } if len(paths) == 0 { @@ -227,7 +230,7 @@ func filterNonCoCoRuntime(runtimeClassNamePrefix string, paths []string, logger for _, path := range paths { data, err := os.ReadFile(path) if err != nil { - logger.Warn("Failed to read file", "path", path, "err", err) + logger.Warn("read file", "path", path, "err", err) continue } if !bytes.Contains(data, []byte(runtimeClassNamePrefix)) { @@ -248,21 +251,21 @@ func generatePolicies(ctx context.Context, regoRulesPath, policySettingsPath, ge } binaryInstallDir, err := installDir() if err != nil { - return fmt.Errorf("failed to get install dir: %w", err) + return fmt.Errorf("get install dir: %w", err) } genpolicyInstall, err := embedbin.New().Install(binaryInstallDir, genpolicyBin) if err != nil { - return fmt.Errorf("failed to install genpolicy: %w", err) + return fmt.Errorf("install genpolicy: %w", err) } defer func() { if err := genpolicyInstall.Uninstall(); err != nil { - logger.Warn("Failed to uninstall genpolicy tool", "err", err) + logger.Warn("uninstall genpolicy tool", "err", err) } }() for _, yamlPath := range yamlPaths { policyHash, err := generatePolicyForFile(ctx, genpolicyInstall.Path(), regoRulesPath, policySettingsPath, yamlPath, genpolicyCachePath, logger) if err != nil { - return fmt.Errorf("failed to generate policy for %s: %w", yamlPath, err) + return fmt.Errorf("generate policy for %s: %w", yamlPath, err) } if policyHash == [32]byte{} { continue @@ -273,7 +276,7 @@ func generatePolicies(ctx context.Context, regoRulesPath, policySettingsPath, ge return nil } -func patchTargets(paths []string, imageReplacementsFile string, skipInitializer bool, logger *slog.Logger) error { +func patchTargets(paths []string, imageReplacementsFile, runtimeHandler string, skipInitializer bool, logger *slog.Logger) error { var replacements map[string]string var err error if imageReplacementsFile != "" { @@ -296,11 +299,11 @@ func patchTargets(paths []string, imageReplacementsFile string, skipInitializer for _, path := range paths { data, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("failed to read %s: %w", path, err) + return fmt.Errorf("read %s: %w", path, err) } kubeObjs, err := kuberesource.UnmarshalApplyConfigurations(data) if err != nil { - return fmt.Errorf("failed to unmarshal %s: %w", path, err) + return fmt.Errorf("unmarshal %s: %w", path, err) } if !skipInitializer { @@ -314,7 +317,7 @@ func patchTargets(paths []string, imageReplacementsFile string, skipInitializer kubeObjs = kuberesource.PatchImages(kubeObjs, replacements) - replaceRuntimeClassName := runtimeClassNamePatcher() + replaceRuntimeClassName := runtimeClassNamePatcher(runtimeHandler) for i := range kubeObjs { kubeObjs[i] = kuberesource.MapPodSpec(kubeObjs[i], replaceRuntimeClassName) } @@ -325,7 +328,7 @@ func patchTargets(paths []string, imageReplacementsFile string, skipInitializer return err } if err := os.WriteFile(path, resource, os.ModePerm); err != nil { - return fmt.Errorf("failed to write %s: %w", path, err) + return fmt.Errorf("write %s: %w", path, err) } } return nil @@ -362,8 +365,7 @@ func injectServiceMesh(resources []any) error { return nil } -func runtimeClassNamePatcher() func(*applycorev1.PodSpecApplyConfiguration) *applycorev1.PodSpecApplyConfiguration { - handler := runtimeHandler(manifest.TrustedMeasurement) +func runtimeClassNamePatcher(handler string) func(*applycorev1.PodSpecApplyConfiguration) *applycorev1.PodSpecApplyConfiguration { return func(spec *applycorev1.PodSpecApplyConfiguration) *applycorev1.PodSpecApplyConfiguration { if spec.RuntimeClassName == nil || *spec.RuntimeClassName == handler { return spec diff --git a/cli/cmd/verify.go b/cli/cmd/verify.go index c8ee86ae84..adb06568c5 100644 --- a/cli/cmd/verify.go +++ b/cli/cmd/verify.go @@ -175,7 +175,7 @@ func parseVerifyFlags(cmd *cobra.Command) (*verifyFlags, error) { } func newCoordinatorValidateOptsGen(mnfst manifest.Manifest, hostData []byte) (*snp.StaticValidateOptsGenerator, error) { - validateOpts, err := mnfst.SNPValidateOpts() + validateOpts, err := mnfst.AKSValidateOpts() if err != nil { return nil, err } diff --git a/cli/main.go b/cli/main.go index 8138174bc5..baa6c7d492 100644 --- a/cli/main.go +++ b/cli/main.go @@ -49,8 +49,7 @@ func buildVersionString() string { } fmt.Fprint(versionsWriter, "\n") fmt.Fprintf(versionsWriter, "reference values for %s platform:\n", platforms.AKSCloudHypervisorSNP.String()) - fmt.Fprintf(versionsWriter, "\truntime handler:\tcontrast-cc-%s\n", manifest.TrustedMeasurement[:32]) - fmt.Fprintf(versionsWriter, "\tlaunch digest:\t%s\n", manifest.TrustedMeasurement) + fmt.Fprintf(versionsWriter, "\tembedded reference values:\t%s\n", manifest.EmbeddedReferenceValuesJSON) fmt.Fprintf(versionsWriter, "\tgenpolicy version:\t%s\n", genpolicyVersion) versionsWriter.Flush() return versionsBuilder.String() diff --git a/coordinator/internal/authority/authority.go b/coordinator/internal/authority/authority.go index 32a09e7b1b..d6435f9130 100644 --- a/coordinator/internal/authority/authority.go +++ b/coordinator/internal/authority/authority.go @@ -94,7 +94,7 @@ func (m *Authority) SNPValidateOpts(report *sevsnp.Report) (*validate.Options, e return nil, fmt.Errorf("hostdata %s not found in manifest", hostData) } - return mnfst.SNPValidateOpts() + return mnfst.AKSValidateOpts() } // ValidateCallback creates a certificate bundle for the verified client. diff --git a/coordinator/internal/authority/authority_test.go b/coordinator/internal/authority/authority_test.go index 564431252d..6f184fa63a 100644 --- a/coordinator/internal/authority/authority_test.go +++ b/coordinator/internal/authority/authority_test.go @@ -15,6 +15,7 @@ import ( "github.com/edgelesssys/contrast/coordinator/history" "github.com/edgelesssys/contrast/internal/manifest" "github.com/edgelesssys/contrast/internal/userapi" + "github.com/edgelesssys/contrast/node-installer/platforms" "github.com/google/go-sev-guest/proto/sevsnp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" @@ -79,7 +80,7 @@ func newManifest(t *testing.T) (*manifest.Manifest, []byte, [][]byte) { policyHash := sha256.Sum256(policy) policyHashHex := manifest.NewHexString(policyHash[:]) - mnfst := manifest.DefaultAKS() + mnfst := manifest.Default(platforms.AKSCloudHypervisorSNP) mnfst.Policies = map[manifest.HexString][]string{policyHashHex: {"test"}} mnfst.WorkloadOwnerKeyDigests = []manifest.HexString{keyDigest} mnfstBytes, err := json.Marshal(mnfst) diff --git a/coordinator/internal/authority/userapi_test.go b/coordinator/internal/authority/userapi_test.go index caa7ce7130..c0bdda0e14 100644 --- a/coordinator/internal/authority/userapi_test.go +++ b/coordinator/internal/authority/userapi_test.go @@ -21,6 +21,7 @@ import ( "github.com/edgelesssys/contrast/coordinator/history" "github.com/edgelesssys/contrast/internal/manifest" "github.com/edgelesssys/contrast/internal/userapi" + "github.com/edgelesssys/contrast/node-installer/platforms" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/afero" "github.com/stretchr/testify/assert" @@ -33,7 +34,7 @@ import ( func TestManifestSet(t *testing.T) { newBaseManifest := func() manifest.Manifest { - return manifest.Default() + return manifest.Default(platforms.AKSCloudHypervisorSNP) } newManifestBytes := func(f func(*manifest.Manifest)) []byte { m := newBaseManifest() @@ -221,7 +222,7 @@ func TestGetManifests(t *testing.T) { require.Equal(codes.FailedPrecondition, status.Code(err)) assert.Nil(resp) - m := manifest.Default() + m := manifest.Default(platforms.AKSCloudHypervisorSNP) m.Policies = map[manifest.HexString][]string{ manifest.HexString("ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb"): {"a1", "a2"}, manifest.HexString("3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d"): {"b1", "b2"}, @@ -374,7 +375,7 @@ func TestRecoveryFlow(t *testing.T) { // gRPCs of the server. func TestUserAPIConcurrent(t *testing.T) { newBaseManifest := func() manifest.Manifest { - return manifest.Default() + return manifest.Default(platforms.AKSCloudHypervisorSNP) } newManifestBytes := func(f func(*manifest.Manifest)) []byte { m := newBaseManifest() @@ -459,7 +460,7 @@ func rpcContext(key *ecdsa.PrivateKey) context.Context { } func manifestWithWorkloadOwnerKey(key *ecdsa.PrivateKey) (*manifest.Manifest, error) { - m := manifest.Default() + m := manifest.Default(platforms.AKSCloudHypervisorSNP) if key == nil { return &m, nil } diff --git a/internal/manifest/assets/reference-values.json b/internal/manifest/assets/reference-values.json new file mode 100755 index 0000000000..eda2960cec --- /dev/null +++ b/internal/manifest/assets/reference-values.json @@ -0,0 +1 @@ +"THIS FILE IS REPLACED DURING BUILD AND ONLY HERE TO SATISFY GO TOOLING" diff --git a/internal/manifest/constants.go b/internal/manifest/constants.go index 1e0900b18e..825d0d987a 100644 --- a/internal/manifest/constants.go +++ b/internal/manifest/constants.go @@ -3,32 +3,31 @@ package manifest -// TrustedMeasurement contains the expected launch digest and is injected at build time. -var TrustedMeasurement = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +import ( + "encoding/json" + "fmt" + "os" -// Default returns a default manifest. -func Default() Manifest { - return Manifest{ - ReferenceValues: ReferenceValues{ - TrustedMeasurement: HexString(TrustedMeasurement), - }, + "github.com/edgelesssys/contrast/node-installer/platforms" +) + +// Default returns a default manifest with reference values for the given platform. +func Default(platform platforms.Platform) Manifest { + if embeddedReferenceValues == nil { + // If we're here, this is the first time this function is called, and the global state is not + // yet initialized. So let's unmarshal the embedded reference values. + if err := json.Unmarshal(EmbeddedReferenceValuesJSON, &embeddedReferenceValues); err != nil { + fmt.Printf("Failed to unmarshal embedded reference values: %s\n", err) + os.Exit(1) + } } -} -// DefaultAKS returns a default manifest with AKS reference values. -func DefaultAKS() Manifest { - mnfst := Default() - mnfst.ReferenceValues.SNP = SNPReferenceValues{ - MinimumTCB: SNPTCB{ - BootloaderVersion: toPtr(SVN(3)), - TEEVersion: toPtr(SVN(0)), - SNPVersion: toPtr(SVN(8)), - MicrocodeVersion: toPtr(SVN(115)), - }, + mnfst := Manifest{} + switch platform { + case platforms.AKSCloudHypervisorSNP: + mnfst.ReferenceValues.AKS = embeddedReferenceValues.AKS + case platforms.RKE2QEMUTDX, platforms.K3sQEMUTDX: + mnfst.ReferenceValues.BareMetalTDX = embeddedReferenceValues.BareMetalTDX } return mnfst } - -func toPtr[T any](t T) *T { - return &t -} diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 6132a9ef7c..2859e34759 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -6,11 +6,9 @@ package manifest import ( "crypto/sha256" "encoding/base64" - "encoding/hex" - "encoding/json" "fmt" - "strconv" + "github.com/edgelesssys/contrast/node-installer/platforms" "github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/kds" "github.com/google/go-sev-guest/validate" @@ -25,72 +23,6 @@ type Manifest struct { SeedshareOwnerPubKeys []HexString } -// ReferenceValues contains the workload independent reference values. -type ReferenceValues struct { - SNP SNPReferenceValues - // TrustedMeasurement is the hash of the trusted launch digest. - TrustedMeasurement HexString -} - -// SNPReferenceValues contains reference values for the SNP report. -type SNPReferenceValues struct { - MinimumTCB SNPTCB -} - -// SNPTCB represents a set of SNP TCB values. -type SNPTCB struct { - BootloaderVersion *SVN - TEEVersion *SVN - SNPVersion *SVN - MicrocodeVersion *SVN -} - -// SVN is a SNP secure version number. -type SVN uint8 - -// UInt8 returns the uint8 value of the SVN. -func (s *SVN) UInt8() uint8 { - return uint8(*s) -} - -// MarshalJSON marshals the SVN to JSON. -func (s SVN) MarshalJSON() ([]byte, error) { - return []byte(strconv.Itoa(int(s))), nil -} - -// UnmarshalJSON unmarshals the SVN from a JSON. -func (s *SVN) UnmarshalJSON(data []byte) error { - var value float64 - if err := json.Unmarshal(data, &value); err != nil { - return err - } - - if value < 0 || value > 255 { // Ensure the value fits into uint8 range - return fmt.Errorf("value out of range for uint8") - } - - *s = SVN(value) - return nil -} - -// HexString is a hex encoded string. -type HexString string - -// NewHexString creates a new HexString from a byte slice. -func NewHexString(b []byte) HexString { - return HexString(hex.EncodeToString(b)) -} - -// String returns the string representation of the HexString. -func (h HexString) String() string { - return string(h) -} - -// Bytes returns the byte slice representation of the HexString. -func (h HexString) Bytes() ([]byte, error) { - return hex.DecodeString(string(h)) -} - // HexStrings is a slice of HexString. type HexStrings []HexString @@ -128,6 +60,23 @@ func (p Policy) Hash() HexString { // Validate checks the validity of all fields in the reference values. func (r ReferenceValues) Validate() error { + if r.AKS != nil { + if err := r.AKS.Validate(); err != nil { + return fmt.Errorf("validating AKS reference values: %w", err) + } + } + + if r.BareMetalTDX != nil { + if err := r.BareMetalTDX.Validate(); err != nil { + return fmt.Errorf("validating bare metal TDX reference values: %w", err) + } + } + + return nil +} + +// Validate checks the validity of all fields in the AKS reference values. +func (r AKSReferenceValues) Validate() error { if r.SNP.MinimumTCB.BootloaderVersion == nil { return fmt.Errorf("field BootloaderVersion in manifest cannot be empty") } else if r.SNP.MinimumTCB.TEEVersion == nil { @@ -145,6 +94,14 @@ func (r ReferenceValues) Validate() error { return nil } +// Validate checks the validity of all fields in the bare metal TDX reference values. +func (r BareMetalTDXReferenceValues) Validate() error { + if r.TrustedMeasurement == "" { + return fmt.Errorf("field TrustedMeasurement in manifest cannot be empty") + } + return nil +} + // Validate checks the validity of all fields in the manifest. func (m *Manifest) Validate() error { for policyHash := range m.Policies { @@ -175,13 +132,13 @@ func (m *Manifest) Validate() error { return nil } -// SNPValidateOpts returns validate options populated with the manifest's -// SNP reference values and trusted measurement. -func (m *Manifest) SNPValidateOpts() (*validate.Options, error) { +// AKSValidateOpts returns validate options populated with the manifest's +// AKS reference values and trusted measurement. +func (m *Manifest) AKSValidateOpts() (*validate.Options, error) { if err := m.Validate(); err != nil { return nil, fmt.Errorf("validating manifest: %w", err) } - trustedMeasurement, err := m.ReferenceValues.TrustedMeasurement.Bytes() + trustedMeasurement, err := m.ReferenceValues.AKS.TrustedMeasurement.Bytes() if err != nil { return nil, fmt.Errorf("failed to convert TrustedMeasurement from manifest to byte slices: %w", err) } @@ -194,17 +151,41 @@ func (m *Manifest) SNPValidateOpts() (*validate.Options, error) { }, VMPL: new(int), // VMPL0 MinimumTCB: kds.TCBParts{ - BlSpl: m.ReferenceValues.SNP.MinimumTCB.BootloaderVersion.UInt8(), - TeeSpl: m.ReferenceValues.SNP.MinimumTCB.TEEVersion.UInt8(), - SnpSpl: m.ReferenceValues.SNP.MinimumTCB.SNPVersion.UInt8(), - UcodeSpl: m.ReferenceValues.SNP.MinimumTCB.MicrocodeVersion.UInt8(), + BlSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.BootloaderVersion.UInt8(), + TeeSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.TEEVersion.UInt8(), + SnpSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.SNPVersion.UInt8(), + UcodeSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.MicrocodeVersion.UInt8(), }, MinimumLaunchTCB: kds.TCBParts{ - BlSpl: m.ReferenceValues.SNP.MinimumTCB.BootloaderVersion.UInt8(), - TeeSpl: m.ReferenceValues.SNP.MinimumTCB.TEEVersion.UInt8(), - SnpSpl: m.ReferenceValues.SNP.MinimumTCB.SNPVersion.UInt8(), - UcodeSpl: m.ReferenceValues.SNP.MinimumTCB.MicrocodeVersion.UInt8(), + BlSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.BootloaderVersion.UInt8(), + TeeSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.TEEVersion.UInt8(), + SnpSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.SNPVersion.UInt8(), + UcodeSpl: m.ReferenceValues.AKS.SNP.MinimumTCB.MicrocodeVersion.UInt8(), }, PermitProvisionalFirmware: true, }, nil } + +// RuntimeHandler returns the runtime handler for the given platform. +func (m *Manifest) RuntimeHandler(platform platforms.Platform) (string, error) { + switch platform { + case platforms.AKSCloudHypervisorSNP: + return fmt.Sprintf("contrast-cc-%s", m.ReferenceValues.AKS.TrustedMeasurement[:32]), nil + case platforms.K3sQEMUTDX, platforms.RKE2QEMUTDX: + return fmt.Sprintf("contrast-cc-%s", m.ReferenceValues.BareMetalTDX.TrustedMeasurement[:32]), nil + default: + return "", fmt.Errorf("unsupported platform %s", platform) + } +} + +// TrustedMeasurement returns the trusted measurement for the given platform. +func (m *Manifest) TrustedMeasurement(platform platforms.Platform) (string, error) { + switch platform { + case platforms.AKSCloudHypervisorSNP: + return m.ReferenceValues.AKS.TrustedMeasurement.String(), nil + case platforms.K3sQEMUTDX, platforms.RKE2QEMUTDX: + return m.ReferenceValues.BareMetalTDX.TrustedMeasurement.String(), nil + default: + return "", fmt.Errorf("unsupported platform %s", platform) + } +} diff --git a/internal/manifest/manifest_test.go b/internal/manifest/manifest_test.go index aa91e7ebb3..45e011b1a4 100644 --- a/internal/manifest/manifest_test.go +++ b/internal/manifest/manifest_test.go @@ -5,91 +5,14 @@ package manifest import ( "encoding/base64" - "encoding/json" "strconv" "testing" + "github.com/edgelesssys/contrast/node-installer/platforms" "github.com/google/go-sev-guest/kds" "github.com/stretchr/testify/assert" ) -func TestSVN(t *testing.T) { - testCases := []struct { - enc string - dec SVN - wantErr bool - }{ - {enc: "0", dec: 0}, - {enc: "1", dec: 1}, - {enc: "255", dec: 255}, - {enc: "256", dec: 0, wantErr: true}, - {enc: "-1", dec: 0, wantErr: true}, - } - - t.Run("MarshalJSON", func(t *testing.T) { - for _, tc := range testCases { - if tc.wantErr { - continue - } - t.Run(tc.enc, func(t *testing.T) { - assert := assert.New(t) - enc, err := json.Marshal(tc.dec) - assert.NoError(err) - assert.Equal(tc.enc, string(enc)) - }) - } - }) - - t.Run("UnmarshalJSON", func(t *testing.T) { - for _, tc := range testCases { - t.Run(tc.enc, func(t *testing.T) { - assert := assert.New(t) - var dec SVN - err := json.Unmarshal([]byte(tc.enc), &dec) - if tc.wantErr { - assert.Error(err) - return - } - assert.NoError(err) - assert.Equal(tc.dec, dec) - }) - } - }) -} - -func TestHexString(t *testing.T) { - testCases := []struct { - b []byte - s string - }{ - {b: []byte{0x00}, s: "00"}, - {b: []byte{0x01}, s: "01"}, - {b: []byte{0x0f}, s: "0f"}, - {b: []byte{0x10}, s: "10"}, - {b: []byte{0x11}, s: "11"}, - {b: []byte{0xff}, s: "ff"}, - {b: []byte{0x00, 0x01}, s: "0001"}, - } - - for _, tc := range testCases { - t.Run(tc.s, func(t *testing.T) { - assert := assert.New(t) - hexString := NewHexString(tc.b) - assert.Equal(tc.s, hexString.String()) - b, err := hexString.Bytes() - assert.NoError(err) - assert.Equal(tc.b, b) - }) - } - - t.Run("invalid hexstring", func(t *testing.T) { - assert := assert.New(t) - hexString := HexString("invalid") - _, err := hexString.Bytes() - assert.Error(err) - }) -} - func TestHexStrings(t *testing.T) { testCases := []struct { hs HexStrings @@ -153,16 +76,12 @@ func TestValidate(t *testing.T) { wantErr bool }{ { - m: DefaultAKS(), - }, - { - m: Default(), - wantErr: true, + m: Default(platforms.AKSCloudHypervisorSNP), }, { m: Manifest{ Policies: map[HexString][]string{HexString(""): {}}, - ReferenceValues: DefaultAKS().ReferenceValues, + ReferenceValues: Default(platforms.AKSCloudHypervisorSNP).ReferenceValues, }, wantErr: true, }, @@ -170,15 +89,17 @@ func TestValidate(t *testing.T) { m: Manifest{ Policies: map[HexString][]string{HexString(""): {}}, ReferenceValues: ReferenceValues{ - SNP: Default().ReferenceValues.SNP, - TrustedMeasurement: "", + AKS: &AKSReferenceValues{ + SNP: Default(platforms.AKSCloudHypervisorSNP).ReferenceValues.AKS.SNP, + TrustedMeasurement: "", + }, }, }, wantErr: true, }, { m: Manifest{ - ReferenceValues: Default().ReferenceValues, + ReferenceValues: Default(platforms.AKSCloudHypervisorSNP).ReferenceValues, WorkloadOwnerKeyDigests: []HexString{HexString("")}, }, wantErr: true, @@ -197,44 +118,30 @@ func TestValidate(t *testing.T) { } } -func TestSNPValidateOpts(t *testing.T) { - testCases := []struct { - m Manifest - wantErr bool - }{ - {m: DefaultAKS()}, - {m: Default(), wantErr: true}, - } +func TestAKSValidateOpts(t *testing.T) { + assert := assert.New(t) - for i, tc := range testCases { - t.Run(strconv.Itoa(i), func(t *testing.T) { - assert := assert.New(t) + m := Default(platforms.AKSCloudHypervisorSNP) - opts, err := tc.m.SNPValidateOpts() - if tc.wantErr { - assert.Error(err) - return - } - assert.NoError(err) - - tcb := tc.m.ReferenceValues.SNP.MinimumTCB - assert.NotNil(tcb.BootloaderVersion) - assert.NotNil(tcb.TEEVersion) - assert.NotNil(tcb.SNPVersion) - assert.NotNil(tcb.MicrocodeVersion) - - trustedMeasurement, err := tc.m.ReferenceValues.TrustedMeasurement.Bytes() - assert.NoError(err) - assert.Equal(trustedMeasurement, opts.Measurement) - - tcbParts := kds.TCBParts{ - BlSpl: tcb.BootloaderVersion.UInt8(), - TeeSpl: tcb.TEEVersion.UInt8(), - SnpSpl: tcb.SNPVersion.UInt8(), - UcodeSpl: tcb.MicrocodeVersion.UInt8(), - } - assert.Equal(tcbParts, opts.MinimumTCB) - assert.Equal(tcbParts, opts.MinimumLaunchTCB) - }) + opts, err := m.AKSValidateOpts() + assert.NoError(err) + + tcb := m.ReferenceValues.AKS.SNP.MinimumTCB + assert.NotNil(tcb.BootloaderVersion) + assert.NotNil(tcb.TEEVersion) + assert.NotNil(tcb.SNPVersion) + assert.NotNil(tcb.MicrocodeVersion) + + trustedMeasurement, err := m.ReferenceValues.AKS.TrustedMeasurement.Bytes() + assert.NoError(err) + assert.Equal(trustedMeasurement, opts.Measurement) + + tcbParts := kds.TCBParts{ + BlSpl: tcb.BootloaderVersion.UInt8(), + TeeSpl: tcb.TEEVersion.UInt8(), + SnpSpl: tcb.SNPVersion.UInt8(), + UcodeSpl: tcb.MicrocodeVersion.UInt8(), } + assert.Equal(tcbParts, opts.MinimumTCB) + assert.Equal(tcbParts, opts.MinimumLaunchTCB) } diff --git a/internal/manifest/referencevalues.go b/internal/manifest/referencevalues.go new file mode 100644 index 0000000000..86c16a5941 --- /dev/null +++ b/internal/manifest/referencevalues.go @@ -0,0 +1,100 @@ +// Copyright 2024 Edgeless Systems GmbH +// SPDX-License-Identifier: AGPL-3.0-only + +package manifest + +import ( + _ "embed" + "encoding/hex" + "encoding/json" + "fmt" + "strconv" +) + +var ( + // EmbeddedReferenceValuesJSON contains the embedded reference values in JSON format. + // At startup, they are unmarshaled into a globally-shared ReferenceValues struct. + // + //go:embed assets/reference-values.json + EmbeddedReferenceValuesJSON []byte + + embeddedReferenceValues *ReferenceValues +) + +// ReferenceValues contains the workload-independent reference values for each platform. +type ReferenceValues struct { + // AKS holds the reference values for AKS. + AKS *AKSReferenceValues + // BareMetalTDX holds the reference values for TDX on bare metal. + BareMetalTDX *BareMetalTDXReferenceValues +} + +// AKSReferenceValues contains reference values for AKS. +type AKSReferenceValues struct { + SNP SNPReferenceValues + TrustedMeasurement HexString +} + +// BareMetalTDXReferenceValues contains reference values for BareMetalTDX. +type BareMetalTDXReferenceValues struct { + TrustedMeasurement HexString +} + +// SNPReferenceValues contains reference values for the SNP report. +type SNPReferenceValues struct { + MinimumTCB SNPTCB +} + +// SNPTCB represents a set of SNP TCB values. +type SNPTCB struct { + BootloaderVersion *SVN + TEEVersion *SVN + SNPVersion *SVN + MicrocodeVersion *SVN +} + +// SVN is a SNP secure version number. +type SVN uint8 + +// UInt8 returns the uint8 value of the SVN. +func (s *SVN) UInt8() uint8 { + return uint8(*s) +} + +// MarshalJSON marshals the SVN to JSON. +func (s SVN) MarshalJSON() ([]byte, error) { + return []byte(strconv.Itoa(int(s))), nil +} + +// UnmarshalJSON unmarshals the SVN from a JSON. +func (s *SVN) UnmarshalJSON(data []byte) error { + var value float64 + if err := json.Unmarshal(data, &value); err != nil { + return err + } + + if value < 0 || value > 255 { // Ensure the value fits into uint8 range + return fmt.Errorf("value out of range for uint8") + } + + *s = SVN(value) + return nil +} + +// HexString is a hex encoded string. +type HexString string + +// NewHexString creates a new HexString from a byte slice. +func NewHexString(b []byte) HexString { + return HexString(hex.EncodeToString(b)) +} + +// String returns the string representation of the HexString. +func (h HexString) String() string { + return string(h) +} + +// Bytes returns the byte slice representation of the HexString. +func (h HexString) Bytes() ([]byte, error) { + return hex.DecodeString(string(h)) +} diff --git a/internal/manifest/referencevalues_test.go b/internal/manifest/referencevalues_test.go new file mode 100644 index 0000000000..9ddf56bc3f --- /dev/null +++ b/internal/manifest/referencevalues_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 Edgeless Systems GmbH +// SPDX-License-Identifier: AGPL-3.0-only + +package manifest + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSVN(t *testing.T) { + testCases := []struct { + enc string + dec SVN + wantErr bool + }{ + {enc: "0", dec: 0}, + {enc: "1", dec: 1}, + {enc: "255", dec: 255}, + {enc: "256", dec: 0, wantErr: true}, + {enc: "-1", dec: 0, wantErr: true}, + } + + t.Run("MarshalJSON", func(t *testing.T) { + for _, tc := range testCases { + if tc.wantErr { + continue + } + t.Run(tc.enc, func(t *testing.T) { + assert := assert.New(t) + enc, err := json.Marshal(tc.dec) + assert.NoError(err) + assert.Equal(tc.enc, string(enc)) + }) + } + }) + + t.Run("UnmarshalJSON", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.enc, func(t *testing.T) { + assert := assert.New(t) + var dec SVN + err := json.Unmarshal([]byte(tc.enc), &dec) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal(tc.dec, dec) + }) + } + }) +} + +func TestHexString(t *testing.T) { + testCases := []struct { + b []byte + s string + }{ + {b: []byte{0x00}, s: "00"}, + {b: []byte{0x01}, s: "01"}, + {b: []byte{0x0f}, s: "0f"}, + {b: []byte{0x10}, s: "10"}, + {b: []byte{0x11}, s: "11"}, + {b: []byte{0xff}, s: "ff"}, + {b: []byte{0x00, 0x01}, s: "0001"}, + } + + t.Run("Bytes", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.s, func(t *testing.T) { + assert := assert.New(t) + hexString := NewHexString(tc.b) + assert.Equal(tc.s, hexString.String()) + b, err := hexString.Bytes() + assert.NoError(err) + assert.Equal(tc.b, b) + }) + } + }) + + t.Run("MarshalJSON", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.s, func(t *testing.T) { + assert := assert.New(t) + hexString := NewHexString(tc.b) + enc, err := json.Marshal(hexString) + assert.NoError(err) + assert.Equal(fmt.Sprintf("\"%s\"", tc.s), string(enc)) + }) + } + }) + + t.Run("UnmarshalJSON", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.s, func(t *testing.T) { + assert := assert.New(t) + var hexString HexString + err := json.Unmarshal([]byte(fmt.Sprintf("\"%s\"", tc.s)), &hexString) + assert.NoError(err) + assert.Equal(tc.s, hexString.String()) + }) + } + }) + + t.Run("invalid hexstring", func(t *testing.T) { + assert := assert.New(t) + hexString := HexString("invalid") + _, err := hexString.Bytes() + assert.Error(err) + }) +} diff --git a/packages/by-name/contrast/package.nix b/packages/by-name/contrast/package.nix index 41498375fe..76dfae9dd2 100644 --- a/packages/by-name/contrast/package.nix +++ b/packages/by-name/contrast/package.nix @@ -6,6 +6,7 @@ buildGoModule, buildGoTest, microsoft, + kata, genpolicy ? microsoft.genpolicy, contrast, installShellFiles, @@ -25,11 +26,7 @@ let tags = [ "e2e" ]; - ldflags = [ - "-s" - "-X github.com/edgelesssys/contrast/internal/manifest.TrustedMeasurement=${launchDigest}" - "-X github.com/edgelesssys/contrast/internal/kuberesource.runtimeHandler=${runtimeHandler}" - ]; + ldflags = [ "-s" ]; subPackages = [ "e2e/genpolicy" @@ -41,10 +38,29 @@ let ]; }; - launchDigest = builtins.readFile "${microsoft.runtime-class-files}/launch-digest.hex"; - - runtimeHandler = lib.removeSuffix "\n" ( - builtins.readFile "${microsoft.runtime-class-files}/runtime-handler" + # Reference values that we embed into the Contrast CLI for + # deployment generation and attestation. + embeddedReferenceValues = builtins.toFile "reference-values.json" ( + builtins.toJSON { + aks = { + snp = { + minimumTCB = { + bootloaderVersion = 3; + teeVersion = 0; + snpVersion = 8; + microcodeVersion = 115; + }; + }; + trustedMeasurement = lib.removeSuffix "\n" ( + builtins.readFile "${microsoft.runtime-class-files}/launch-digest.hex" + ); + }; + bareMetalTDX = { + trustedMeasurement = lib.removeSuffix "\n" ( + builtins.readFile "${kata.runtime-class-files}/launch-digest.hex" + ); + }; + } ); packageOutputs = [ @@ -75,6 +91,7 @@ buildGoModule rec { (path.append root "cli/cmd/assets/image-replacements.txt") (path.append root "internal/attestation/snp/Milan.pem") (path.append root "internal/attestation/snp/Genoa.pem") + (path.append root "internal/manifest/assets/reference-values.json") (path.append root "node-installer") (fileset.difference (fileset.fileFilter (file: hasSuffix ".go" file.name) root) ( path.append root "service-mesh" @@ -93,6 +110,7 @@ buildGoModule rec { install -D ${lib.getExe genpolicy} cli/cmd/assets/genpolicy install -D ${genpolicy.settings-dev}/genpolicy-settings.json cli/cmd/assets/genpolicy-settings.json install -D ${genpolicy.rules}/genpolicy-rules.rego cli/cmd/assets/genpolicy-rules.rego + install -D ${embeddedReferenceValues} internal/manifest/assets/reference-values.json ''; CGO_ENABLED = 0; @@ -101,8 +119,6 @@ buildGoModule rec { "-w" "-X main.version=v${version}" "-X main.genpolicyVersion=${genpolicy.version}" - "-X github.com/edgelesssys/contrast/internal/manifest.TrustedMeasurement=${launchDigest}" - "-X github.com/edgelesssys/contrast/internal/kuberesource.runtimeHandler=${runtimeHandler}" ]; preCheck = ''