From 6f0a4e1b44415790796341ecc1e3db14036af2b5 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Thu, 12 Dec 2024 15:04:20 +0100 Subject: [PATCH] feat(cmd/rofl): Add TDX container build support --- build/rofl/manifest.go | 214 +++++++++++++++++++++++++ build/rofl/manifest_test.go | 161 +++++++++++++++++++ cmd/rofl/build/artifacts.go | 9 +- cmd/rofl/build/container.go | 190 ++++++++++++++++++++++ cmd/rofl/build/tdx.go | 310 ++++++++++++++++++++++++------------ cmd/rofl/mgmt.go | 39 +++-- 6 files changed, 809 insertions(+), 114 deletions(-) create mode 100644 build/rofl/manifest.go create mode 100644 build/rofl/manifest_test.go create mode 100644 cmd/rofl/build/container.go diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go new file mode 100644 index 00000000..4800eb9e --- /dev/null +++ b/build/rofl/manifest.go @@ -0,0 +1,214 @@ +package rofl + +import ( + "errors" + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/oasisprotocol/oasis-core/go/common/version" + + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl" +) + +// ManifestFileNames are the manifest file names that are tried when loading the manifest. +var ManifestFileNames = []string{ + "rofl.yml", + "rofl.yaml", +} + +// Supported ROFL app kinds. +const ( + AppKindRaw = "raw" + AppKindContainer = "container" +) + +// Supported TEE types. +const ( + TEETypeSGX = "sgx" + TEETypeTDX = "tdx" +) + +// Manifest is the ROFL app manifest that configures various aspects of the app in a single place. +type Manifest struct { + // AppID is the Bech32-encoded ROFL app ID. + AppID string `yaml:"app_id" json:"app_id"` + // Name is the human readable ROFL app name. + Name string `yaml:"name" json:"name"` + // Version is the ROFL app version. + Version string `yaml:"version" json:"version"` + // Network is the identifier of the network to deploy to by default. + Network string `yaml:"network,omitempty" json:"network,omitempty"` + // ParaTime is the identifier of the paratime to deploy to by default. + ParaTime string `yaml:"paratime,omitempty" json:"paratime,omitempty"` + // TEE is the type of TEE to build for. + TEE string `yaml:"tee" json:"tee"` + // Kind is the kind of ROFL app to build. + Kind string `yaml:"kind" json:"kind"` + // TrustRoot is the optional trust root configuration. + TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"` + // Resources are the requested ROFL app resources. + Resources ResourcesConfig `yaml:"resources" json:"resources"` + // Artifacts are the optional artifact location overrides. + Artifacts *ArtifactsConfig `yaml:"artifacts,omitempty" json:"artifacts,omitempty"` + + // Policy is the ROFL app policy to deploy by default. + Policy *rofl.AppAuthPolicy `yaml:"policy,omitempty" json:"policy,omitempty"` +} + +// LoadManifest attempts to find and load the ROFL app manifest from a local file. +func LoadManifest() (*Manifest, error) { + for _, fn := range ManifestFileNames { + f, err := os.Open(fn) + switch { + case err == nil: + case errors.Is(err, os.ErrNotExist): + continue + default: + return nil, fmt.Errorf("failed to load manifest from '%s': %w", fn, err) + } + + var m Manifest + dec := yaml.NewDecoder(f) + if err = dec.Decode(&m); err != nil { + f.Close() + return nil, fmt.Errorf("malformed manifest '%s': %w", fn, err) + } + if err = m.Validate(); err != nil { + f.Close() + return nil, fmt.Errorf("invalid manifest '%s': %w", fn, err) + } + + f.Close() + return &m, nil + } + return nil, fmt.Errorf("no ROFL app manifest found (tried: %s)", strings.Join(ManifestFileNames, ", ")) +} + +// Validate validates the manifest for correctness. +func (m *Manifest) Validate() error { + if len(m.AppID) == 0 { + return fmt.Errorf("app ID cannot be empty") + } + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(m.AppID)); err != nil { + return fmt.Errorf("malformed app ID: %w", err) + } + + if len(m.Name) == 0 { + return fmt.Errorf("name cannot be empty") + } + + if len(m.Version) == 0 { + return fmt.Errorf("version cannot be empty") + } + if _, err := version.FromString(m.Version); err != nil { + return fmt.Errorf("malformed version: %w", err) + } + + switch m.TEE { + case TEETypeSGX, TEETypeTDX: + default: + return fmt.Errorf("unsupported TEE type: %s", m.TEE) + } + + switch m.Kind { + case AppKindRaw: + case AppKindContainer: + if m.TEE != TEETypeTDX { + return fmt.Errorf("containers are only supported under TDX") + } + default: + return fmt.Errorf("unsupported app kind: %s", m.Kind) + } + + if err := m.Resources.Validate(); err != nil { + return fmt.Errorf("bad resources config: %w", err) + } + + return nil +} + +// TrustRootConfig is the trust root configuration. +type TrustRootConfig struct { + // Height is the consensus layer block height where to take the trust root. + Height uint64 `yaml:"height,omitempty" json:"height,omitempty"` +} + +// ResourcesConfig is the resources configuration. +type ResourcesConfig struct { + // Memory is the amount of memory needed by the app in megabytes. + Memory uint64 `yaml:"memory" json:"memory"` + // CpuCount is the number of vCPUs needed by the app. + CpuCount uint8 `yaml:"cpus" json:"cpus"` + // EphemeralStorage is the ephemeral storage configuration. + EphemeralStorage *EphemeralStorageConfig `yaml:"ephemeral_storage,omitempty" json:"ephemeral_storage,omitempty"` +} + +// Validate validates the resources configuration for correctness. +func (r *ResourcesConfig) Validate() error { + if r.Memory < 16 { + return fmt.Errorf("memory size must be at least 16M") + } + if r.CpuCount < 1 { + return fmt.Errorf("vCPU count must be at least 1") + } + if r.EphemeralStorage != nil { + err := r.EphemeralStorage.Validate() + if err != nil { + return fmt.Errorf("bad ephemeral storage config: %w", err) + } + } + return nil +} + +// Supported ephemeral storage kinds. +const ( + EphemeralStorageKindNone = "none" + EphemeralStorageKindDisk = "disk" + EphemeralStorageKindRAM = "ram" +) + +// EphemeralStorageConfig is the ephemeral storage configuration. +type EphemeralStorageConfig struct { + // Kind is the storage kind. + Kind string `yaml:"kind" json:"kind"` + // Size is the amount of ephemeral storage in megabytes. + Size uint64 `yaml:"size" json:"size"` +} + +// Validate validates the ephemeral storage configuration for correctness. +func (e *EphemeralStorageConfig) Validate() error { + switch e.Kind { + case EphemeralStorageKindNone, EphemeralStorageKindDisk, EphemeralStorageKindRAM: + default: + return fmt.Errorf("unsupported ephemeral storage kind: %s", e.Kind) + } + + if e.Size < 16 { + return fmt.Errorf("ephemeral storage size must be at least 16M") + } + return nil +} + +// ArtifactsConfig is the artifact location override configuration. +type ArtifactsConfig struct { + // Firmware is the URI/path to the firmware artifact (empty to use default). + Firmware string `yaml:"firmware,omitempty" json:"firmware,omitempty"` + // Kernel is the URI/path to the kernel artifact (empty to use default). + Kernel string `yaml:"kernel,omitempty" json:"kernel,omitempty"` + // Stage2 is the URI/path to the stage 2 disk artifact (empty to use default). + Stage2 string `yaml:"stage2,omitempty" json:"stage2,omitempty"` + // Container is the container artifacts configuration. + Container ContainerArtifactsConfig `yaml:"container,omitempty" json:"container,omitempty"` +} + +// ContainerArtifactsConfig is the container artifacts configuration. +type ContainerArtifactsConfig struct { + // Runtime is the URI/path to the container runtime artifact (empty to use default). + Runtime string `yaml:"runtime,omitempty" json:"runtime,omitempty"` + // Compose is the URI/path to the docker-compose.yaml artifact (empty to use default). + Compose string `yaml:"compose,omitempty" json:"compose,omitempty"` +} diff --git a/build/rofl/manifest_test.go b/build/rofl/manifest_test.go new file mode 100644 index 00000000..3413a2ce --- /dev/null +++ b/build/rofl/manifest_test.go @@ -0,0 +1,161 @@ +package rofl + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestManifestValidation(t *testing.T) { + require := require.New(t) + + // Empty manifest is not valid. + m := Manifest{} + err := m.Validate() + require.ErrorContains(err, "app ID cannot be empty") + + // Invalid app ID. + m.AppID = "foo" + err = m.Validate() + require.ErrorContains(err, "malformed app ID") + + // Empty name. + m.AppID = "rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j" + err = m.Validate() + require.ErrorContains(err, "name cannot be empty") + + // Empty version. + m.Name = "my-simple-app" + err = m.Validate() + require.ErrorContains(err, "version cannot be empty") + + // Invalid version. + m.Version = "foo" + err = m.Validate() + require.ErrorContains(err, "malformed version") + + // Unsupported TEE type. + m.Version = "0.1.0" + err = m.Validate() + require.ErrorContains(err, "unsupported TEE type") + + // Unsupported app kind. + m.TEE = "sgx" + err = m.Validate() + require.ErrorContains(err, "unsupported app kind") + + // Containers are only supported under TDX. + m.Kind = "container" + err = m.Validate() + require.ErrorContains(err, "containers are only supported under TDX") + + // Bad resources configuration. + m.TEE = "tdx" + err = m.Validate() + require.ErrorContains(err, "bad resources config: memory size must be at least 16M") + + m.Resources.Memory = 16 + err = m.Validate() + require.ErrorContains(err, "bad resources config: vCPU count must be at least 1") + + // Finally, everything is valid. + m.Resources.CpuCount = 1 + err = m.Validate() + require.NoError(err) + + // Add ephemeral storage configuration. + m.Resources.EphemeralStorage = &EphemeralStorageConfig{} + err = m.Validate() + require.ErrorContains(err, "bad resources config: bad ephemeral storage config: unsupported ephemeral storage kind") + + m.Resources.EphemeralStorage.Kind = "ram" + err = m.Validate() + require.ErrorContains(err, "bad resources config: bad ephemeral storage config: ephemeral storage size must be at least 16M") + + m.Resources.EphemeralStorage.Size = 16 + err = m.Validate() + require.NoError(err) +} + +const serializedYamlManifest = ` +app_id: rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j +name: my-simple-app +version: 0.1.0 +tee: tdx +kind: container +resources: + memory: 16 + cpus: 1 + ephemeral_storage: + kind: ram + size: 16 +` + +func TestManifestSerialization(t *testing.T) { + require := require.New(t) + + var m Manifest + err := yaml.Unmarshal([]byte(serializedYamlManifest), &m) + require.NoError(err, "yaml.Unmarshal") + err = m.Validate() + require.NoError(err, "m.Validate") + require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) + require.Equal("my-simple-app", m.Name) + require.Equal("0.1.0", m.Version) + require.Equal("tdx", m.TEE) + require.Equal("container", m.Kind) + require.EqualValues(16, m.Resources.Memory) + require.EqualValues(1, m.Resources.CpuCount) + require.NotNil(m.Resources.EphemeralStorage) + require.Equal("ram", m.Resources.EphemeralStorage.Kind) + require.EqualValues(16, m.Resources.EphemeralStorage.Size) + + enc, err := yaml.Marshal(m) + require.NoError(err, "yaml.Marshal") + + var dec Manifest + err = yaml.Unmarshal(enc, &dec) + require.NoError(err, "yaml.Unmarshal(round-trip)") + require.EqualValues(m, dec, "serialization should round-trip") + err = dec.Validate() + require.NoError(err, "dec.Validate") +} + +func TestLoadManifest(t *testing.T) { + require := require.New(t) + + tmpDir, err := os.MkdirTemp("", "oasis-test-load-manifest") + require.NoError(err) + defer os.RemoveAll(tmpDir) + + err = os.Chdir(tmpDir) + require.NoError(err) + + _, err = LoadManifest() + require.ErrorIs(err, "no ROFL app manifest found") + + manifestFn := filepath.Join(tmpDir, "rofl.yml") + err = os.WriteFile(manifestFn, []byte("foo"), 0o644) + require.NoError(err) + _, err = LoadManifest() + require.ErrorContains(err, "malformed manifest 'rofl.yml'") + + err = os.WriteFile(manifestFn, []byte(serializedYamlManifest), 0o644) + require.NoError(err) + m, err := LoadManifest() + require.NoError(err) + require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) + + err = os.Remove(manifestFn) + require.NoError(err) + + manifestFn = "rofl.yaml" + err = os.WriteFile(manifestFn, []byte(serializedYamlManifest), 0o644) + require.NoError(err) + m, err = LoadManifest() + require.NoError(err) + require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) +} diff --git a/cmd/rofl/build/artifacts.go b/cmd/rofl/build/artifacts.go index 54228f2d..b5b3ddd5 100644 --- a/cmd/rofl/build/artifacts.go +++ b/cmd/rofl/build/artifacts.go @@ -38,8 +38,10 @@ func maybeDownloadArtifact(kind, uri, knownHash string) string { cobra.CheckErr(fmt.Errorf("failed to parse %s artifact URL: %w", kind, err)) } - // In case the URI represents a local file, just return it. + // In case the URI represents a local file, check that it exists and return it. if url.Host == "" { + _, err = os.Stat(url.Path) + cobra.CheckErr(err) return url.Path } @@ -192,6 +194,11 @@ FILES: // copyFile copies the file at path src to a file at path dst using the given mode. func copyFile(src, dst string, mode os.FileMode) error { + err := os.MkdirAll(filepath.Dir(dst), 0o755) + if err != nil { + return fmt.Errorf("failed to create destination directory for '%s': %w", dst, err) + } + sf, err := os.Open(src) if err != nil { return fmt.Errorf("failed to open '%s': %w", src, err) diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go new file mode 100644 index 00000000..a38ac95c --- /dev/null +++ b/cmd/rofl/build/container.go @@ -0,0 +1,190 @@ +package build + +import ( + "context" + "encoding/base64" + "fmt" + "os" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + coreCommon "github.com/oasisprotocol/oasis-core/go/common" + "github.com/oasisprotocol/oasis-core/go/common/cbor" + "github.com/oasisprotocol/oasis-core/go/common/version" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + + buildRofl "github.com/oasisprotocol/cli/build/rofl" + "github.com/oasisprotocol/cli/cmd/common" + cliConfig "github.com/oasisprotocol/cli/config" +) + +const ( + artifactContainerRuntime = "rofl-container runtime" + artifactContainerCompose = "docker-compose.yaml" + + defaultContainerStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage2-podman.tar.bz2" + + defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers/v0.1.0/runtime" +) + +var ( + tdxContainerRuntimeURI string + tdxContainerRuntimeHash string + tdxContainerComposeURI string + tdxContainerComposeHash string + + tdxContainerCmd = &cobra.Command{ + Use: "container", + Short: "Build a container-based TDX ROFL application", + Args: cobra.NoArgs, + Run: func(_ *cobra.Command, _ []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + + manifest, err := buildRofl.LoadManifest() + cobra.CheckErr(err) + _ = manifest + + if npa.ParaTime == nil { + switch manifest.ParaTime { + case "": + cobra.CheckErr("no ParaTime selected") + default: + npa.ParaTime = npa.Network.ParaTimes.All[manifest.ParaTime] + if npa.ParaTime == nil { + cobra.CheckErr(fmt.Errorf("paratime '%s' does not exist", manifest.ParaTime)) + } + } + } + + wantedArtifacts := tdxGetDefaultArtifacts() + wantedArtifacts = append(wantedArtifacts, + &artifact{ + kind: artifactContainerRuntime, + uri: tdxContainerRuntimeURI, + knownHash: tdxContainerRuntimeHash, + }, + &artifact{ + kind: artifactContainerCompose, + uri: tdxContainerComposeURI, + knownHash: tdxContainerComposeHash, + }, + ) + // TODO: Override artifacts from manifest. + artifacts := tdxFetchArtifacts(wantedArtifacts) + + fmt.Println("Building a container-based TDX ROFL application...") + + detectBuildMode(npa) + + // Start creating the bundle early so we can fail before building anything. + bnd := &bundle.Bundle{ + Manifest: &bundle.Manifest{ + Name: manifest.Name, + ID: npa.ParaTime.Namespace(), + }, + } + bnd.Manifest.Version, err = version.FromString(manifest.Version) + if err != nil { + cobra.CheckErr(fmt.Errorf("unsupported package version format: %w", err)) + } + + fmt.Printf("Name: %s\n", bnd.Manifest.Name) + fmt.Printf("Version: %s\n", bnd.Manifest.Version) + + // Use the pre-built container runtime. + initPath := artifacts[artifactContainerRuntime] + + stage2, err := tdxPrepareStage2(artifacts, initPath, map[string]string{ + artifacts[artifactContainerCompose]: "etc/oasis/containers/docker-compose.yaml", + }) + if err != nil { + cobra.CheckErr(err) + } + defer os.RemoveAll(stage2.tmpDir) + + // Configure app ID. + var extraKernelOpts []string + extraKernelOpts = append(extraKernelOpts, + fmt.Sprintf("ROFL_APP_ID=%s", manifest.AppID), + ) + + // Obtain and configure trust root. + trustRoot, err := fetchTrustRoot(npa, manifest.TrustRoot) + extraKernelOpts = append(extraKernelOpts, + fmt.Sprintf("ROFL_CONSENSUS_TRUST_ROOT=%s", trustRoot), + ) + + fmt.Println("Creating ORC bundle...") + + outFn, err := tdxBundleComponent(artifacts, bnd, stage2, extraKernelOpts) + if err != nil { + _ = os.RemoveAll(stage2.tmpDir) + cobra.CheckErr(err) + } + + fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + }, + } +) + +// fetchTrustRoot fetches the trust root based on configuration and returns a serialized version +// suitable for inclusion as an environment variable. +func fetchTrustRoot(npa *common.NPASelection, cfg *buildRofl.TrustRootConfig) (string, error) { + if offline { + // TODO: Use trust root from manifest in offline mode. + return "", fmt.Errorf("offline mode currently not supported") + } + + // Establish connection with the target network. + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + if err != nil { + return "", err + } + + var height int64 + switch cfg { + case nil: + // Use latest height. + height, err = common.GetActualHeight(ctx, conn.Consensus()) + if err != nil { + return "", err + } + default: + height = int64(cfg.Height) + } + + blk, err := conn.Consensus().GetBlock(ctx, height) + if err != nil { + return "", err + } + + // TODO: Move this structure to Core. + type trustRoot struct { + Height uint64 `json:"height"` + Hash string `json:"hash"` + RuntimeID coreCommon.Namespace `json:"runtime_id"` + ChainContext string `json:"chain_context"` + } + root := trustRoot{ + Height: uint64(blk.Height), + Hash: blk.Hash.Hex(), + RuntimeID: npa.ParaTime.Namespace(), + ChainContext: npa.Network.ChainContext, + } + encRoot := cbor.Marshal(root) + return base64.StdEncoding.EncodeToString(encRoot), nil +} + +func init() { + tdxContainerFlags := flag.NewFlagSet("", flag.ContinueOnError) + tdxContainerFlags.StringVar(&tdxContainerRuntimeURI, "runtime", defaultContainerRuntimeURI, "URL or path to runtime binary") + tdxContainerFlags.StringVar(&tdxContainerRuntimeHash, "runtime-hash", "", "optional SHA256 hash of runtime binary") + tdxContainerFlags.StringVar(&tdxContainerComposeURI, "compose", "docker-compose.yaml", "URL or path to docker-compose.yaml") + tdxContainerFlags.StringVar(&tdxContainerComposeHash, "compose-hash", "", "optional SHA256 hash of docker-compose.yaml") + + tdxContainerCmd.Flags().AddFlagSet(tdxContainerFlags) +} diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 1c2aaf43..03edbd98 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -35,6 +35,11 @@ var knownHashes = map[string]string{ defaultStage2TemplateURI: "8cbc67e4a05b01e6fc257a3ef378db50ec230bc4c7aacbfb9abf0f5b17dcb8fd", } +var ( + tdxFlags *flag.FlagSet + tdxStorageFlags *flag.FlagSet +) + var ( tdxFirmwareURI string tdxFirmwareHash string @@ -46,6 +51,9 @@ var ( tdxResourcesMemory uint64 tdxResourcesCPUCount uint8 + tdxTmpStorageMode string + tdxTmpStorageSize uint64 + tdxCmd = &cobra.Command{ Use: "tdx", Short: "Build a TDX-based ROFL application", @@ -58,24 +66,7 @@ var ( cobra.CheckErr("no ParaTime selected") } - // Obtain required artifacts. - artifacts := make(map[string]string) - for _, ar := range []struct { - kind string - uri string - knownHash string - }{ - {artifactFirmware, tdxFirmwareURI, tdxFirmwareHash}, - {artifactKernel, tdxKernelURI, tdxKernelHash}, - {artifactStage2, tdxStage2TemplateURI, tdxStage2TemplateHash}, - } { - // Automatically populate known hashes for known URIs. - if ar.knownHash == "" { - ar.knownHash = knownHashes[ar.uri] - } - - artifacts[ar.kind] = maybeDownloadArtifact(ar.kind, ar.uri, ar.knownHash) - } + artifacts := tdxFetchArtifacts(tdxGetDefaultArtifacts()) fmt.Println("Building a TDX-based Rust ROFL application...") @@ -120,105 +111,206 @@ var ( cobra.CheckErr(fmt.Errorf("failed to build runtime binary: %w", err)) } - // Create temporary directory and unpack stage 2 template into it. - fmt.Println("Preparing stage 2 root filesystem...") - tmpDir, err := os.MkdirTemp("", "oasis-build-stage2") + stage2, err := tdxPrepareStage2(artifacts, initPath, nil) if err != nil { - cobra.CheckErr(fmt.Errorf("failed to create temporary stage 2 build directory: %w", err)) + cobra.CheckErr(err) } - defer os.RemoveAll(tmpDir) // TODO: This doesn't work because of cobra.CheckErr + defer os.RemoveAll(stage2.tmpDir) - rootfsDir := filepath.Join(tmpDir, "rootfs") - if err = os.Mkdir(rootfsDir, 0o755); err != nil { - cobra.CheckErr(fmt.Errorf("failed to create temporary rootfs directory: %w", err)) - } + fmt.Println("Creating ORC bundle...") - // Unpack template into temporary directory. - fmt.Println("Unpacking template...") - if err = extractArchive(artifacts[artifactStage2], rootfsDir); err != nil { - cobra.CheckErr(fmt.Errorf("failed to extract stage 2 template: %w", err)) + outFn, err := tdxBundleComponent(artifacts, bnd, stage2, nil) + if err != nil { + _ = os.RemoveAll(stage2.tmpDir) + cobra.CheckErr(err) } - // Add runtime as init. - fmt.Println("Adding runtime as init...") - err = copyFile(initPath, filepath.Join(rootfsDir, "init"), 0o755) - cobra.CheckErr(err) + fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + }, + } +) - // Create an ext4 filesystem. - fmt.Println("Creating ext4 filesystem...") - rootfsImage := filepath.Join(tmpDir, "rootfs.ext4") - rootfsSize, err := createExt4Fs(rootfsImage, rootfsDir) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to create rootfs image: %w", err)) - } +type artifact struct { + kind string + uri string + knownHash string +} - // Create dm-verity hash tree. - fmt.Println("Creating dm-verity hash tree...") - hashFile := filepath.Join(tmpDir, "rootfs.hash") - rootHash, err := createVerityHashTree(rootfsImage, hashFile) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to create verity hash tree: %w", err)) - } +// tdxGetDefaultArtifacts returns the list of default TDX artifacts. +func tdxGetDefaultArtifacts() []*artifact { + return []*artifact{ + {artifactFirmware, tdxFirmwareURI, tdxFirmwareHash}, + {artifactKernel, tdxKernelURI, tdxKernelHash}, + {artifactStage2, tdxStage2TemplateURI, tdxStage2TemplateHash}, + } +} - // Concatenate filesystem and hash tree into one image. - if err = concatFiles(rootfsImage, hashFile); err != nil { - cobra.CheckErr(fmt.Errorf("failed to concatenate rootfs and hash tree files: %w", err)) - } +// tdxFetchArtifacts obtains all of the required artifacts for a TDX image. +func tdxFetchArtifacts(artifacts []*artifact) map[string]string { + result := make(map[string]string) + for _, ar := range artifacts { + // Automatically populate known hashes for known URIs. + if ar.knownHash == "" { + ar.knownHash = knownHashes[ar.uri] + } - fmt.Println("Creating ORC bundle...") + result[ar.kind] = maybeDownloadArtifact(ar.kind, ar.uri, ar.knownHash) + } + return result +} - // Add the ROFL component. - firmwareName := "firmware.fd" - kernelName := "kernel.bin" - stage2Name := "stage2.img" - - comp := bundle.Component{ - Kind: component.ROFL, - Name: pkgMeta.Name, - TDX: &bundle.TDXMetadata{ - Firmware: firmwareName, - Kernel: kernelName, - Stage2Image: stage2Name, - ExtraKernelOptions: []string{ - "console=ttyS0", - fmt.Sprintf("oasis.stage2.roothash=%s", rootHash), - fmt.Sprintf("oasis.stage2.hash_offset=%d", rootfsSize), - }, - Resources: bundle.TDXResources{ - Memory: tdxResourcesMemory, - CPUCount: tdxResourcesCPUCount, - }, - }, - } - bnd.Manifest.Components = append(bnd.Manifest.Components, &comp) +type tdxStage2 struct { + tmpDir string + fn string + rootHash string + fsSize int64 +} - if err = bnd.Manifest.Validate(); err != nil { - cobra.CheckErr(fmt.Errorf("failed to validate manifest: %w", err)) - } +// tdxPrepareStage2 prepares the stage 2 rootfs. +func tdxPrepareStage2(artifacts map[string]string, initPath string, extraFiles map[string]string) (*tdxStage2, error) { + var ok bool - // Add all files. - fileMap := map[string]string{ - firmwareName: artifacts[artifactFirmware], - kernelName: artifacts[artifactKernel], - stage2Name: rootfsImage, - } - for dst, src := range fileMap { - _ = bnd.Add(dst, bundle.NewFileData(src)) - } + // Create temporary directory and unpack stage 2 template into it. + fmt.Println("Preparing stage 2 root filesystem...") + tmpDir, err := os.MkdirTemp("", "oasis-build-stage2") + if err != nil { + return nil, fmt.Errorf("failed to create temporary stage 2 build directory: %w", err) + } + defer func() { + // Ensure temporary directory is removed on errors. + if !ok { + _ = os.RemoveAll(tmpDir) + } + }() - // Write the bundle out. - outFn := fmt.Sprintf("%s.orc", bnd.Manifest.Name) - if outputFn != "" { - outFn = outputFn - } - if err = bnd.Write(outFn); err != nil { - cobra.CheckErr(fmt.Errorf("failed to write output bundle: %w", err)) - } + rootfsDir := filepath.Join(tmpDir, "rootfs") + if err = os.Mkdir(rootfsDir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create temporary rootfs directory: %w", err) + } - fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + // Unpack template into temporary directory. + fmt.Println("Unpacking template...") + if err = extractArchive(artifacts[artifactStage2], rootfsDir); err != nil { + return nil, fmt.Errorf("failed to extract stage 2 template: %w", err) + } + + // Add runtime as init. + fmt.Println("Adding runtime as init...") + if err = copyFile(initPath, filepath.Join(rootfsDir, "init"), 0o755); err != nil { + return nil, err + } + + // Copy any extra files. + fmt.Println("Adding extra files...") + for src, dst := range extraFiles { + if err = copyFile(src, filepath.Join(rootfsDir, dst), 0o644); err != nil { + return nil, err + } + } + + // Create an ext4 filesystem. + fmt.Println("Creating ext4 filesystem...") + rootfsImage := filepath.Join(tmpDir, "rootfs.ext4") + rootfsSize, err := createExt4Fs(rootfsImage, rootfsDir) + if err != nil { + return nil, fmt.Errorf("failed to create rootfs image: %w", err) + } + + // Create dm-verity hash tree. + fmt.Println("Creating dm-verity hash tree...") + hashFile := filepath.Join(tmpDir, "rootfs.hash") + rootHash, err := createVerityHashTree(rootfsImage, hashFile) + if err != nil { + return nil, fmt.Errorf("failed to create verity hash tree: %w", err) + } + + // Concatenate filesystem and hash tree into one image. + if err = concatFiles(rootfsImage, hashFile); err != nil { + return nil, fmt.Errorf("failed to concatenate rootfs and hash tree files: %w", err) + } + + ok = true + + return &tdxStage2{ + tmpDir: tmpDir, + fn: rootfsImage, + rootHash: rootHash, + fsSize: rootfsSize, + }, nil +} + +// tdxBundleComponent adds the ROFL component to the given bundle. +func tdxBundleComponent(artifacts map[string]string, bnd *bundle.Bundle, stage2 *tdxStage2, extraKernelOpts []string) (string, error) { + // Add the ROFL component. + firmwareName := "firmware.fd" + kernelName := "kernel.bin" + stage2Name := "stage2.img" + + comp := bundle.Component{ + Kind: component.ROFL, + Name: bnd.Manifest.Name, + TDX: &bundle.TDXMetadata{ + Firmware: firmwareName, + Kernel: kernelName, + Stage2Image: stage2Name, + ExtraKernelOptions: []string{ + "console=ttyS0", + fmt.Sprintf("oasis.stage2.roothash=%s", stage2.rootHash), + fmt.Sprintf("oasis.stage2.hash_offset=%d", stage2.fsSize), + }, + Resources: bundle.TDXResources{ + Memory: tdxResourcesMemory, + CPUCount: tdxResourcesCPUCount, + }, }, } -) + + switch tdxTmpStorageMode { + case "none": + case "ram": + comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, + fmt.Sprintf("oasis.stage2.storage_mode=ram"), + fmt.Sprintf("oasis.stage2.storage_size=%d", tdxTmpStorageSize*1024*1024), + ) + case "disk": + comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, + fmt.Sprintf("oasis.stage2.storage_mode=disk"), + fmt.Sprintf("oasis.stage2.storage_size=%d", tdxTmpStorageSize*1024*1024), + fmt.Sprintf("oasis.stage2.storage_offset=%d", 0), // TODO + ) + default: + return "", fmt.Errorf("unsupported ephemeral storage mode: %s", tdxTmpStorageMode) + } + + // Add extra kernel options. + comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, extraKernelOpts...) + + bnd.Manifest.Components = append(bnd.Manifest.Components, &comp) + + if err := bnd.Manifest.Validate(); err != nil { + return "", fmt.Errorf("failed to validate manifest: %w", err) + } + + // Add all files. + fileMap := map[string]string{ + firmwareName: artifacts[artifactFirmware], + kernelName: artifacts[artifactKernel], + stage2Name: stage2.fn, + } + for dst, src := range fileMap { + _ = bnd.Add(dst, bundle.NewFileData(src)) + } + + // Write the bundle out. + outFn := fmt.Sprintf("%s.orc", bnd.Manifest.Name) + if outputFn != "" { + outFn = outputFn + } + if err := bnd.Write(outFn); err != nil { + return "", fmt.Errorf("failed to write output bundle: %w", err) + } + return outFn, nil +} // tdxSetupBuildEnv sets up the TDX build environment. func tdxSetupBuildEnv() { @@ -248,7 +340,7 @@ func tdxSetupBuildEnv() { } func init() { - tdxFlags := flag.NewFlagSet("", flag.ContinueOnError) + tdxFlags = flag.NewFlagSet("", flag.ContinueOnError) tdxFlags.StringVar(&tdxFirmwareURI, "firmware", defaultFirmwareURI, "URL or path to firmware image") tdxFlags.StringVar(&tdxFirmwareHash, "firmware-hash", "", "optional SHA256 hash of firmware image") tdxFlags.StringVar(&tdxKernelURI, "kernel", defaultKernelURI, "URL or path to kernel image") @@ -259,6 +351,22 @@ func init() { tdxFlags.Uint64Var(&tdxResourcesMemory, "memory", 512, "required amount of VM memory in megabytes") tdxFlags.Uint8Var(&tdxResourcesCPUCount, "cpus", 1, "required number of vCPUs") + tdxFlags.StringVar(&tdxTmpStorageMode, "ephemeral-storage-mode", "none", "ephemeral storage mode") + tdxFlags.Uint64Var(&tdxTmpStorageSize, "ephemeral-storage-size", 64, "ephemeral storage size in megabytes") + tdxCmd.Flags().AddFlagSet(common.SelectorNPFlags) tdxCmd.Flags().AddFlagSet(tdxFlags) + + // XXX: We need to define the flags here due to init order (container gets called before tdx). + tdxContainerCmd.Flags().AddFlagSet(common.SelectorNPFlags) + tdxContainerCmd.Flags().AddFlagSet(tdxFlags) + + // Override some flag defaults. + flags := tdxContainerCmd.Flags() + flags.Lookup("template").DefValue = defaultContainerStage2TemplateURI + flags.Lookup("template").Value.Set(defaultContainerStage2TemplateURI) + flags.Lookup("ephemeral-storage-mode").DefValue = "ram" + flags.Lookup("ephemeral-storage-mode").Value.Set("ram") + + tdxCmd.AddCommand(tdxContainerCmd) } diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 79bbc8b5..f19b93ce 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -15,6 +15,7 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl" + buildRofl "github.com/oasisprotocol/cli/build/rofl" "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -30,14 +31,13 @@ var ( adminAddress string createCmd = &cobra.Command{ - Use: "create ", + Use: "create []", Short: "Create a new ROFL application", - Args: cobra.ExactArgs(1), + Args: cobra.RangeArgs(0, 1), Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) txCfg := common.GetTransactionConfig() - policyFn = args[0] if npa.Account == nil { cobra.CheckErr("no accounts configured in your wallet") @@ -46,6 +46,10 @@ var ( cobra.CheckErr("no ParaTime selected") } + var policyFn string + if len(args) > 0 { + policyFn = args[0] + } policy := loadPolicy(policyFn) // When not in offline mode, connect to the given network endpoint. @@ -240,16 +244,27 @@ var ( ) func loadPolicy(fn string) *rofl.AppAuthPolicy { - // Load app policy. - rawPolicy, err := os.ReadFile(fn) - cobra.CheckErr(err) - - // Parse policy. - var policy rofl.AppAuthPolicy - if err = yaml.Unmarshal(rawPolicy, &policy); err != nil { - cobra.CheckErr(fmt.Errorf("malformed ROFL app policy: %w", err)) + if len(fn) > 0 { + // When policy file is passed, load from file. + rawPolicy, err := os.ReadFile(fn) + cobra.CheckErr(err) + + // Parse policy. + var policy rofl.AppAuthPolicy + if err = yaml.Unmarshal(rawPolicy, &policy); err != nil { + cobra.CheckErr(fmt.Errorf("malformed ROFL app policy: %w", err)) + } + return &policy + } else { + // Otherwise, try to load from ROFL app manifest. + manifest, err := buildRofl.LoadManifest() + cobra.CheckErr(err) + + if manifest.Policy == nil { + cobra.CheckErr("no policy found in ROFL app manifest") + } + return manifest.Policy } - return &policy } func init() {