From ea4e8621f86a54f4b40934d528118f0e027d3f27 Mon Sep 17 00:00:00 2001 From: Jernej Kos <jernej@kos.mx> Date: Thu, 12 Dec 2024 15:04:20 +0100 Subject: [PATCH] feat(cmd/rofl): Add TDX container build support --- build/rofl/manifest.go | 218 +++++++++++++++++++ build/rofl/manifest_test.go | 161 ++++++++++++++ cmd/rofl/build/artifacts.go | 116 ++++++++--- cmd/rofl/build/build.go | 80 +++++++ cmd/rofl/build/container.go | 132 ++++++++++++ cmd/rofl/build/sgx.go | 25 ++- cmd/rofl/build/tdx.go | 405 +++++++++++++++++++++++++----------- cmd/rofl/common/identity.go | 57 +++++ cmd/rofl/common/manifest.go | 68 ++++++ cmd/rofl/identity.go | 55 +---- cmd/rofl/mgmt.go | 75 +++++-- 11 files changed, 1172 insertions(+), 220 deletions(-) create mode 100644 build/rofl/manifest.go create mode 100644 build/rofl/manifest_test.go create mode 100644 cmd/rofl/build/container.go create mode 100644 cmd/rofl/common/identity.go create mode 100644 cmd/rofl/common/manifest.go diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go new file mode 100644 index 00000000..5b890f83 --- /dev/null +++ b/build/rofl/manifest.go @@ -0,0 +1,218 @@ +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"` + // Admin is the identifier of the admin account. + Admin string `yaml:"admin,omitempty" json:"admin,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"` + // Hash is the consensus layer block header hash corresponding to the passed height. + Hash string `yaml:"hash,omitempty" json:"hash,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..bb341c13 100644 --- a/cmd/rofl/build/artifacts.go +++ b/cmd/rofl/build/artifacts.go @@ -4,6 +4,7 @@ import ( "archive/tar" "compress/bzip2" "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" @@ -25,24 +26,32 @@ import ( const artifactCacheDir = "build_cache" // maybeDownloadArtifact downloads the given artifact and optionally verifies its integrity against -// the provided hash. -func maybeDownloadArtifact(kind, uri, knownHash string) string { +// the hash provided in the URI fragment. +func maybeDownloadArtifact(kind, uri string) string { fmt.Printf("Downloading %s artifact...\n", kind) fmt.Printf(" URI: %s\n", uri) - if knownHash != "" { - fmt.Printf(" Hash: %s\n", knownHash) - } url, err := url.Parse(uri) if err != nil { 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 } + // If the URI contains a fragment and the known hash is empty, treat it as a known hash. + var knownHash string + if url.Fragment != "" { + knownHash = url.Fragment + } + if knownHash != "" { + fmt.Printf(" Hash: %s\n", knownHash) + } + // TODO: Prune cache. cacheHash := hash.NewFromBytes([]byte(uri)).Hex() cacheFn, err := xdg.CacheFile(filepath.Join("oasis", artifactCacheDir, cacheHash)) @@ -192,6 +201,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) @@ -204,8 +218,16 @@ func copyFile(src, dst string, mode os.FileMode) error { } defer df.Close() - _, err = io.Copy(df, sf) - return err + if _, err = io.Copy(df, sf); err != nil { + return fmt.Errorf("failed to copy '%s': %w", src, err) + } + + // Ensure times are constant for deterministic builds. + if err = extractChtimes(dst, time.Time{}, time.Time{}); err != nil { + return fmt.Errorf("failed to change atime/mtime for '%s': %w", dst, err) + } + + return nil } // computeDirSize computes the size of the given directory. @@ -237,35 +259,33 @@ func ensureBinaryExists(name, pkg string) error { return nil } -// createExt4Fs creates an ext4 filesystem in the given file using directory dir to populate it. +// createSquashFs creates a squashfs filesystem in the given file using directory dir to populate +// it. // // Returns the size of the created filesystem image in bytes. -func createExt4Fs(fn, dir string) (int64, error) { - const mkfsExt4Bin = "mkfs.ext4" - if err := ensureBinaryExists(mkfsExt4Bin, "e2fsprogs"); err != nil { +func createSquashFs(fn, dir string) (int64, error) { + const mkSquashFsBin = "mksquashfs" + if err := ensureBinaryExists(mkSquashFsBin, "squashfs-tools"); err != nil { return 0, err } - // Compute filesystem size in bytes. - fsSize, err := computeDirSize(dir) - if err != nil { - return 0, err - } - fsSize /= 1024 // Convert to kilobytes. - fsSize = (fsSize * 150) / 100 // Scale by overhead factor of 1.5. - // Execute mkfs.ext4. cmd := exec.Command( //nolint:gosec - mkfsExt4Bin, - "-E", "root_owner=0:0", - "-d", dir, + mkSquashFsBin, + dir, fn, - fmt.Sprintf("%dK", fsSize), + "-comp", "gzip", + "-noappend", + "-mkfs-time", "1234", + "-all-time", "1234", + "-root-time", "1234", + "-all-root", + "-reproducible", ) var out strings.Builder cmd.Stderr = &out cmd.Stdout = &out - if err = cmd.Run(); err != nil { + if err := cmd.Run(); err != nil { return 0, fmt.Errorf("%w\n%s", err, out.String()) } @@ -285,12 +305,26 @@ func createVerityHashTree(fsFn, hashFn string) (string, error) { return "", err } + // Generate a deterministic salt by hashing the filesystem. + f, err := os.Open(fsFn) + if err != nil { + return "", fmt.Errorf("failed to open filesystem file: %w", err) + } + defer f.Close() + h := sha256.New() + if _, err = io.Copy(h, f); err != nil { + return "", fmt.Errorf("failed to read filesystem file: %w", err) + } + salt := h.Sum([]byte{}) + rootHashFn := hashFn + ".roothash" cmd := exec.Command( //nolint:gosec veritysetupBin, "format", "--data-block-size=4096", "--hash-block-size=4096", + "--uuid=00000000-0000-0000-0000-000000000000", + "--salt="+hex.EncodeToString(salt), "--root-hash-file="+rootHashFn, fsFn, hashFn, @@ -326,3 +360,35 @@ func concatFiles(a, b string) error { _, err = io.Copy(df, sf) return err } + +// appendEmptySpace appends empty space to the given file. If the filesystem supports sparse files, +// this should not actually take any extra space. +// +// The function ensures that the given space respects alignment by adding padding as needed. +// +// Returns the offset where the empty space starts. +func appendEmptySpace(fn string, size uint64, align uint64) (uint64, error) { + f, err := os.OpenFile(fn, os.O_RDWR, 0644) + if err != nil { + return 0, err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return 0, err + } + offset := uint64(fi.Size()) + + // Ensure proper alignment. + if size % align != 0 { + return 0, fmt.Errorf("size is not properly aligned") + } + offset += (align - (offset % align)) % align + + if err = f.Truncate(int64(offset + size)); err != nil { + return 0, err + } + + return offset, nil +} diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index 466497e8..0ecfb4b4 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -2,14 +2,19 @@ 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" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + buildRofl "github.com/oasisprotocol/cli/build/rofl" "github.com/oasisprotocol/cli/cmd/common" ) @@ -54,6 +59,81 @@ func detectBuildMode(npa *common.NPASelection) { } } +func setupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) { + if manifest == nil { + return + } + + // Configure app ID. + os.Setenv("ROFL_APP_ID", manifest.AppID) + + // Obtain and configure trust root. + trustRoot, err := fetchTrustRoot(npa, manifest.TrustRoot) + cobra.CheckErr(err) + os.Setenv("ROFL_CONSENSUS_TRUST_ROOT", trustRoot) +} + +// 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) { + var ( + height int64 + hash string + ) + switch { + case cfg == nil || cfg.Hash == "": + // Hash is not known, we need to fetch it if not in offline mode. + if offline { + return "", fmt.Errorf("trust root hash not available in manifest while in offline mode") + } + + // Establish connection with the target network. + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + if err != nil { + return "", err + } + + switch cfg { + case nil: + // Use latest height. + height, err = common.GetActualHeight(ctx, conn.Consensus()) + if err != nil { + return "", err + } + default: + // Use configured height. + height = int64(cfg.Height) + } + + blk, err := conn.Consensus().GetBlock(ctx, height) + if err != nil { + return "", err + } + hash = blk.Hash.Hex() + default: + // Hash is known, just use it. + height = int64(cfg.Height) + hash = cfg.Hash + } + + // 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(height), + Hash: hash, + RuntimeID: npa.ParaTime.Namespace(), + ChainContext: npa.Network.ChainContext, + } + encRoot := cbor.Marshal(root) + return base64.StdEncoding.EncodeToString(encRoot), nil +} + func init() { globalFlags := flag.NewFlagSet("", flag.ContinueOnError) globalFlags.StringVar(&buildMode, "mode", "auto", "build mode [production, unsafe, auto]") diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go new file mode 100644 index 00000000..33d5bde5 --- /dev/null +++ b/cmd/rofl/build/container.go @@ -0,0 +1,132 @@ +package build + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/oasisprotocol/oasis-core/go/common/version" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle" + + "github.com/oasisprotocol/cli/cmd/common" + roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" + cliConfig "github.com/oasisprotocol/cli/config" +) + +const ( + artifactContainerRuntime = "rofl-container runtime" + artifactContainerCompose = "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 + tdxContainerComposeURI 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 := roflCommon.LoadManifestAndSetNPA(cfg, npa) + + wantedArtifacts := tdxGetDefaultArtifacts() + wantedArtifacts = append(wantedArtifacts, + &artifact{ + kind: artifactContainerRuntime, + uri: tdxContainerRuntimeURI, + }, + &artifact{ + kind: artifactContainerCompose, + uri: tdxContainerComposeURI, + }, + ) + tdxOverrideArtifacts(manifest, wantedArtifacts) + 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(), + }, + } + var err error + bnd.Manifest.Version, err = version.FromString(manifest.Version) + if err != nil { + cobra.CheckErr(fmt.Errorf("unsupported package version format: %w", err)) + } + + fmt.Printf("App ID: %s\n", manifest.AppID) + 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/compose.yaml", + }) + 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) + if err != nil { + _ = os.RemoveAll(stage2.tmpDir) + cobra.CheckErr(err) + } + extraKernelOpts = append(extraKernelOpts, + fmt.Sprintf("ROFL_CONSENSUS_TRUST_ROOT=%s", trustRoot), + ) + + fmt.Println("Creating ORC bundle...") + + outFn, err := tdxBundleComponent(manifest, artifacts, bnd, stage2, extraKernelOpts) + if err != nil { + _ = os.RemoveAll(stage2.tmpDir) + cobra.CheckErr(err) + } + + fmt.Println("Computing enclave identity...") + + eids, err := roflCommon.ComputeEnclaveIdentity(bnd, "") + cobra.CheckErr(err) + + fmt.Println("Update the manifest with the following identities to use the new app:") + fmt.Println() + for _, enclaveID := range eids { + data, _ := enclaveID.MarshalText() + fmt.Printf("- \"%s\"\n", string(data)) + } + fmt.Println() + + fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + }, + } +) + +func init() { + tdxContainerFlags := flag.NewFlagSet("", flag.ContinueOnError) + tdxContainerFlags.StringVar(&tdxContainerRuntimeURI, "runtime", defaultContainerRuntimeURI, "URL or path to runtime binary") + tdxContainerFlags.StringVar(&tdxContainerComposeURI, "compose", "compose.yaml", "URL or path to compose.yaml") + + tdxContainerCmd.Flags().AddFlagSet(tdxContainerFlags) +} diff --git a/cmd/rofl/build/sgx.go b/cmd/rofl/build/sgx.go index 21251ab2..bf27045f 100644 --- a/cmd/rofl/build/sgx.go +++ b/cmd/rofl/build/sgx.go @@ -20,8 +20,10 @@ import ( "github.com/oasisprotocol/oasis-core/go/runtime/bundle/component" "github.com/oasisprotocol/cli/build/cargo" + buildRofl "github.com/oasisprotocol/cli/build/rofl" "github.com/oasisprotocol/cli/build/sgxs" "github.com/oasisprotocol/cli/cmd/common" + roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -37,6 +39,7 @@ var ( Run: func(_ *cobra.Command, _ []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) + manifest, _ := roflCommon.MaybeLoadManifestAndSetNPA(cfg, npa) if npa.ParaTime == nil { cobra.CheckErr("no ParaTime selected") @@ -47,7 +50,7 @@ var ( fmt.Println("Building an SGX-based Rust ROFL application...") detectBuildMode(npa) - features := sgxSetupBuildEnv() + features := sgxSetupBuildEnv(manifest, npa) // Obtain package metadata. pkgMeta, err := cargo.GetMetadata() @@ -58,11 +61,21 @@ var ( // Start creating the bundle early so we can fail before building anything. bnd := &bundle.Bundle{ Manifest: &bundle.Manifest{ - Name: pkgMeta.Name, - ID: npa.ParaTime.Namespace(), + ID: npa.ParaTime.Namespace(), }, } - bnd.Manifest.Version, err = version.FromString(pkgMeta.Version) + var rawVersion string + switch manifest { + case nil: + // No ROFL app manifest, use Cargo manifest. + bnd.Manifest.Name = pkgMeta.Name + rawVersion = pkgMeta.Version + default: + // Use ROFL app manifest. + bnd.Manifest.Name = manifest.Name + rawVersion = manifest.Version + } + bnd.Manifest.Version, err = version.FromString(rawVersion) if err != nil { cobra.CheckErr(fmt.Errorf("unsupported package version format: %w", err)) } @@ -259,7 +272,9 @@ NextSetOfPrimes: } // sgxSetupBuildEnv sets up the SGX build environment and returns the list of features to enable. -func sgxSetupBuildEnv() []string { +func sgxSetupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) []string { + setupBuildEnv(manifest, npa) + switch buildMode { case buildModeProduction, buildModeAuto: // Production builds. diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 1c2aaf43..50330288 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -14,7 +14,9 @@ import ( "github.com/oasisprotocol/oasis-core/go/runtime/bundle/component" "github.com/oasisprotocol/cli/build/cargo" + buildRofl "github.com/oasisprotocol/cli/build/rofl" "github.com/oasisprotocol/cli/cmd/common" + roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -24,28 +26,22 @@ const ( artifactKernel = "kernel" artifactStage2 = "stage 2 template" - defaultFirmwareURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.2.0/ovmf.tdx.fd" - defaultKernelURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.2.0/stage1.bin" - defaultStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.2.0/stage2-basic.tar.bz2" + defaultFirmwareURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.2.0/ovmf.tdx.fd#db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f" + defaultKernelURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.2.0/stage1.bin#0c4a74af5e3860e1b9c79b38aff9de8c59aa92f14da715fbfd04a9362ee4cd59" + defaultStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.2.0/stage2-basic.tar.bz2#8cbc67e4a05b01e6fc257a3ef378db50ec230bc4c7aacbfb9abf0f5b17dcb8fd" ) -var knownHashes = map[string]string{ - defaultFirmwareURI: "db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f", - defaultKernelURI: "0c4a74af5e3860e1b9c79b38aff9de8c59aa92f14da715fbfd04a9362ee4cd59", - defaultStage2TemplateURI: "8cbc67e4a05b01e6fc257a3ef378db50ec230bc4c7aacbfb9abf0f5b17dcb8fd", -} - var ( - tdxFirmwareURI string - tdxFirmwareHash string - tdxKernelURI string - tdxKernelHash string - tdxStage2TemplateURI string - tdxStage2TemplateHash string + tdxFirmwareURI string + tdxKernelURI string + tdxStage2TemplateURI string tdxResourcesMemory uint64 tdxResourcesCPUCount uint8 + tdxTmpStorageMode string + tdxTmpStorageSize uint64 + tdxCmd = &cobra.Command{ Use: "tdx", Short: "Build a TDX-based ROFL application", @@ -53,34 +49,20 @@ var ( Run: func(_ *cobra.Command, _ []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) + manifest, _ := roflCommon.MaybeLoadManifestAndSetNPA(cfg, npa) if npa.ParaTime == nil { 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) - } + wantedArtifacts := tdxGetDefaultArtifacts() + tdxOverrideArtifacts(manifest, wantedArtifacts) + artifacts := tdxFetchArtifacts(wantedArtifacts) fmt.Println("Building a TDX-based Rust ROFL application...") detectBuildMode(npa) - tdxSetupBuildEnv() + tdxSetupBuildEnv(manifest, npa) // Obtain package metadata. pkgMeta, err := cargo.GetMetadata() @@ -102,11 +84,21 @@ var ( // Start creating the bundle early so we can fail before building anything. bnd := &bundle.Bundle{ Manifest: &bundle.Manifest{ - Name: pkgMeta.Name, - ID: npa.ParaTime.Namespace(), + ID: npa.ParaTime.Namespace(), }, } - bnd.Manifest.Version, err = version.FromString(pkgMeta.Version) + var rawVersion string + switch manifest { + case nil: + // No ROFL app manifest, use Cargo manifest. + bnd.Manifest.Name = pkgMeta.Name + rawVersion = pkgMeta.Version + default: + // Use ROFL app manifest. + bnd.Manifest.Name = manifest.Name + rawVersion = manifest.Version + } + bnd.Manifest.Version, err = version.FromString(rawVersion) if err != nil { cobra.CheckErr(fmt.Errorf("unsupported package version format: %w", err)) } @@ -120,108 +112,262 @@ 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(manifest, 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 +} - // 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}, + {artifactKernel, tdxKernelURI}, + {artifactStage2, tdxStage2TemplateURI}, + } +} - // 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 { + result[ar.kind] = maybeDownloadArtifact(ar.kind, ar.uri) + } + return result +} - fmt.Println("Creating ORC bundle...") +// tdxOverrideArtifacts overrides artifacts based on the manifest. +func tdxOverrideArtifacts(manifest *buildRofl.Manifest, artifacts []*artifact) { + if manifest == nil || manifest.Artifacts == nil { + return + } + overrides := manifest.Artifacts + + for _, artifact := range artifacts { + var overrideURI string + switch artifact.kind { + case artifactFirmware: + overrideURI = overrides.Firmware + case artifactKernel: + overrideURI = overrides.Kernel + case artifactStage2: + overrideURI = overrides.Stage2 + case artifactContainerRuntime: + overrideURI = overrides.Container.Runtime + case artifactContainerCompose: + overrideURI = overrides.Container.Compose + default: + } - // 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) + if overrideURI == "" { + continue + } + artifact.uri = overrideURI + } +} - if err = bnd.Manifest.Validate(); err != nil { - cobra.CheckErr(fmt.Errorf("failed to validate manifest: %w", err)) - } +type tdxStage2 struct { + tmpDir string + fn string + rootHash string + fsSize int64 +} - // 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)) - } +// tdxPrepareStage2 prepares the stage 2 rootfs. +func tdxPrepareStage2(artifacts map[string]string, initPath string, extraFiles map[string]string) (*tdxStage2, error) { + var ok bool - // 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)) - } + // 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) + } + }() - fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + 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) + } + + // 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 the root filesystem. + fmt.Println("Creating squashfs filesystem...") + rootfsImage := filepath.Join(tmpDir, "rootfs.squashfs") + rootfsSize, err := createSquashFs(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( + manifest *buildRofl.Manifest, + 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, + }, }, } -) + + // When manifest is available, override values from manifest. + if manifest != nil { + switch { + case manifest.Resources.EphemeralStorage == nil: + tdxTmpStorageMode = buildRofl.EphemeralStorageKindNone + default: + tdxTmpStorageMode = manifest.Resources.EphemeralStorage.Kind + tdxTmpStorageSize = manifest.Resources.EphemeralStorage.Size + } + } + + switch tdxTmpStorageMode { + case buildRofl.EphemeralStorageKindNone: + case buildRofl.EphemeralStorageKindRAM: + comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, + "oasis.stage2.storage_mode=ram", + fmt.Sprintf("oasis.stage2.storage_size=%d", tdxTmpStorageSize*1024*1024), + ) + case buildRofl.EphemeralStorageKindDisk: + // Allocate some space after regular stage2. + const sectorSize = 512 + storageSize := tdxTmpStorageSize * 1024 * 1024 + storageOffset, err := appendEmptySpace(stage2.fn, storageSize, sectorSize) + if err != nil { + return "", err + } + + comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, + "oasis.stage2.storage_mode=disk", + fmt.Sprintf("oasis.stage2.storage_size=%d", storageSize / sectorSize), + fmt.Sprintf("oasis.stage2.storage_offset=%d", storageOffset / sectorSize), + ) + default: + return "", fmt.Errorf("unsupported ephemeral storage mode: %s", tdxTmpStorageMode) + } + + // TODO: (Oasis Core 25.0+) Use qcow2 image format to support sparse files. + + // 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() { +func tdxSetupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) { + setupBuildEnv(manifest, npa) + switch buildMode { case buildModeProduction, buildModeAuto: // Production builds. @@ -250,15 +396,28 @@ func tdxSetupBuildEnv() { func init() { 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") - tdxFlags.StringVar(&tdxKernelHash, "kernel-hash", "", "optional SHA256 hash of kernel image") tdxFlags.StringVar(&tdxStage2TemplateURI, "template", defaultStage2TemplateURI, "URL or path to stage 2 template") - tdxFlags.StringVar(&tdxStage2TemplateHash, "template-hash", "", "optional SHA256 hash of stage 2 template") 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/common/identity.go b/cmd/rofl/common/identity.go new file mode 100644 index 00000000..792e43ba --- /dev/null +++ b/cmd/rofl/common/identity.go @@ -0,0 +1,57 @@ +package common + +import ( + "fmt" + + "github.com/oasisprotocol/oasis-core/go/common/sgx" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle/component" + + "github.com/oasisprotocol/cli/build/measurement" +) + +// ComputeEnclaveIdentity computes the enclave identity of the given ROFL components. If no specific +// component ID is passed, it uses the first ROFL component. +func ComputeEnclaveIdentity(bnd *bundle.Bundle, compID string) ([]*sgx.EnclaveIdentity, error) { + var cid component.ID + if compID != "" { + if err := cid.UnmarshalText([]byte(compID)); err != nil { + return nil, fmt.Errorf("malformed component ID: %w", err) + } + } + + for _, comp := range bnd.Manifest.GetAvailableComponents() { + if comp.Kind != component.ROFL { + continue // Skip non-ROFL components. + } + switch compID { + case "": + // When not specified we use the first ROFL app. + default: + if !comp.Matches(cid) { + continue + } + } + + switch teeKind := comp.TEEKind(); teeKind { + case component.TEEKindSGX: + var enclaveID *sgx.EnclaveIdentity + enclaveID, err := bnd.EnclaveIdentity(comp.ID()) + if err != nil { + return nil, err + } + return []*sgx.EnclaveIdentity{enclaveID}, nil + case component.TEEKindTDX: + return measurement.MeasureTdxQemu(bnd, comp) + default: + return nil, fmt.Errorf("identity computation for TEE kind '%s' not supported", teeKind) + } + } + + switch compID { + case "": + return nil, fmt.Errorf("no ROFL apps found in bundle") + default: + return nil, fmt.Errorf("ROFL app '%s' not found in bundle", compID) + } +} diff --git a/cmd/rofl/common/manifest.go b/cmd/rofl/common/manifest.go new file mode 100644 index 00000000..9ae2323a --- /dev/null +++ b/cmd/rofl/common/manifest.go @@ -0,0 +1,68 @@ +package common + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/build/rofl" + "github.com/oasisprotocol/cli/cmd/common" + "github.com/oasisprotocol/cli/config" +) + +// LoadManifestAndSetNPA loads the ROFL app manifest and reconfigures the network/paratime/account +// selection. +// +// In case there is an error in loading the manifest, it aborts the application. +func LoadManifestAndSetNPA(cfg *config.Config, npa *common.NPASelection) *rofl.Manifest { + manifest, err := MaybeLoadManifestAndSetNPA(cfg, npa) + cobra.CheckErr(err) + return manifest +} + +// MaybeLoadManifestAndSetNPA loads the ROFL app manifest and reconfigures the +// network/paratime/account selection. +// +// In case there is an error in loading the manifest, it is returned. +func MaybeLoadManifestAndSetNPA(cfg *config.Config, npa *common.NPASelection) (*rofl.Manifest, error) { + manifest, err := rofl.LoadManifest() + if err != nil { + return nil, err + } + + switch manifest.Network { + case "": + if npa.Network == nil { + return nil, fmt.Errorf("no network selected") + } + default: + npa.Network = cfg.Networks.All[manifest.Network] + if npa.Network == nil { + return nil, fmt.Errorf("network '%s' does not exist", manifest.Network) + } + npa.NetworkName = manifest.Network + } + switch manifest.ParaTime { + case "": + if npa.ParaTime == nil { + return nil, fmt.Errorf("no ParaTime selected") + } + default: + npa.ParaTime = npa.Network.ParaTimes.All[manifest.ParaTime] + if npa.ParaTime == nil { + return nil, fmt.Errorf("paratime '%s' does not exist", manifest.ParaTime) + } + npa.ParaTimeName = manifest.ParaTime + } + switch manifest.Admin { + case "": + default: + accCfg, err := common.LoadAccountConfig(cfg, manifest.Admin) + if err != nil { + return nil, err + } + npa.Account = accCfg + npa.AccountName = manifest.Admin + } + return manifest, nil +} diff --git a/cmd/rofl/identity.go b/cmd/rofl/identity.go index ad9fbbd1..a45835c0 100644 --- a/cmd/rofl/identity.go +++ b/cmd/rofl/identity.go @@ -6,11 +6,9 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" - "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/runtime/bundle" - "github.com/oasisprotocol/oasis-core/go/runtime/bundle/component" - "github.com/oasisprotocol/cli/build/measurement" + roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" ) var ( @@ -29,53 +27,12 @@ var ( cobra.CheckErr(fmt.Errorf("failed to open bundle: %w", err)) } - var cid component.ID - if compID != "" { - if err = cid.UnmarshalText([]byte(compID)); err != nil { - cobra.CheckErr(fmt.Errorf("malformed component ID: %w", err)) - } - } - - for _, comp := range bnd.Manifest.GetAvailableComponents() { - if comp.Kind != component.ROFL { - continue // Skip non-ROFL components. - } - switch compID { - case "": - // When not specified we use the first ROFL app. - default: - if !comp.Matches(cid) { - continue - } - } - - var eids []*sgx.EnclaveIdentity - switch teeKind := comp.TEEKind(); teeKind { - case component.TEEKindSGX: - var enclaveID *sgx.EnclaveIdentity - enclaveID, err = bnd.EnclaveIdentity(comp.ID()) - eids = append(eids, enclaveID) - case component.TEEKindTDX: - eids, err = measurement.MeasureTdxQemu(bnd, comp) - default: - cobra.CheckErr(fmt.Errorf("identity computation for TEE kind '%s' not supported", teeKind)) - } - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to generate enclave identity of '%s': %w", comp.ID(), err)) - } - - for _, enclaveID := range eids { - data, _ := enclaveID.MarshalText() - fmt.Println(string(data)) - } - return - } + eids, err := roflCommon.ComputeEnclaveIdentity(bnd, compID) + cobra.CheckErr(err) - switch compID { - case "": - cobra.CheckErr("no ROFL apps found in bundle") - default: - cobra.CheckErr(fmt.Errorf("ROFL app '%s' not found in bundle", compID)) + for _, enclaveID := range eids { + data, _ := enclaveID.MarshalText() + fmt.Println(string(data)) } }, } diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 79bbc8b5..d2d41ac8 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -16,6 +16,7 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl" "github.com/oasisprotocol/cli/cmd/common" + roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -30,14 +31,21 @@ var ( adminAddress string createCmd = &cobra.Command{ - Use: "create <policy.yml>", + Use: "create [<policy.yml>]", Short: "Create a new ROFL application", - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) txCfg := common.GetTransactionConfig() - policyFn = args[0] + + var policy *rofl.AppAuthPolicy + if len(args) > 0 { + policy = loadPolicy(args[0]) + } else { + manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) + policy = manifest.Policy + } if npa.Account == nil { cobra.CheckErr("no accounts configured in your wallet") @@ -46,8 +54,6 @@ var ( cobra.CheckErr("no ParaTime selected") } - policy := loadPolicy(policyFn) - // When not in offline mode, connect to the given network endpoint. ctx := context.Background() var conn connection.Connection @@ -82,14 +88,30 @@ var ( } updateCmd = &cobra.Command{ - Use: "update <app-id> --policy <policy.yml> --admin <address>", + Use: "update [<app-id> --policy <policy.yml> --admin <address>]", Short: "Update an existing ROFL application", - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) txCfg := common.GetTransactionConfig() - rawAppID := args[0] + + var ( + rawAppID string + policy *rofl.AppAuthPolicy + ) + if len(args) > 0 { + rawAppID = args[0] + policy = loadPolicy(policyFn) + } else { + manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) + rawAppID = manifest.AppID + + if adminAddress == "" && manifest.Admin != "" { + adminAddress = "self" + } + policy = manifest.Policy + } var appID rofl.AppID if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) @@ -102,8 +124,12 @@ var ( cobra.CheckErr("no ParaTime selected") } - if policyFn == "" || adminAddress == "" { - fmt.Println("You must specify both --policy and --admin.") + if adminAddress == "" { + fmt.Println("You must specify --admin or configure an admin in the manifest.") + return + } + if policy == nil { + fmt.Println("You must specify --policy or configure policy in the manifest.") return } @@ -118,7 +144,7 @@ var ( updateBody := rofl.Update{ ID: appID, - Policy: *loadPolicy(policyFn), + Policy: *policy, } // Update administrator address. @@ -143,14 +169,21 @@ var ( } removeCmd = &cobra.Command{ - Use: "remove <app-id>", + Use: "remove [<app-id>]", Short: "Remove an existing ROFL application", - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) txCfg := common.GetTransactionConfig() - rawAppID := args[0] + + var rawAppID string + if len(args) > 0 { + rawAppID = args[0] + } else { + manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) + rawAppID = manifest.AppID + } var appID rofl.AppID if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) @@ -186,13 +219,20 @@ var ( } showCmd = &cobra.Command{ - Use: "show <app-id>", + Use: "show [<app-id>]", Short: "Show information about a ROFL application", - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) - rawAppID := args[0] + + var rawAppID string + if len(args) > 0 { + rawAppID = args[0] + } else { + manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) + rawAppID = manifest.AppID + } var appID rofl.AppID if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) @@ -240,7 +280,6 @@ var ( ) func loadPolicy(fn string) *rofl.AppAuthPolicy { - // Load app policy. rawPolicy, err := os.ReadFile(fn) cobra.CheckErr(err)