From 98a2b65244b3b7accaa18e7a6a27053330186b8b Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Thu, 12 Dec 2024 15:04:20 +0100 Subject: [PATCH 01/20] feat(cmd/rofl): Add TDX container build support --- build/rofl/manifest.go | 218 +++++++++++++++++++ build/rofl/manifest_test.go | 161 ++++++++++++++ cmd/rofl/build/artifacts.go | 135 ++++++++---- cmd/rofl/build/build.go | 80 +++++++ cmd/rofl/build/container.go | 132 ++++++++++++ cmd/rofl/build/sgx.go | 25 ++- cmd/rofl/build/tdx.go | 408 +++++++++++++++++++++++++----------- 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, 1174 insertions(+), 240 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..7d2360bc --- /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..86d3d21b --- /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.ErrorContains(err, "no ROFL app manifest found") + + manifestFn := filepath.Join(tmpDir, "rofl.yml") + err = os.WriteFile(manifestFn, []byte("foo"), 0o600) + require.NoError(err) + _, err = LoadManifest() + require.ErrorContains(err, "malformed manifest 'rofl.yml'") + + err = os.WriteFile(manifestFn, []byte(serializedYamlManifest), 0o600) + 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), 0o600) + 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..26cc19c4 100644 --- a/cmd/rofl/build/artifacts.go +++ b/cmd/rofl/build/artifacts.go @@ -4,10 +4,10 @@ import ( "archive/tar" "compress/bzip2" "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" - "io/fs" "net/http" "net/url" "os" @@ -25,24 +25,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 +200,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,28 +217,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) + } -// computeDirSize computes the size of the given directory. -func computeDirSize(path string) (int64, error) { - var size int64 - err := filepath.WalkDir(path, func(_ string, d fs.DirEntry, derr error) error { - if derr != nil { - return derr - } - fi, err := d.Info() - if err != nil { - return err - } - size += fi.Size() - return nil - }) - if err != nil { - return 0, 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 size, nil + + return nil } // ensureBinaryExists checks whether the given binary name exists in path and returns a nice error @@ -237,35 +238,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 +284,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, @@ -298,7 +311,7 @@ func createVerityHashTree(fsFn, hashFn string) (string, error) { var out strings.Builder cmd.Stderr = &out cmd.Stdout = &out - if err := cmd.Run(); err != nil { + if err = cmd.Run(); err != nil { return "", fmt.Errorf("%w\n%s", err, out.String()) } @@ -326,3 +339,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, 0o644) + 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..662e3968 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,265 @@ 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" + + // When manifest is available, override values from manifest. + if manifest != nil { + tdxResourcesMemory = manifest.Resources.Memory + tdxResourcesCPUCount = manifest.Resources.CPUCount + + switch { + case manifest.Resources.EphemeralStorage == nil: + tdxTmpStorageMode = buildRofl.EphemeralStorageKindNone + default: + tdxTmpStorageMode = manifest.Resources.EphemeralStorage.Kind + tdxTmpStorageSize = manifest.Resources.EphemeralStorage.Size + } + } + + 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 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 +399,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 ", + Use: "create []", 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 --policy --admin
", + Use: "update [ --policy --admin
]", 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 ", + Use: "remove []", 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 ", + Use: "show []", 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) From 56bf57c35b33717f38bdcf6f0a6ae1d0222cfeed Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Wed, 8 Jan 2025 14:03:37 +0100 Subject: [PATCH 02/20] feat(cmd/rofl): Unify build command and always use manifest --- cmd/rofl/build/artifacts.go | 4 +- cmd/rofl/build/build.go | 96 ++++++++- cmd/rofl/build/container.go | 156 +++++--------- cmd/rofl/build/sgx.go | 262 ++++++++--------------- cmd/rofl/build/tdx.go | 240 ++++++--------------- docs/rofl.md | 37 +--- examples/rofl/build-sgx-unsafe.in.static | 1 - examples/rofl/build-sgx.in.static | 1 - 8 files changed, 301 insertions(+), 496 deletions(-) delete mode 100644 examples/rofl/build-sgx-unsafe.in.static delete mode 100644 examples/rofl/build-sgx.in.static diff --git a/cmd/rofl/build/artifacts.go b/cmd/rofl/build/artifacts.go index 26cc19c4..e2c2f4fd 100644 --- a/cmd/rofl/build/artifacts.go +++ b/cmd/rofl/build/artifacts.go @@ -248,8 +248,8 @@ func createSquashFs(fn, dir string) (int64, error) { return 0, err } - // Execute mkfs.ext4. - cmd := exec.Command( //nolint:gosec + // Execute mksquashfs. + cmd := exec.Command( mkSquashFsBin, dir, fn, diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index 0ecfb4b4..340666e3 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -11,11 +11,15 @@ import ( coreCommon "github.com/oasisprotocol/oasis-core/go/common" "github.com/oasisprotocol/oasis-core/go/common/cbor" + "github.com/oasisprotocol/oasis-core/go/common/version" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" + "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" + roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" + cliConfig "github.com/oasisprotocol/cli/config" ) // Build modes. @@ -33,6 +37,92 @@ var ( Cmd = &cobra.Command{ Use: "build", Short: "Build a ROFL application", + Args: cobra.NoArgs, + Run: func(_ *cobra.Command, _ []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) + + fmt.Println("Building a ROFL application...") + fmt.Printf("App ID: %s\n", manifest.AppID) + fmt.Printf("Name: %s\n", manifest.Name) + fmt.Printf("Version: %s\n", manifest.Version) + fmt.Printf("TEE: %s\n", manifest.TEE) + fmt.Printf("Kind: %s\n", manifest.Kind) + + // Prepare temporary build directory. + tmpDir, err := os.MkdirTemp("", "oasis-build") + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to create temporary build directory: %w", err)) + } + defer os.RemoveAll(tmpDir) + + bnd := &bundle.Bundle{ + Manifest: &bundle.Manifest{ + Name: manifest.AppID, + ID: npa.ParaTime.Namespace(), + }, + } + bnd.Manifest.Version, err = version.FromString(manifest.Version) + if err != nil { + fmt.Printf("unsupported package version format: %s\n", err) + return + } + + switch manifest.TEE { + case buildRofl.TEETypeSGX: + // SGX. + if manifest.Kind != buildRofl.AppKindRaw { + fmt.Printf("unsupported app kind for SGX TEE: %s\n", manifest.Kind) + return + } + + sgxBuild(npa, manifest, bnd) + case buildRofl.TEETypeTDX: + // TDX. + switch manifest.Kind { + case buildRofl.AppKindRaw: + err = tdxBuildRaw(tmpDir, npa, manifest, bnd) + case buildRofl.AppKindContainer: + err = tdxBuildContainer(tmpDir, npa, manifest, bnd) + } + default: + fmt.Printf("unsupported TEE kind: %s\n", manifest.TEE) + return + } + if err != nil { + fmt.Printf("%s\n", err) + return + } + + // Write the bundle out. + outFn := fmt.Sprintf("%s.orc", manifest.Name) + if outputFn != "" { + outFn = outputFn + } + if err = bnd.Write(outFn); err != nil { + fmt.Printf("failed to write output bundle: %s\n", err) + return + } + + fmt.Println("Computing enclave identity...") + + eids, err := roflCommon.ComputeEnclaveIdentity(bnd, "") + if err != nil { + fmt.Printf("%s\n", err) + return + } + + 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) + }, } ) @@ -60,10 +150,6 @@ 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) @@ -141,6 +227,4 @@ func init() { globalFlags.StringVar(&outputFn, "output", "", "output bundle filename") Cmd.PersistentFlags().AddFlagSet(globalFlags) - Cmd.AddCommand(sgxCmd) - Cmd.AddCommand(tdxCmd) } diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go index 33d5bde5..1bb9c535 100644 --- a/cmd/rofl/build/container.go +++ b/cmd/rofl/build/container.go @@ -2,17 +2,11 @@ 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" + 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" ) const ( @@ -22,111 +16,57 @@ const ( 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" + + defaultContainerComposeURI = "compose.yaml" ) -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) +// tdxBuildContainer builds a TDX-based container ROFL app. +func tdxBuildContainer(tmpDir string, npa *common.NPASelection, manifest *buildRofl.Manifest, bnd *bundle.Bundle) error { + fmt.Println("Building a container-based TDX ROFL application...") + + tdxStage2TemplateURI = defaultContainerStage2TemplateURI + + wantedArtifacts := tdxGetDefaultArtifacts() + wantedArtifacts = append(wantedArtifacts, + &artifact{ + kind: artifactContainerRuntime, + uri: defaultContainerRuntimeURI, + }, + &artifact{ + kind: artifactContainerCompose, + uri: defaultContainerComposeURI, }, + ) + tdxOverrideArtifacts(manifest, wantedArtifacts) + artifacts := tdxFetchArtifacts(wantedArtifacts) + detectBuildMode(npa) + + // Use the pre-built container runtime. + initPath := artifacts[artifactContainerRuntime] + + stage2, err := tdxPrepareStage2(tmpDir, artifacts, initPath, map[string]string{ + artifacts[artifactContainerCompose]: "etc/oasis/containers/compose.yaml", + }) + if err != nil { + return err } -) -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") + // 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 { + return err + } + extraKernelOpts = append(extraKernelOpts, + fmt.Sprintf("ROFL_CONSENSUS_TRUST_ROOT=%s", trustRoot), + ) + + fmt.Println("Creating ORC bundle...") - tdxContainerCmd.Flags().AddFlagSet(tdxContainerFlags) + return tdxBundleComponent(manifest, artifacts, bnd, stage2, extraKernelOpts) } diff --git a/cmd/rofl/build/sgx.go b/cmd/rofl/build/sgx.go index bf27045f..f4ab719a 100644 --- a/cmd/rofl/build/sgx.go +++ b/cmd/rofl/build/sgx.go @@ -11,11 +11,9 @@ import ( "time" "github.com/spf13/cobra" - flag "github.com/spf13/pflag" "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/common/sgx/sigstruct" - "github.com/oasisprotocol/oasis-core/go/common/version" "github.com/oasisprotocol/oasis-core/go/runtime/bundle" "github.com/oasisprotocol/oasis-core/go/runtime/bundle/component" @@ -23,175 +21,114 @@ import ( 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" ) -var ( - sgxHeapSize uint64 - sgxStackSize uint64 - sgxThreads uint64 - - sgxCmd = &cobra.Command{ - Use: "sgx", - Short: "Build an SGX-based Rust ROFL application", - Args: cobra.NoArgs, - 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") - } - - // For SGX we currently only support Rust applications. +// sgxBuild builds an SGX-based "raw" ROFL app. +func sgxBuild(npa *common.NPASelection, manifest *buildRofl.Manifest, bnd *bundle.Bundle) { + fmt.Println("Building an SGX-based Rust ROFL application...") - fmt.Println("Building an SGX-based Rust ROFL application...") + detectBuildMode(npa) + features := sgxSetupBuildEnv(manifest, npa) - detectBuildMode(npa) - features := sgxSetupBuildEnv(manifest, npa) - - // Obtain package metadata. - pkgMeta, err := cargo.GetMetadata() - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to obtain package metadata: %w", err)) - } - - // Start creating the bundle early so we can fail before building anything. - bnd := &bundle.Bundle{ - Manifest: &bundle.Manifest{ - ID: npa.ParaTime.Namespace(), - }, - } - 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)) - } - - fmt.Printf("Name: %s\n", bnd.Manifest.Name) - fmt.Printf("Version: %s\n", bnd.Manifest.Version) - - // First build for the default target. - fmt.Println("Building ELF binary...") - elfPath, err := cargo.Build(true, "x86_64-unknown-linux-gnu", features) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to build ELF binary: %w", err)) - } - - // Then build for the SGX target. - fmt.Println("Building SGXS binary...") - elfSgxPath, err := cargo.Build(true, "x86_64-fortanix-unknown-sgx", nil) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to build SGXS binary: %w", err)) - } + // First build for the default target. + fmt.Println("Building ELF binary...") + elfPath, err := cargo.Build(true, "x86_64-unknown-linux-gnu", features) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to build ELF binary: %w", err)) + } - sgxsPath := fmt.Sprintf("%s.sgxs", elfSgxPath) - err = sgxs.Elf2Sgxs(elfSgxPath, sgxsPath, sgxHeapSize, sgxStackSize, sgxThreads) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to generate SGXS binary: %w", err)) - } + // Then build for the SGX target. + fmt.Println("Building SGXS binary...") + elfSgxPath, err := cargo.Build(true, "x86_64-fortanix-unknown-sgx", nil) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to build SGXS binary: %w", err)) + } - // Compute MRENCLAVE. - var b []byte - if b, err = os.ReadFile(sgxsPath); err != nil { - cobra.CheckErr(fmt.Errorf("failed to read SGXS binary: %w", err)) - } - var enclaveHash sgx.MrEnclave - if err = enclaveHash.FromSgxsBytes(b); err != nil { - cobra.CheckErr(fmt.Errorf("failed to compute MRENCLAVE for SGXS binary: %w", err)) - } + sgxThreads := uint64(32) + sgxHeapSize := manifest.Resources.Memory * 1024 * 1024 + sgxStackSize := uint64(2 * 1024 * 1024) - fmt.Println("Creating ORC bundle...") + sgxsPath := fmt.Sprintf("%s.sgxs", elfSgxPath) + err = sgxs.Elf2Sgxs(elfSgxPath, sgxsPath, sgxHeapSize, sgxStackSize, sgxThreads) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to generate SGXS binary: %w", err)) + } - // Create a random 3072-bit RSA signer and prepare SIGSTRUCT. - // TODO: Support a specific signer to be set. - sigKey, err := sgxGenerateKey(rand.Reader) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to generate signer key: %w", err)) - } - sigStruct := sigstruct.New( - sigstruct.WithBuildDate(time.Now()), - sigstruct.WithSwDefined([4]byte{0, 0, 0, 0}), - sigstruct.WithISVProdID(0), - sigstruct.WithISVSVN(0), - - sigstruct.WithMiscSelect(0), - sigstruct.WithMiscSelectMask(^uint32(0)), - - sigstruct.WithAttributes(sgx.Attributes{ - Flags: sgx.AttributeMode64Bit, - Xfrm: 3, - }), - sigstruct.WithAttributesMask([2]uint64{ - ^uint64(2), - ^uint64(3), - }), - - sigstruct.WithEnclaveHash(enclaveHash), - ) - sigData, err := sigStruct.Sign(sigKey) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to sign SIGSTRUCT: %w", err)) - } + // Compute MRENCLAVE. + var b []byte + if b, err = os.ReadFile(sgxsPath); err != nil { + cobra.CheckErr(fmt.Errorf("failed to read SGXS binary: %w", err)) + } + var enclaveHash sgx.MrEnclave + if err = enclaveHash.FromSgxsBytes(b); err != nil { + cobra.CheckErr(fmt.Errorf("failed to compute MRENCLAVE for SGXS binary: %w", err)) + } - // Add the ROFL component. - execName := "app.elf" - sgxsName := "app.sgxs" - sigName := "app.sig" - - comp := bundle.Component{ - Kind: component.ROFL, - Name: pkgMeta.Name, - Executable: execName, - SGX: &bundle.SGXMetadata{ - Executable: sgxsName, - Signature: sigName, - }, - } - bnd.Manifest.Components = append(bnd.Manifest.Components, &comp) + fmt.Println("Creating ORC bundle...") - if err = bnd.Manifest.Validate(); err != nil { - cobra.CheckErr(fmt.Errorf("failed to validate manifest: %w", err)) - } + // Create a random 3072-bit RSA signer and prepare SIGSTRUCT. + sigKey, err := sgxGenerateKey(rand.Reader) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to generate signer key: %w", err)) + } + sigStruct := sigstruct.New( + sigstruct.WithBuildDate(time.Now()), + sigstruct.WithSwDefined([4]byte{0, 0, 0, 0}), + sigstruct.WithISVProdID(0), + sigstruct.WithISVSVN(0), + + sigstruct.WithMiscSelect(0), + sigstruct.WithMiscSelectMask(^uint32(0)), + + sigstruct.WithAttributes(sgx.Attributes{ + Flags: sgx.AttributeMode64Bit, + Xfrm: 3, + }), + sigstruct.WithAttributesMask([2]uint64{ + ^uint64(2), + ^uint64(3), + }), + + sigstruct.WithEnclaveHash(enclaveHash), + ) + sigData, err := sigStruct.Sign(sigKey) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to sign SIGSTRUCT: %w", err)) + } - // Add all files. - fileMap := map[string]string{ - execName: elfPath, - sgxsName: sgxsPath, - } - for dst, src := range fileMap { - if b, err = os.ReadFile(src); err != nil { - cobra.CheckErr(fmt.Errorf("failed to load asset '%s': %w", src, err)) - } - _ = bnd.Add(dst, bundle.NewBytesData(b)) - } - _ = bnd.Add(sigName, bundle.NewBytesData(sigData)) + // Add the ROFL component. + execName := "app.elf" + sgxsName := "app.sgxs" + sigName := "app.sig" + + comp := bundle.Component{ + Kind: component.ROFL, + Name: bnd.Manifest.Name, + Executable: execName, + SGX: &bundle.SGXMetadata{ + Executable: sgxsName, + Signature: sigName, + }, + } + bnd.Manifest.Components = append(bnd.Manifest.Components, &comp) - // 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)) - } + if err = bnd.Manifest.Validate(); err != nil { + cobra.CheckErr(fmt.Errorf("failed to validate manifest: %w", err)) + } - fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) - }, + // Add all files. + fileMap := map[string]string{ + execName: elfPath, + sgxsName: sgxsPath, } -) + for dst, src := range fileMap { + if b, err = os.ReadFile(src); err != nil { + cobra.CheckErr(fmt.Errorf("failed to load asset '%s': %w", src, err)) + } + _ = bnd.Add(dst, bundle.NewBytesData(b)) + } + _ = bnd.Add(sigName, bundle.NewBytesData(sigData)) +} // sgxGenerateKey generates a 3072-bit RSA key with public exponent 3 as required for SGX. // @@ -296,6 +233,7 @@ func sgxSetupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) [] os.Setenv("OASIS_UNSAFE_SKIP_AVR_VERIFY", "1") os.Setenv("OASIS_UNSAFE_ALLOW_DEBUG_ENCLAVES", "1") os.Setenv("OASIS_UNSAFE_MOCK_SGX", "1") + os.Setenv("OASIS_UNSAFE_MOCK_TEE", "1") os.Unsetenv("OASIS_UNSAFE_SKIP_KM_POLICY") return []string{"debug-mock-sgx"} @@ -304,13 +242,3 @@ func sgxSetupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) [] return nil } } - -func init() { - sgxFlags := flag.NewFlagSet("", flag.ContinueOnError) - sgxFlags.Uint64Var(&sgxHeapSize, "sgx-heap-size", 512*1024*1024, "SGX enclave heap size") - sgxFlags.Uint64Var(&sgxStackSize, "sgx-stack-size", 2*1024*1024, "SGX enclave stack size") - sgxFlags.Uint64Var(&sgxThreads, "sgx-threads", 32, "SGX enclave maximum number of threads") - - sgxCmd.Flags().AddFlagSet(common.SelectorNPFlags) - sgxCmd.Flags().AddFlagSet(sgxFlags) -} diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 662e3968..77abc32a 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -7,17 +7,13 @@ import ( "strings" "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/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" ) // TODO: Replace these URIs with a better mechanism for managing releases. @@ -32,104 +28,54 @@ const ( ) var ( - 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", - Args: cobra.NoArgs, - 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") - } + tdxFirmwareURI = defaultFirmwareURI + tdxKernelURI = defaultKernelURI + tdxStage2TemplateURI = defaultStage2TemplateURI +) - wantedArtifacts := tdxGetDefaultArtifacts() - tdxOverrideArtifacts(manifest, wantedArtifacts) - artifacts := tdxFetchArtifacts(wantedArtifacts) +// tdxBuildRaw builds a TDX-based "raw" ROFL app. +func tdxBuildRaw(tmpDir string, npa *common.NPASelection, manifest *buildRofl.Manifest, bnd *bundle.Bundle) error { + wantedArtifacts := tdxGetDefaultArtifacts() + tdxOverrideArtifacts(manifest, wantedArtifacts) + artifacts := tdxFetchArtifacts(wantedArtifacts) - fmt.Println("Building a TDX-based Rust ROFL application...") + fmt.Println("Building a TDX-based Rust ROFL application...") - detectBuildMode(npa) - tdxSetupBuildEnv(manifest, npa) + detectBuildMode(npa) + tdxSetupBuildEnv(manifest, npa) - // Obtain package metadata. - pkgMeta, err := cargo.GetMetadata() - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to obtain package metadata: %w", err)) - } - // Quickly validate whether the SDK has been built for TDX. - dep := pkgMeta.FindDependency("oasis-runtime-sdk") - switch dep { - case nil: - fmt.Println("WARNING: No oasis-runtime-sdk dependency found. Skipping validation of TDX binary.") - default: - // Check for presence of TDX feature. - if !dep.HasFeature("tdx") { - cobra.CheckErr(fmt.Errorf("runtime does not use the 'tdx' feature for oasis-runtime-sdk")) - } - } - - // Start creating the bundle early so we can fail before building anything. - bnd := &bundle.Bundle{ - Manifest: &bundle.Manifest{ - ID: npa.ParaTime.Namespace(), - }, - } - 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)) - } - - fmt.Printf("Name: %s\n", bnd.Manifest.Name) - fmt.Printf("Version: %s\n", bnd.Manifest.Version) - - fmt.Println("Building runtime binary...") - initPath, err := cargo.Build(true, "", nil) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to build runtime binary: %w", err)) - } + // Obtain package metadata. + pkgMeta, err := cargo.GetMetadata() + if err != nil { + return fmt.Errorf("failed to obtain package metadata: %w", err) + } + // Quickly validate whether the SDK has been built for TDX. + dep := pkgMeta.FindDependency("oasis-runtime-sdk") + switch dep { + case nil: + fmt.Println("WARNING: No oasis-runtime-sdk dependency found. Skipping validation of TDX binary.") + default: + // Check for presence of TDX feature. + if !dep.HasFeature("tdx") { + return fmt.Errorf("runtime does not use the 'tdx' feature for oasis-runtime-sdk") + } + } - stage2, err := tdxPrepareStage2(artifacts, initPath, nil) - if err != nil { - cobra.CheckErr(err) - } - defer os.RemoveAll(stage2.tmpDir) + fmt.Println("Building runtime binary...") + initPath, err := cargo.Build(true, "", nil) + if err != nil { + return fmt.Errorf("failed to build runtime binary: %w", err) + } - fmt.Println("Creating ORC bundle...") + stage2, err := tdxPrepareStage2(tmpDir, artifacts, initPath, nil) + if err != nil { + return err + } - outFn, err := tdxBundleComponent(manifest, artifacts, bnd, stage2, nil) - if err != nil { - _ = os.RemoveAll(stage2.tmpDir) - cobra.CheckErr(err) - } + fmt.Println("Creating ORC bundle...") - fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) - }, - } -) + return tdxBundleComponent(manifest, artifacts, bnd, stage2, nil) +} type artifact struct { kind string @@ -185,50 +131,36 @@ func tdxOverrideArtifacts(manifest *buildRofl.Manifest, artifacts []*artifact) { } type tdxStage2 struct { - tmpDir string fn string rootHash string fsSize int64 } // tdxPrepareStage2 prepares the stage 2 rootfs. -func tdxPrepareStage2(artifacts map[string]string, initPath string, extraFiles map[string]string) (*tdxStage2, error) { - var ok bool - +func tdxPrepareStage2(tmpDir string, artifacts map[string]string, initPath string, extraFiles map[string]string) (*tdxStage2, error) { // 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) - } - }() - rootfsDir := filepath.Join(tmpDir, "rootfs") - if err = os.Mkdir(rootfsDir, 0o755); err != nil { + 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 { + 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 { + 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 { + if err := copyFile(src, filepath.Join(rootfsDir, dst), 0o644); err != nil { return nil, err } } @@ -254,10 +186,7 @@ func tdxPrepareStage2(artifacts map[string]string, initPath string, extraFiles m 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, @@ -271,26 +200,12 @@ func tdxBundleComponent( bnd *bundle.Bundle, stage2 *tdxStage2, extraKernelOpts []string, -) (string, error) { +) error { // Add the ROFL component. firmwareName := "firmware.fd" kernelName := "kernel.bin" stage2Name := "stage2.img" - // When manifest is available, override values from manifest. - if manifest != nil { - tdxResourcesMemory = manifest.Resources.Memory - tdxResourcesCPUCount = manifest.Resources.CPUCount - - switch { - case manifest.Resources.EphemeralStorage == nil: - tdxTmpStorageMode = buildRofl.EphemeralStorageKindNone - default: - tdxTmpStorageMode = manifest.Resources.EphemeralStorage.Kind - tdxTmpStorageSize = manifest.Resources.EphemeralStorage.Size - } - } - comp := bundle.Component{ Kind: component.ROFL, Name: bnd.Manifest.Name, @@ -304,26 +219,31 @@ func tdxBundleComponent( fmt.Sprintf("oasis.stage2.hash_offset=%d", stage2.fsSize), }, Resources: bundle.TDXResources{ - Memory: tdxResourcesMemory, - CPUCount: tdxResourcesCPUCount, + Memory: manifest.Resources.Memory, + CPUCount: manifest.Resources.CPUCount, }, }, } - switch tdxTmpStorageMode { + tmpStorageKind := buildRofl.EphemeralStorageKindNone + if manifest.Resources.EphemeralStorage != nil { + tmpStorageKind = manifest.Resources.EphemeralStorage.Kind + } + + switch tmpStorageKind { 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), + fmt.Sprintf("oasis.stage2.storage_size=%d", manifest.Resources.EphemeralStorage.Size*1024*1024), ) case buildRofl.EphemeralStorageKindDisk: // Allocate some space after regular stage2. const sectorSize = 512 - storageSize := tdxTmpStorageSize * 1024 * 1024 + storageSize := manifest.Resources.EphemeralStorage.Size * 1024 * 1024 storageOffset, err := appendEmptySpace(stage2.fn, storageSize, sectorSize) if err != nil { - return "", err + return err } comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, @@ -332,7 +252,7 @@ func tdxBundleComponent( fmt.Sprintf("oasis.stage2.storage_offset=%d", storageOffset/sectorSize), ) default: - return "", fmt.Errorf("unsupported ephemeral storage mode: %s", tdxTmpStorageMode) + return fmt.Errorf("unsupported ephemeral storage mode: %s", tmpStorageKind) } // TODO: (Oasis Core 25.0+) Use qcow2 image format to support sparse files. @@ -343,7 +263,7 @@ func tdxBundleComponent( bnd.Manifest.Components = append(bnd.Manifest.Components, &comp) if err := bnd.Manifest.Validate(); err != nil { - return "", fmt.Errorf("failed to validate manifest: %w", err) + return fmt.Errorf("failed to validate manifest: %w", err) } // Add all files. @@ -356,15 +276,7 @@ func tdxBundleComponent( _ = 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 + return nil } // tdxSetupBuildEnv sets up the TDX build environment. @@ -384,43 +296,15 @@ func tdxSetupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) { } case buildModeUnsafe: // Unsafe debug builds. - fmt.Println("WARNING: Building in UNSAFE DEBUG mode with MOCK SGX.") + fmt.Println("WARNING: Building in UNSAFE DEBUG mode with MOCK TDX.") fmt.Println("WARNING: This build will NOT BE DEPLOYABLE outside local test environments.") os.Setenv("OASIS_UNSAFE_SKIP_AVR_VERIFY", "1") os.Setenv("OASIS_UNSAFE_ALLOW_DEBUG_ENCLAVES", "1") os.Unsetenv("OASIS_UNSAFE_MOCK_SGX") + os.Unsetenv("OASIS_UNSAFE_MOCK_TEE") os.Unsetenv("OASIS_UNSAFE_SKIP_KM_POLICY") default: cobra.CheckErr(fmt.Errorf("unsupported build mode: %s", buildMode)) } } - -func init() { - tdxFlags := flag.NewFlagSet("", flag.ContinueOnError) - tdxFlags.StringVar(&tdxFirmwareURI, "firmware", defaultFirmwareURI, "URL or path to firmware image") - tdxFlags.StringVar(&tdxKernelURI, "kernel", defaultKernelURI, "URL or path to kernel image") - tdxFlags.StringVar(&tdxStage2TemplateURI, "template", defaultStage2TemplateURI, "URL or path to 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/docs/rofl.md b/docs/rofl.md index c0ca7670..6713087d 100644 --- a/docs/rofl.md +++ b/docs/rofl.md @@ -22,7 +22,10 @@ The `build` command will execute a series of build commands depending on the target Trusted Execution Environment (TEE) and produce the Oasis Runtime Container (ORC) bundle. -Building a ROFL bundle requires the [Network and ParaTime][npa] selectors. +Building a ROFL bundle requires a ROFL app manifest (`rofl.yml`) to be present +in the current working directory. All information about what kind of ROFL app +to build is specified in the manifest. + Additionally, the following flags are available: - `--mode` specifies a `production` (enabled SGX attestations suitable for the @@ -43,38 +46,6 @@ and toolchains. Check out the [ROFL Prerequisites] chapter for details. [ROFL Prerequisites]: https://github.com/oasisprotocol/oasis-sdk/blob/main/docs/rofl/prerequisites.md [npa]: ./account.md#npa -### For SGX {#build-sgx} - -To build an SGX-based ROFL for the default [Network and ParaTime][npa], run -`build sgx`. - -![code shell](../examples/rofl/build-sgx.in.static) - -If you want to build a version for debugging and you're not running a [Sapphire -Localnet] at build time, you can force the *unsafe* build mode: - -![code shell](../examples/rofl/build-sgx-unsafe.in.static) - -The following SGX-specific flags are supported: - -- `--sgx-heap-size` is the heap size in bytes. -- `--sgx-stack-size` is the stack size in bytes. -- `--sgx-threads` is the maximum number of threads that an enclave can spawn. - This number must be at least the number of threads required by the [Oasis - Core runtime], otherwise the app may crash during the execution under actual - SGX. It can be greater, if the app needs to spawn more threads. - -[Sapphire Localnet]: https://github.com/oasisprotocol/docs/blob/main/docs/build/tools/localnet.mdx -[Oasis Core runtime]: https://github.com/oasisprotocol/oasis-core/blob/master/runtime/THREADS.md - - - ## Show ROFL identity {#identity} Run `rofl identity` to compute the **cryptographic identity** of the ROFL app: diff --git a/examples/rofl/build-sgx-unsafe.in.static b/examples/rofl/build-sgx-unsafe.in.static deleted file mode 100644 index 2632546c..00000000 --- a/examples/rofl/build-sgx-unsafe.in.static +++ /dev/null @@ -1 +0,0 @@ -oasis rofl build sgx --mode unsafe diff --git a/examples/rofl/build-sgx.in.static b/examples/rofl/build-sgx.in.static deleted file mode 100644 index 30f84fea..00000000 --- a/examples/rofl/build-sgx.in.static +++ /dev/null @@ -1 +0,0 @@ -oasis rofl build sgx From 26199ac958811b5354869f2d0edb3cd8c8140253 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Wed, 8 Jan 2025 14:34:15 +0100 Subject: [PATCH 03/20] feat(cmd/rofl): Update to oasis-boot v0.3.0 --- cmd/rofl/build/container.go | 4 ++-- cmd/rofl/build/tdx.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go index 1bb9c535..28e760c4 100644 --- a/cmd/rofl/build/container.go +++ b/cmd/rofl/build/container.go @@ -13,9 +13,9 @@ const ( artifactContainerRuntime = "rofl-container runtime" artifactContainerCompose = "compose.yaml" - defaultContainerStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage2-podman.tar.bz2" + defaultContainerStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage2-podman.tar.bz2#d84e0ca961fb0913b73a50ed90eb743af24b3d0acecdbe2594650e2801b41171" - defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers/v0.1.0/runtime" + defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.1.0/rofl-containers#89d533f8c2c0a8015fdc269ae350ab5b6a271c3aa6b17dd2c1ea49b3ffff2e06" defaultContainerComposeURI = "compose.yaml" ) diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 77abc32a..44dabd93 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -22,9 +22,9 @@ const ( artifactKernel = "kernel" artifactStage2 = "stage 2 template" - 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" + defaultFirmwareURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/ovmf.tdx.fd#db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f" + defaultKernelURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage1.bin#029255ff97cd0e6e3be04372578e7c980a8b8c0138b8153afc047cca98fe6008" + defaultStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage2-basic.tar.bz2#2dfbc01d62744052afa95feb737d5a0d6a68e2b58d71743751c4e3fc5faf4d36" ) var ( From 11302d644d1b1a4aa021c09363139daa5acedb7b Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Wed, 8 Jan 2025 14:43:57 +0100 Subject: [PATCH 04/20] feat(cmd/rofl): Use cached artifacts when available --- cmd/rofl/build/artifacts.go | 59 +++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/cmd/rofl/build/artifacts.go b/cmd/rofl/build/artifacts.go index e2c2f4fd..7e44418c 100644 --- a/cmd/rofl/build/artifacts.go +++ b/cmd/rofl/build/artifacts.go @@ -58,33 +58,48 @@ func maybeDownloadArtifact(kind, uri string) string { cobra.CheckErr(fmt.Errorf("failed to create cache directory for %s artifact: %w", kind, err)) } - f, err := os.Create(cacheFn) - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to create file for %s artifact: %w", kind, err)) - } - defer f.Close() + // First attempt to use the cached artifact. + f, err := os.Open(cacheFn) + switch { + case err == nil: + // Already exists in cache. + // TODO: Verify checksum and discard if invalid. + f.Close() + + fmt.Printf(" (using cached artifact)\n") + case errors.Is(err, os.ErrNotExist): + // Does not exist in cache, download. + f, err = os.Create(cacheFn) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to create file for %s artifact: %w", kind, err)) + } + defer f.Close() - // Download the remote artifact. - res, err := http.Get(uri) //nolint:gosec,noctx - if err != nil { - cobra.CheckErr(fmt.Errorf("failed to download %s artifact: %w", kind, err)) - } - defer res.Body.Close() + // Download the remote artifact. + var res *http.Response + res, err = http.Get(uri) //nolint:gosec,noctx + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to download %s artifact: %w", kind, err)) + } + defer res.Body.Close() - // Compute the SHA256 hash while downloading the artifact. - h := sha256.New() - rd := io.TeeReader(res.Body, h) + // Compute the SHA256 hash while downloading the artifact. + h := sha256.New() + rd := io.TeeReader(res.Body, h) - if _, err = io.Copy(f, rd); err != nil { - cobra.CheckErr(fmt.Errorf("failed to download %s artifact: %w", kind, err)) - } + if _, err = io.Copy(f, rd); err != nil { + cobra.CheckErr(fmt.Errorf("failed to download %s artifact: %w", kind, err)) + } - // Verify integrity if available. - if knownHash != "" { - artifactHash := fmt.Sprintf("%x", h.Sum(nil)) - if artifactHash != knownHash { - cobra.CheckErr(fmt.Errorf("hash mismatch for %s artifact (expected: %s got: %s)", kind, knownHash, artifactHash)) + // Verify integrity if available. + if knownHash != "" { + artifactHash := fmt.Sprintf("%x", h.Sum(nil)) + if artifactHash != knownHash { + cobra.CheckErr(fmt.Errorf("hash mismatch for %s artifact (expected: %s got: %s)", kind, knownHash, artifactHash)) + } } + default: + cobra.CheckErr(fmt.Errorf("failed to open cached %s artifact: %w", kind, err)) } return cacheFn From aaaca2d912ccc9bf82211b65afe26a45b34c729e Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Sat, 11 Jan 2025 11:33:07 +0100 Subject: [PATCH 05/20] Bump Go Client SDK to v0.12.1 --- examples/paratime-show/show-parameters.out | 5 ++--- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/paratime-show/show-parameters.out b/examples/paratime-show/show-parameters.out index 734d2730..ca58cfea 100644 --- a/examples/paratime-show/show-parameters.out +++ b/examples/paratime-show/show-parameters.out @@ -2,7 +2,6 @@ Network: testnet ParaTime: sapphire === ROFL PARAMETERS === - app_create: - Amount: 10000000000000000000000 - Denomination: + Stake thresholds: + App create: 10000.0 TEST diff --git a/go.mod b/go.mod index b4068596..20449678 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 github.com/oasisprotocol/metadata-registry-tools v0.0.0-20220406100644-7e9a2b991920 github.com/oasisprotocol/oasis-core/go v0.2403.1 - github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.11.1 + github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.12.1 github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index fdf11e6d..2c0410fa 100644 --- a/go.sum +++ b/go.sum @@ -418,8 +418,8 @@ github.com/oasisprotocol/metadata-registry-tools v0.0.0-20220406100644-7e9a2b991 github.com/oasisprotocol/metadata-registry-tools v0.0.0-20220406100644-7e9a2b991920/go.mod h1:MKr/giwakLyCCjSWh0W9Pbaf7rDD1K96Wr57OhNoUK0= github.com/oasisprotocol/oasis-core/go v0.2403.1 h1:WS9upKZ5S53lMc9eRcLdNOC4X0IKILFf/6GwlOvdpS0= github.com/oasisprotocol/oasis-core/go v0.2403.1/go.mod h1:H0wKb3CmUGVSioHjLCtzphc6MKODWcZs+YYAegoUT4g= -github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.11.1 h1:CdQ3SznnCVfnSRJszoR10+Nrv3a8nKQFxodZXKpePIo= -github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.11.1/go.mod h1:HZcEGQc/XnfJkDTj/bnfHxch3J9AxFKrCU4iQo1w3Gw= +github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.12.1 h1:4F/9liMod+uDT7FYj1X92LtB18UQxD0aTpklNFiCNrs= +github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.12.1/go.mod h1:HZcEGQc/XnfJkDTj/bnfHxch3J9AxFKrCU4iQo1w3Gw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= From 053ba764a521d20561f4eeb0e3a88b6e7e757f9e Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Mon, 13 Jan 2025 13:44:56 +0100 Subject: [PATCH 06/20] feat(cmd/rofl): Add rofl init subcommand for easier boostrap --- build/rofl/manifest.go | 19 ++++++- cmd/rofl/mgmt.go | 120 +++++++++++++++++++++++++++++++++++++++++ cmd/rofl/rofl.go | 1 + 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 7d2360bc..44fb516e 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -43,6 +43,8 @@ type Manifest struct { 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"` + // Admin is the identifier of the admin account. + Admin string `yaml:"admin,omitempty" json:"admin,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. @@ -56,8 +58,21 @@ type Manifest struct { // 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"` +} + +// ManifestExists checks whether a manifest file exist. No attempt is made to load, parse or +// validate any of the found manifest files. +func ManifestExists() bool { + for _, fn := range ManifestFileNames { + _, err := os.Stat(fn) + switch { + case errors.Is(err, os.ErrNotExist): + continue + default: + return true + } + } + return false } // LoadManifest attempts to find and load the ROFL app manifest from a local file. diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index d2d41ac8..2aadd970 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -10,11 +10,14 @@ import ( flag "github.com/spf13/pflag" "gopkg.in/yaml.v3" + "github.com/oasisprotocol/oasis-core/go/common/sgx/pcs" + "github.com/oasisprotocol/oasis-core/go/common/sgx/quote" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" "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" roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common" cliConfig "github.com/oasisprotocol/cli/config" @@ -30,6 +33,117 @@ var ( scheme string adminAddress string + appTEE string + appKind string + + initCmd = &cobra.Command{ + Use: "init [--tee TEE] [--kind KIND]", + Short: "Create a new ROFL app and initialize the manifest", + Args: cobra.ExactArgs(1), + Run: func(_ *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + txCfg := common.GetTransactionConfig() + + if npa.Account == nil { + cobra.CheckErr("no accounts configured in your wallet") + } + if npa.ParaTime == nil { + cobra.CheckErr("no ParaTime selected") + } + if txCfg.Offline { + cobra.CheckErr("offline mode currently not supported") + } + + // TODO: Support an interactive mode. + appName := args[0] + // Fail in case there is an existing manifest. + if buildRofl.ManifestExists() { + cobra.CheckErr("refusing to overwrite existing manifest") + } + + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + + // Determine latest height for the trust root. + height, err := common.GetActualHeight(ctx, conn.Consensus()) + cobra.CheckErr(err) + + // Generate manifest and a default policy which does not accept any enclaves. + manifest := buildRofl.Manifest{ + AppID: rofl.NewAppIDGlobalName("").String(), // Temporary for initial validation. + Name: appName, + Version: "0.1.0", + Network: npa.NetworkName, + ParaTime: npa.ParaTimeName, + Admin: npa.AccountName, + TEE: appTEE, + Kind: appKind, + Policy: &rofl.AppAuthPolicy{ + Quotes: quote.Policy{ + PCS: &pcs.QuotePolicy{ + TCBValidityPeriod: 30, + MinTCBEvaluationDataNumber: 17, + TDX: &pcs.TdxQuotePolicy{}, + }, + }, + Endorsements: []rofl.AllowedEndorsement{ + {Any: &struct{}{}}, + }, + Fees: rofl.FeePolicyEndorsingNodePays, + MaxExpiration: 3, + }, + TrustRoot: &buildRofl.TrustRootConfig{ + Height: uint64(height), + }, + Resources: buildRofl.ResourcesConfig{ + Memory: 512, + CPUCount: 1, + EphemeralStorage: &buildRofl.EphemeralStorageConfig{ + Kind: buildRofl.EphemeralStorageKindDisk, + Size: 512, + }, + }, + } + err = manifest.Validate() + cobra.CheckErr(err) + + fmt.Printf("Creating a new ROFL app with default policy...\n") + fmt.Printf("Name: %s\n", manifest.Name) + fmt.Printf("Version: %s\n", manifest.Version) + fmt.Printf("TEE: %s\n", manifest.TEE) + fmt.Printf("Kind: %s\n", manifest.Kind) + + idScheme, ok := identifierSchemes[scheme] + if !ok { + cobra.CheckErr(fmt.Errorf("unknown scheme %s", scheme)) + } + + // Register a new ROFL application to determine the identifier. + tx := rofl.NewCreateTx(nil, &rofl.Create{ + Policy: *manifest.Policy, + Scheme: idScheme, + }) + + acc := common.LoadAccount(cfg, npa.AccountName) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, nil) + cobra.CheckErr(err) + + var appID rofl.AppID + common.BroadcastTransaction(ctx, npa.ParaTime, conn, sigTx, meta, &appID) + manifest.AppID = appID.String() + + fmt.Printf("Created ROFL application: %s\n", appID) + + // Serialize manifest and write it to file. + data, _ := yaml.Marshal(manifest) + if err = os.WriteFile("rofl.yml", data, 0o644); err != nil { //nolint: gosec + cobra.CheckErr(fmt.Errorf("failed to write manifest: %w", err)) + } + }, + } + createCmd = &cobra.Command{ Use: "create []", Short: "Create a new ROFL application", @@ -296,6 +410,12 @@ func init() { updateFlags.StringVar(&policyFn, "policy", "", "set the ROFL application policy") updateFlags.StringVar(&adminAddress, "admin", "", "set the administrator address") + initCmd.Flags().AddFlagSet(common.SelectorFlags) + initCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + initCmd.Flags().StringVar(&appTEE, "tee", "tdx", "TEE kind [tdx, sgx]") + initCmd.Flags().StringVar(&appKind, "kind", "container", "ROFL app kind [container, raw]") + initCmd.Flags().StringVar(&scheme, "scheme", "cn", "app ID generation scheme: creator+round+index [cri] or creator+nonce [cn]") + createCmd.Flags().AddFlagSet(common.SelectorFlags) createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) createCmd.Flags().StringVar(&scheme, "scheme", "cn", "app ID generation scheme: creator+round+index [cri] or creator+nonce [cn]") diff --git a/cmd/rofl/rofl.go b/cmd/rofl/rofl.go index 3cf97ee8..bd6435f2 100644 --- a/cmd/rofl/rofl.go +++ b/cmd/rofl/rofl.go @@ -13,6 +13,7 @@ var Cmd = &cobra.Command{ } func init() { + Cmd.AddCommand(initCmd) Cmd.AddCommand(createCmd) Cmd.AddCommand(updateCmd) Cmd.AddCommand(removeCmd) From e3364730c817c685414965b8117ea52d7e9b2fab Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Mon, 13 Jan 2025 14:37:25 +0100 Subject: [PATCH 07/20] feat(cmd/rofl): Support --update-manifest flag for build --- build/rofl/manifest.go | 11 +++++++++ cmd/rofl/build/build.go | 49 +++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 44fb516e..4f5c2dd9 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" "gopkg.in/yaml.v3" @@ -58,6 +59,9 @@ type Manifest struct { // Policy is the ROFL app policy to deploy by default. Policy *rofl.AppAuthPolicy `yaml:"policy,omitempty" json:"policy,omitempty"` + + // sourceFn is the filename from which the manifest has been loaded. + sourceFn string } // ManifestExists checks whether a manifest file exist. No attempt is made to load, parse or @@ -97,6 +101,7 @@ func LoadManifest() (*Manifest, error) { f.Close() return nil, fmt.Errorf("invalid manifest '%s': %w", fn, err) } + m.sourceFn, _ = filepath.Abs(f.Name()) // Record source filename. f.Close() return &m, nil @@ -148,6 +153,12 @@ func (m *Manifest) Validate() error { return nil } +// SourceFileName returns the filename of the manifest file from which the manifest was loaded or +// an empty string in case the filename is not available. +func (m *Manifest) SourceFileName() string { + return m.sourceFn +} + // TrustRootConfig is the trust root configuration. type TrustRootConfig struct { // Height is the consensus layer block height where to take the trust root. diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index 340666e3..95791639 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -8,9 +8,11 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" + "gopkg.in/yaml.v3" coreCommon "github.com/oasisprotocol/oasis-core/go/common" "github.com/oasisprotocol/oasis-core/go/common/cbor" + "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/common/version" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" "github.com/oasisprotocol/oasis-core/go/runtime/bundle" @@ -33,6 +35,7 @@ var ( outputFn string buildMode string offline bool + doUpdate bool Cmd = &cobra.Command{ Use: "build", @@ -105,6 +108,8 @@ var ( return } + fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + fmt.Println("Computing enclave identity...") eids, err := roflCommon.ComputeEnclaveIdentity(bnd, "") @@ -113,15 +118,34 @@ var ( return } - 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)) + // Override the update manifest flag in case the policy does not exist. + if manifest.Policy == nil { + doUpdate = false } - fmt.Println() - fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + switch doUpdate { + case false: + // Ask the user to update the manifest manually. + 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() + case true: + // Update the manifest with the given enclave identities, overwriting existing ones. + manifest.Policy.Enclaves = make([]sgx.EnclaveIdentity, 0, len(eids)) + for _, eid := range eids { + manifest.Policy.Enclaves = append(manifest.Policy.Enclaves, *eid) + } + + // Serialize manifest and write it to file. + data, _ := yaml.Marshal(manifest) + if err = os.WriteFile(manifest.SourceFileName(), data, 0o644); err != nil { //nolint: gosec + cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err)) + } + } }, } ) @@ -221,10 +245,11 @@ func fetchTrustRoot(npa *common.NPASelection, cfg *buildRofl.TrustRootConfig) (s } func init() { - globalFlags := flag.NewFlagSet("", flag.ContinueOnError) - globalFlags.StringVar(&buildMode, "mode", "auto", "build mode [production, unsafe, auto]") - globalFlags.BoolVar(&offline, "offline", false, "do not perform any operations requiring network access") - globalFlags.StringVar(&outputFn, "output", "", "output bundle filename") + buildFlags := flag.NewFlagSet("", flag.ContinueOnError) + buildFlags.StringVar(&buildMode, "mode", "auto", "build mode [production, unsafe, auto]") + buildFlags.BoolVar(&offline, "offline", false, "do not perform any operations requiring network access") + buildFlags.StringVar(&outputFn, "output", "", "output bundle filename") + buildFlags.BoolVar(&doUpdate, "update-manifest", false, "automatically update the manifest") - Cmd.PersistentFlags().AddFlagSet(globalFlags) + Cmd.Flags().AddFlagSet(buildFlags) } From fb5a4041ea51cd373e2a90174bda1f01fcab6260 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Tue, 14 Jan 2025 08:05:52 +0100 Subject: [PATCH 08/20] Rename ephemeral storage to just storage --- build/rofl/manifest.go | 35 ++++++++++++++++++----------------- cmd/rofl/build/tdx.go | 20 +++++++++++--------- cmd/rofl/mgmt.go | 4 ++-- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 4f5c2dd9..94dcb0c7 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -173,8 +173,8 @@ type ResourcesConfig struct { 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"` + // Storage is the storage configuration. + Storage *StorageConfig `yaml:"storage,omitempty" json:"storage,omitempty"` } // Validate validates the resources configuration for correctness. @@ -185,40 +185,41 @@ func (r *ResourcesConfig) Validate() error { if r.CPUCount < 1 { return fmt.Errorf("vCPU count must be at least 1") } - if r.EphemeralStorage != nil { - err := r.EphemeralStorage.Validate() + if r.Storage != nil { + err := r.Storage.Validate() if err != nil { - return fmt.Errorf("bad ephemeral storage config: %w", err) + return fmt.Errorf("bad storage config: %w", err) } } return nil } -// Supported ephemeral storage kinds. +// Supported storage kinds. const ( - EphemeralStorageKindNone = "none" - EphemeralStorageKindDisk = "disk" - EphemeralStorageKindRAM = "ram" + StorageKindNone = "none" + StorageKindDiskEphemeral = "disk-ephemeral" + StorageKindDiskPersistent = "disk-persistent" + StorageKindRAM = "ram" ) -// EphemeralStorageConfig is the ephemeral storage configuration. -type EphemeralStorageConfig struct { +// StorageConfig is the storage configuration. +type StorageConfig struct { // Kind is the storage kind. Kind string `yaml:"kind" json:"kind"` - // Size is the amount of ephemeral storage in megabytes. + // Size is the amount of storage in megabytes. Size uint64 `yaml:"size" json:"size"` } -// Validate validates the ephemeral storage configuration for correctness. -func (e *EphemeralStorageConfig) Validate() error { +// Validate validates the storage configuration for correctness. +func (e *StorageConfig) Validate() error { switch e.Kind { - case EphemeralStorageKindNone, EphemeralStorageKindDisk, EphemeralStorageKindRAM: + case StorageKindNone, StorageKindDiskEphemeral, StorageKindDiskPersistent, StorageKindRAM: default: - return fmt.Errorf("unsupported ephemeral storage kind: %s", e.Kind) + return fmt.Errorf("unsupported storage kind: %s", e.Kind) } if e.Size < 16 { - return fmt.Errorf("ephemeral storage size must be at least 16M") + return fmt.Errorf("storage size must be at least 16M") } return nil } diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 44dabd93..88b09033 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -225,34 +225,36 @@ func tdxBundleComponent( }, } - tmpStorageKind := buildRofl.EphemeralStorageKindNone - if manifest.Resources.EphemeralStorage != nil { - tmpStorageKind = manifest.Resources.EphemeralStorage.Kind + tmpStorageKind := buildRofl.StorageKindNone + if manifest.Resources.Storage != nil { + tmpStorageKind = manifest.Resources.Storage.Kind } switch tmpStorageKind { - case buildRofl.EphemeralStorageKindNone: - case buildRofl.EphemeralStorageKindRAM: + case buildRofl.StorageKindNone: + case buildRofl.StorageKindRAM: comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, "oasis.stage2.storage_mode=ram", - fmt.Sprintf("oasis.stage2.storage_size=%d", manifest.Resources.EphemeralStorage.Size*1024*1024), + fmt.Sprintf("oasis.stage2.storage_size=%d", manifest.Resources.Storage.Size*1024*1024), ) - case buildRofl.EphemeralStorageKindDisk: + case buildRofl.StorageKindDiskEphemeral, buildRofl.StorageKindDiskPersistent: // Allocate some space after regular stage2. const sectorSize = 512 - storageSize := manifest.Resources.EphemeralStorage.Size * 1024 * 1024 + storageSize := manifest.Resources.Storage.Size * 1024 * 1024 storageOffset, err := appendEmptySpace(stage2.fn, storageSize, sectorSize) if err != nil { return err } + // TODO: For persistent disk, configure Stage2Persist flag and storage mode. + 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", tmpStorageKind) + return fmt.Errorf("unsupported storage mode: %s", tmpStorageKind) } // TODO: (Oasis Core 25.0+) Use qcow2 image format to support sparse files. diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 2aadd970..25b975b3 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -100,8 +100,8 @@ var ( Resources: buildRofl.ResourcesConfig{ Memory: 512, CPUCount: 1, - EphemeralStorage: &buildRofl.EphemeralStorageConfig{ - Kind: buildRofl.EphemeralStorageKindDisk, + Storage: &buildRofl.StorageConfig{ + Kind: buildRofl.StorageKindDiskEphemeral, Size: 512, }, }, From 02edf7f391965bf4135d34fccae8eb2adef569e1 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Tue, 14 Jan 2025 09:42:13 +0100 Subject: [PATCH 09/20] feat(cmd/rofl): Add support for multiple deployments --- build/rofl/manifest.go | 72 +++++++++++++++++++-------- build/rofl/manifest_test.go | 97 +++++++++++++++++++++++++++---------- cmd/rofl/build/build.go | 47 ++++++++++-------- cmd/rofl/build/container.go | 12 +++-- cmd/rofl/build/sgx.go | 13 +++-- cmd/rofl/build/tdx.go | 19 ++++++-- cmd/rofl/common/manifest.go | 45 +++++++++-------- cmd/rofl/mgmt.go | 65 ++++++++++++++++--------- 8 files changed, 249 insertions(+), 121 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 94dcb0c7..d4d9487f 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -34,31 +34,21 @@ const ( // 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"` - // Admin is the identifier of the admin account. - Admin string `yaml:"admin,omitempty" json:"admin,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"` + // Deployments are the ROFL app deployments. + Deployments map[string]*Deployment `yaml:"deployments" json:"deployments"` // sourceFn is the filename from which the manifest has been loaded. sourceFn string @@ -111,14 +101,6 @@ func LoadManifest() (*Manifest, error) { // 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") } @@ -150,6 +132,18 @@ func (m *Manifest) Validate() error { return fmt.Errorf("bad resources config: %w", err) } + for name, d := range m.Deployments { + if d == nil { + return fmt.Errorf("bad deployment: %s", name) + } + if err := d.Validate(); err != nil { + return fmt.Errorf("bad deployment '%s': %w", name, err) + } + } + if _, ok := m.Deployments[DefaultDeploymentName]; !ok { + return fmt.Errorf("must define at least the '%s' deployment", DefaultDeploymentName) + } + return nil } @@ -159,6 +153,44 @@ func (m *Manifest) SourceFileName() string { return m.sourceFn } +// DefaultDeploymentName is the name of the default deployment that must always be defined and is +// used in case no deployment is passed. +const DefaultDeploymentName = "default" + +// Deployment describes a single ROFL app deployment. +type Deployment struct { + // AppID is the Bech32-encoded ROFL app ID. + AppID string `yaml:"app_id" json:"app_id"` + // Network is the identifier of the network to deploy to. + Network string `yaml:"network" json:"network"` + // ParaTime is the identifier of the paratime to deploy to. + ParaTime string `yaml:"paratime" json:"paratime"` + // Admin is the identifier of the admin account. + Admin string `yaml:"admin,omitempty" json:"admin,omitempty"` + // TrustRoot is the optional trust root configuration. + TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"` + // Policy is the ROFL app policy. + Policy *rofl.AppAuthPolicy `yaml:"policy,omitempty" json:"policy,omitempty"` +} + +// Validate validates the manifest for correctness. +func (d *Deployment) Validate() error { + if len(d.AppID) == 0 { + return fmt.Errorf("app ID cannot be empty") + } + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(d.AppID)); err != nil { + return fmt.Errorf("malformed app ID: %w", err) + } + if d.Network == "" { + return fmt.Errorf("network cannot be empty") + } + if d.ParaTime == "" { + return fmt.Errorf("paratime cannot be empty") + } + return nil +} + // TrustRootConfig is the trust root configuration. type TrustRootConfig struct { // Height is the consensus layer block height where to take the trust root. diff --git a/build/rofl/manifest_test.go b/build/rofl/manifest_test.go index 86d3d21b..a901fc66 100644 --- a/build/rofl/manifest_test.go +++ b/build/rofl/manifest_test.go @@ -15,16 +15,6 @@ func TestManifestValidation(t *testing.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. @@ -33,7 +23,7 @@ func TestManifestValidation(t *testing.T) { require.ErrorContains(err, "version cannot be empty") // Invalid version. - m.Version = "foo" + m.Version = "invalidversion" err = m.Validate() require.ErrorContains(err, "malformed version") @@ -61,27 +51,53 @@ func TestManifestValidation(t *testing.T) { err = m.Validate() require.ErrorContains(err, "bad resources config: vCPU count must be at least 1") - // Finally, everything is valid. + // No default deployment. m.Resources.CPUCount = 1 err = m.Validate() + require.ErrorContains(err, "must define at least the 'default' deployment") + + // Missing app ID in deployment. + m.Deployments = map[string]*Deployment{ + "default": {}, + } + err = m.Validate() + require.ErrorContains(err, "bad deployment 'default': app ID cannot be empty") + + // Invalid app ID. + m.Deployments["default"].AppID = "foo" + err = m.Validate() + require.ErrorContains(err, "bad deployment 'default': malformed app ID") + + // Missing network in deployment. + m.Deployments["default"].AppID = "rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j" + err = m.Validate() + require.ErrorContains(err, "bad deployment 'default': network cannot be empty") + + // Missing paratime in deployment. + m.Deployments["default"].Network = "foo" + err = m.Validate() + require.ErrorContains(err, "bad deployment 'default': paratime cannot be empty") + + // Finally, everything is valid. + m.Deployments["default"].ParaTime = "bar" + err = m.Validate() require.NoError(err) // Add ephemeral storage configuration. - m.Resources.EphemeralStorage = &EphemeralStorageConfig{} + m.Resources.Storage = &StorageConfig{} err = m.Validate() - require.ErrorContains(err, "bad resources config: bad ephemeral storage config: unsupported ephemeral storage kind") + require.ErrorContains(err, "bad resources config: bad storage config: unsupported storage kind") - m.Resources.EphemeralStorage.Kind = "ram" + m.Resources.Storage.Kind = "ram" err = m.Validate() - require.ErrorContains(err, "bad resources config: bad ephemeral storage config: ephemeral storage size must be at least 16M") + require.ErrorContains(err, "bad resources config: bad storage config: storage size must be at least 16M") - m.Resources.EphemeralStorage.Size = 16 + m.Resources.Storage.Size = 16 err = m.Validate() require.NoError(err) } const serializedYamlManifest = ` -app_id: rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j name: my-simple-app version: 0.1.0 tee: tdx @@ -89,9 +105,30 @@ kind: container resources: memory: 16 cpus: 1 - ephemeral_storage: + storage: kind: ram size: 16 +deployments: + default: + app_id: rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j + network: foo + paratime: bar + admin: blah + trust_root: + height: 24805610 + policy: + quotes: + pcs: + tcb_validity_period: 30 + min_tcb_evaluation_data_number: 17 + tdx: {} + enclaves: + - BeCAq9adUtEvhDYmYbZjMBvbIf1CmlVOD57z6aEGlRMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + - P1vM2Zp/qP1Kx07ObFuBDBFCuHGji2gPxrnts7G6g3gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + endorsements: + - any: {} + fees: endorsing_node + max_expiration: 3 ` func TestManifestSerialization(t *testing.T) { @@ -102,16 +139,22 @@ func TestManifestSerialization(t *testing.T) { 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) + require.NotNil(m.Resources.Storage) + require.Equal("ram", m.Resources.Storage.Kind) + require.EqualValues(16, m.Resources.Storage.Size) + require.Len(m.Deployments, 1) + require.Contains(m.Deployments, "default") + require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.Deployments["default"].AppID) + require.Equal("foo", m.Deployments["default"].Network) + require.Equal("bar", m.Deployments["default"].ParaTime) + require.Equal("blah", m.Deployments["default"].Admin) + require.EqualValues(24805610, m.Deployments["default"].TrustRoot.Height) enc, err := yaml.Marshal(m) require.NoError(err, "yaml.Marshal") @@ -147,7 +190,9 @@ func TestLoadManifest(t *testing.T) { require.NoError(err) m, err := LoadManifest() require.NoError(err) - require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) + require.Len(m.Deployments, 1) + require.Contains(m.Deployments, "default") + require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.Deployments["default"].AppID) err = os.Remove(manifestFn) require.NoError(err) @@ -157,5 +202,7 @@ func TestLoadManifest(t *testing.T) { require.NoError(err) m, err = LoadManifest() require.NoError(err) - require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.AppID) + require.Len(m.Deployments, 1) + require.Contains(m.Deployments, "default") + require.Equal("rofl1qpa9ydy3qmka3yrqzx0pxuvyfexf9mlh75hker5j", m.Deployments["default"].AppID) } diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index 95791639..cf341a23 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -32,10 +32,11 @@ const ( ) var ( - outputFn string - buildMode string - offline bool - doUpdate bool + outputFn string + buildMode string + offline bool + doUpdate bool + deploymentName string Cmd = &cobra.Command{ Use: "build", @@ -44,14 +45,17 @@ var ( Run: func(_ *cobra.Command, _ []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) - manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) + manifest, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) fmt.Println("Building a ROFL application...") - fmt.Printf("App ID: %s\n", manifest.AppID) - fmt.Printf("Name: %s\n", manifest.Name) - fmt.Printf("Version: %s\n", manifest.Version) - fmt.Printf("TEE: %s\n", manifest.TEE) - fmt.Printf("Kind: %s\n", manifest.Kind) + fmt.Printf("Deployment: %s\n", deploymentName) + fmt.Printf("Network: %s\n", deployment.Network) + fmt.Printf("ParaTime: %s\n", deployment.ParaTime) + fmt.Printf("App ID: %s\n", deployment.AppID) + fmt.Printf("Name: %s\n", manifest.Name) + fmt.Printf("Version: %s\n", manifest.Version) + fmt.Printf("TEE: %s\n", manifest.TEE) + fmt.Printf("Kind: %s\n", manifest.Kind) // Prepare temporary build directory. tmpDir, err := os.MkdirTemp("", "oasis-build") @@ -62,7 +66,7 @@ var ( bnd := &bundle.Bundle{ Manifest: &bundle.Manifest{ - Name: manifest.AppID, + Name: deployment.AppID, ID: npa.ParaTime.Namespace(), }, } @@ -80,14 +84,14 @@ var ( return } - sgxBuild(npa, manifest, bnd) + sgxBuild(npa, manifest, deployment, bnd) case buildRofl.TEETypeTDX: // TDX. switch manifest.Kind { case buildRofl.AppKindRaw: - err = tdxBuildRaw(tmpDir, npa, manifest, bnd) + err = tdxBuildRaw(tmpDir, npa, manifest, deployment, bnd) case buildRofl.AppKindContainer: - err = tdxBuildContainer(tmpDir, npa, manifest, bnd) + err = tdxBuildContainer(tmpDir, npa, manifest, deployment, bnd) } default: fmt.Printf("unsupported TEE kind: %s\n", manifest.TEE) @@ -99,7 +103,7 @@ var ( } // Write the bundle out. - outFn := fmt.Sprintf("%s.orc", manifest.Name) + outFn := fmt.Sprintf("%s.%s.orc", manifest.Name, deploymentName) if outputFn != "" { outFn = outputFn } @@ -119,7 +123,7 @@ var ( } // Override the update manifest flag in case the policy does not exist. - if manifest.Policy == nil { + if deployment.Policy == nil { doUpdate = false } @@ -135,9 +139,9 @@ var ( fmt.Println() case true: // Update the manifest with the given enclave identities, overwriting existing ones. - manifest.Policy.Enclaves = make([]sgx.EnclaveIdentity, 0, len(eids)) + deployment.Policy.Enclaves = make([]sgx.EnclaveIdentity, 0, len(eids)) for _, eid := range eids { - manifest.Policy.Enclaves = append(manifest.Policy.Enclaves, *eid) + deployment.Policy.Enclaves = append(deployment.Policy.Enclaves, *eid) } // Serialize manifest and write it to file. @@ -173,12 +177,12 @@ func detectBuildMode(npa *common.NPASelection) { } } -func setupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) { +func setupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection) { // Configure app ID. - os.Setenv("ROFL_APP_ID", manifest.AppID) + os.Setenv("ROFL_APP_ID", deployment.AppID) // Obtain and configure trust root. - trustRoot, err := fetchTrustRoot(npa, manifest.TrustRoot) + trustRoot, err := fetchTrustRoot(npa, deployment.TrustRoot) cobra.CheckErr(err) os.Setenv("ROFL_CONSENSUS_TRUST_ROOT", trustRoot) } @@ -250,6 +254,7 @@ func init() { buildFlags.BoolVar(&offline, "offline", false, "do not perform any operations requiring network access") buildFlags.StringVar(&outputFn, "output", "", "output bundle filename") buildFlags.BoolVar(&doUpdate, "update-manifest", false, "automatically update the manifest") + buildFlags.StringVar(&deploymentName, "deployment", buildRofl.DefaultDeploymentName, "deployment name") Cmd.Flags().AddFlagSet(buildFlags) } diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go index 28e760c4..e6991667 100644 --- a/cmd/rofl/build/container.go +++ b/cmd/rofl/build/container.go @@ -21,7 +21,13 @@ const ( ) // tdxBuildContainer builds a TDX-based container ROFL app. -func tdxBuildContainer(tmpDir string, npa *common.NPASelection, manifest *buildRofl.Manifest, bnd *bundle.Bundle) error { +func tdxBuildContainer( + tmpDir string, + npa *common.NPASelection, + manifest *buildRofl.Manifest, + deployment *buildRofl.Deployment, + bnd *bundle.Bundle, +) error { fmt.Println("Building a container-based TDX ROFL application...") tdxStage2TemplateURI = defaultContainerStage2TemplateURI @@ -54,11 +60,11 @@ func tdxBuildContainer(tmpDir string, npa *common.NPASelection, manifest *buildR // Configure app ID. var extraKernelOpts []string extraKernelOpts = append(extraKernelOpts, - fmt.Sprintf("ROFL_APP_ID=%s", manifest.AppID), + fmt.Sprintf("ROFL_APP_ID=%s", deployment.AppID), ) // Obtain and configure trust root. - trustRoot, err := fetchTrustRoot(npa, manifest.TrustRoot) + trustRoot, err := fetchTrustRoot(npa, deployment.TrustRoot) if err != nil { return err } diff --git a/cmd/rofl/build/sgx.go b/cmd/rofl/build/sgx.go index f4ab719a..5bb1747a 100644 --- a/cmd/rofl/build/sgx.go +++ b/cmd/rofl/build/sgx.go @@ -24,11 +24,16 @@ import ( ) // sgxBuild builds an SGX-based "raw" ROFL app. -func sgxBuild(npa *common.NPASelection, manifest *buildRofl.Manifest, bnd *bundle.Bundle) { +func sgxBuild( + npa *common.NPASelection, + manifest *buildRofl.Manifest, + deployment *buildRofl.Deployment, + bnd *bundle.Bundle, +) { fmt.Println("Building an SGX-based Rust ROFL application...") detectBuildMode(npa) - features := sgxSetupBuildEnv(manifest, npa) + features := sgxSetupBuildEnv(deployment, npa) // First build for the default target. fmt.Println("Building ELF binary...") @@ -209,8 +214,8 @@ NextSetOfPrimes: } // sgxSetupBuildEnv sets up the SGX build environment and returns the list of features to enable. -func sgxSetupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) []string { - setupBuildEnv(manifest, npa) +func sgxSetupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection) []string { + setupBuildEnv(deployment, npa) switch buildMode { case buildModeProduction, buildModeAuto: diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 88b09033..22f37266 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -34,7 +34,13 @@ var ( ) // tdxBuildRaw builds a TDX-based "raw" ROFL app. -func tdxBuildRaw(tmpDir string, npa *common.NPASelection, manifest *buildRofl.Manifest, bnd *bundle.Bundle) error { +func tdxBuildRaw( + tmpDir string, + npa *common.NPASelection, + manifest *buildRofl.Manifest, + deployment *buildRofl.Deployment, + bnd *bundle.Bundle, +) error { wantedArtifacts := tdxGetDefaultArtifacts() tdxOverrideArtifacts(manifest, wantedArtifacts) artifacts := tdxFetchArtifacts(wantedArtifacts) @@ -42,7 +48,7 @@ func tdxBuildRaw(tmpDir string, npa *common.NPASelection, manifest *buildRofl.Ma fmt.Println("Building a TDX-based Rust ROFL application...") detectBuildMode(npa) - tdxSetupBuildEnv(manifest, npa) + tdxSetupBuildEnv(deployment, npa) // Obtain package metadata. pkgMeta, err := cargo.GetMetadata() @@ -246,7 +252,10 @@ func tdxBundleComponent( return err } - // TODO: For persistent disk, configure Stage2Persist flag and storage mode. + if tmpStorageKind == buildRofl.StorageKindDiskPersistent { + // TODO: For persistent disk, configure Stage2Persist flag and storage mode. + return fmt.Errorf("persistent disk not yet supported, use 'disk-ephemeral'") + } comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, "oasis.stage2.storage_mode=disk", @@ -282,8 +291,8 @@ func tdxBundleComponent( } // tdxSetupBuildEnv sets up the TDX build environment. -func tdxSetupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) { - setupBuildEnv(manifest, npa) +func tdxSetupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection) { + setupBuildEnv(deployment, npa) switch buildMode { case buildModeProduction, buildModeAuto: diff --git a/cmd/rofl/common/manifest.go b/cmd/rofl/common/manifest.go index 9ae2323a..d6bb9122 100644 --- a/cmd/rofl/common/manifest.go +++ b/cmd/rofl/common/manifest.go @@ -14,55 +14,60 @@ import ( // 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) +func LoadManifestAndSetNPA(cfg *config.Config, npa *common.NPASelection, deployment string) (*rofl.Manifest, *rofl.Deployment) { + manifest, d, err := MaybeLoadManifestAndSetNPA(cfg, npa, deployment) cobra.CheckErr(err) - return manifest + return manifest, d } // 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) { +func MaybeLoadManifestAndSetNPA(cfg *config.Config, npa *common.NPASelection, deployment string) (*rofl.Manifest, *rofl.Deployment, error) { manifest, err := rofl.LoadManifest() if err != nil { - return nil, err + return nil, nil, err } - switch manifest.Network { + d, ok := manifest.Deployments[deployment] + if !ok { + return nil, nil, fmt.Errorf("deployment '%s' does not exist", deployment) + } + + switch d.Network { case "": if npa.Network == nil { - return nil, fmt.Errorf("no network selected") + return nil, nil, fmt.Errorf("no network selected") } default: - npa.Network = cfg.Networks.All[manifest.Network] + npa.Network = cfg.Networks.All[d.Network] if npa.Network == nil { - return nil, fmt.Errorf("network '%s' does not exist", manifest.Network) + return nil, nil, fmt.Errorf("network '%s' does not exist", d.Network) } - npa.NetworkName = manifest.Network + npa.NetworkName = d.Network } - switch manifest.ParaTime { + switch d.ParaTime { case "": if npa.ParaTime == nil { - return nil, fmt.Errorf("no ParaTime selected") + return nil, nil, fmt.Errorf("no ParaTime selected") } default: - npa.ParaTime = npa.Network.ParaTimes.All[manifest.ParaTime] + npa.ParaTime = npa.Network.ParaTimes.All[d.ParaTime] if npa.ParaTime == nil { - return nil, fmt.Errorf("paratime '%s' does not exist", manifest.ParaTime) + return nil, nil, fmt.Errorf("paratime '%s' does not exist", d.ParaTime) } - npa.ParaTimeName = manifest.ParaTime + npa.ParaTimeName = d.ParaTime } - switch manifest.Admin { + switch d.Admin { case "": default: - accCfg, err := common.LoadAccountConfig(cfg, manifest.Admin) + accCfg, err := common.LoadAccountConfig(cfg, d.Admin) if err != nil { - return nil, err + return nil, nil, err } npa.Account = accCfg - npa.AccountName = manifest.Admin + npa.AccountName = d.Admin } - return manifest, nil + return manifest, d, nil } diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 25b975b3..1363b4c5 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -33,8 +33,9 @@ var ( scheme string adminAddress string - appTEE string - appKind string + appTEE string + appKind string + deploymentName string initCmd = &cobra.Command{ Use: "init [--tee TEE] [--kind KIND]", @@ -71,15 +72,11 @@ var ( cobra.CheckErr(err) // Generate manifest and a default policy which does not accept any enclaves. - manifest := buildRofl.Manifest{ + deployment := &buildRofl.Deployment{ AppID: rofl.NewAppIDGlobalName("").String(), // Temporary for initial validation. - Name: appName, - Version: "0.1.0", Network: npa.NetworkName, ParaTime: npa.ParaTimeName, Admin: npa.AccountName, - TEE: appTEE, - Kind: appKind, Policy: &rofl.AppAuthPolicy{ Quotes: quote.Policy{ PCS: &pcs.QuotePolicy{ @@ -97,6 +94,12 @@ var ( TrustRoot: &buildRofl.TrustRootConfig{ Height: uint64(height), }, + } + manifest := buildRofl.Manifest{ + Name: appName, + Version: "0.1.0", + TEE: appTEE, + Kind: appKind, Resources: buildRofl.ResourcesConfig{ Memory: 512, CPUCount: 1, @@ -105,15 +108,22 @@ var ( Size: 512, }, }, + Deployments: map[string]*buildRofl.Deployment{ + buildRofl.DefaultDeploymentName: deployment, + }, } err = manifest.Validate() cobra.CheckErr(err) fmt.Printf("Creating a new ROFL app with default policy...\n") - fmt.Printf("Name: %s\n", manifest.Name) - fmt.Printf("Version: %s\n", manifest.Version) - fmt.Printf("TEE: %s\n", manifest.TEE) - fmt.Printf("Kind: %s\n", manifest.Kind) + fmt.Printf("Name: %s\n", manifest.Name) + fmt.Printf("Version: %s\n", manifest.Version) + fmt.Printf("TEE: %s\n", manifest.TEE) + fmt.Printf("Kind: %s\n", manifest.Kind) + fmt.Printf("Deployment '%s':\n", buildRofl.DefaultDeploymentName) + fmt.Printf(" Network: %s\n", deployment.Network) + fmt.Printf(" ParaTime: %s\n", deployment.ParaTime) + fmt.Printf(" Admin: %s\n", deployment.Admin) idScheme, ok := identifierSchemes[scheme] if !ok { @@ -122,7 +132,7 @@ var ( // Register a new ROFL application to determine the identifier. tx := rofl.NewCreateTx(nil, &rofl.Create{ - Policy: *manifest.Policy, + Policy: *deployment.Policy, Scheme: idScheme, }) @@ -132,7 +142,7 @@ var ( var appID rofl.AppID common.BroadcastTransaction(ctx, npa.ParaTime, conn, sigTx, meta, &appID) - manifest.AppID = appID.String() + deployment.AppID = appID.String() fmt.Printf("Created ROFL application: %s\n", appID) @@ -157,8 +167,8 @@ var ( if len(args) > 0 { policy = loadPolicy(args[0]) } else { - manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) - policy = manifest.Policy + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + policy = deployment.Policy } if npa.Account == nil { @@ -218,13 +228,13 @@ var ( rawAppID = args[0] policy = loadPolicy(policyFn) } else { - manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) - rawAppID = manifest.AppID + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + rawAppID = deployment.AppID - if adminAddress == "" && manifest.Admin != "" { + if adminAddress == "" && deployment.Admin != "" { adminAddress = "self" } - policy = manifest.Policy + policy = deployment.Policy } var appID rofl.AppID if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { @@ -295,8 +305,8 @@ var ( if len(args) > 0 { rawAppID = args[0] } else { - manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) - rawAppID = manifest.AppID + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + rawAppID = deployment.AppID } var appID rofl.AppID if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { @@ -344,8 +354,8 @@ var ( if len(args) > 0 { rawAppID = args[0] } else { - manifest := roflCommon.LoadManifestAndSetNPA(cfg, npa) - rawAppID = manifest.AppID + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + rawAppID = deployment.AppID } var appID rofl.AppID if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { @@ -406,26 +416,35 @@ func loadPolicy(fn string) *rofl.AppAuthPolicy { } func init() { + deploymentFlags := flag.NewFlagSet("", flag.ContinueOnError) + deploymentFlags.StringVar(&deploymentName, "deployment", buildRofl.DefaultDeploymentName, "deployment name") + updateFlags := flag.NewFlagSet("", flag.ContinueOnError) updateFlags.StringVar(&policyFn, "policy", "", "set the ROFL application policy") updateFlags.StringVar(&adminAddress, "admin", "", "set the administrator address") + updateCmd.Flags().AddFlagSet(deploymentFlags) initCmd.Flags().AddFlagSet(common.SelectorFlags) initCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + initCmd.Flags().AddFlagSet(deploymentFlags) initCmd.Flags().StringVar(&appTEE, "tee", "tdx", "TEE kind [tdx, sgx]") initCmd.Flags().StringVar(&appKind, "kind", "container", "ROFL app kind [container, raw]") initCmd.Flags().StringVar(&scheme, "scheme", "cn", "app ID generation scheme: creator+round+index [cri] or creator+nonce [cn]") createCmd.Flags().AddFlagSet(common.SelectorFlags) createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + createCmd.Flags().AddFlagSet(deploymentFlags) createCmd.Flags().StringVar(&scheme, "scheme", "cn", "app ID generation scheme: creator+round+index [cri] or creator+nonce [cn]") updateCmd.Flags().AddFlagSet(common.SelectorFlags) updateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + updateCmd.Flags().AddFlagSet(deploymentFlags) updateCmd.Flags().AddFlagSet(updateFlags) removeCmd.Flags().AddFlagSet(common.SelectorFlags) removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + removeCmd.Flags().AddFlagSet(deploymentFlags) showCmd.Flags().AddFlagSet(common.SelectorFlags) + showCmd.Flags().AddFlagSet(deploymentFlags) } From aaf57f03bc8ed88e62e45e1f0cec4bc0ae17657c Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Tue, 14 Jan 2025 12:21:01 +0100 Subject: [PATCH 10/20] feat(cmd/rofl): Add support for build scripts --- build/rofl/manifest.go | 9 +++++++++ build/rofl/manifest_test.go | 5 +++++ cmd/rofl/build/build.go | 11 +++++++++++ cmd/rofl/build/scripts.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 cmd/rofl/build/scripts.go diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index d4d9487f..01b63de5 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -32,6 +32,12 @@ const ( TEETypeTDX = "tdx" ) +// Well-known scripts. +const ( + ScriptBuildPre = "build-pre" + ScriptBuildPost = "build-post" +) + // Manifest is the ROFL app manifest that configures various aspects of the app in a single place. type Manifest struct { // Name is the human readable ROFL app name. @@ -50,6 +56,9 @@ type Manifest struct { // Deployments are the ROFL app deployments. Deployments map[string]*Deployment `yaml:"deployments" json:"deployments"` + // Scripts are custom scripts that are executed by the build system at specific stages. + Scripts map[string]string `yaml:"scripts,omitempty" json:"scripts,omitempty"` + // sourceFn is the filename from which the manifest has been loaded. sourceFn string } diff --git a/build/rofl/manifest_test.go b/build/rofl/manifest_test.go index a901fc66..11368ac1 100644 --- a/build/rofl/manifest_test.go +++ b/build/rofl/manifest_test.go @@ -129,6 +129,9 @@ deployments: - any: {} fees: endorsing_node max_expiration: 3 +scripts: + pre-build: foo + post-build: bar ` func TestManifestSerialization(t *testing.T) { @@ -155,6 +158,8 @@ func TestManifestSerialization(t *testing.T) { require.Equal("bar", m.Deployments["default"].ParaTime) require.Equal("blah", m.Deployments["default"].Admin) require.EqualValues(24805610, m.Deployments["default"].TrustRoot.Height) + require.Equal("foo", m.Scripts["pre-build"]) + require.Equal("bar", m.Scripts["post-build"]) enc, err := yaml.Marshal(m) require.NoError(err, "yaml.Marshal") diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index cf341a23..a716835c 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -76,6 +76,15 @@ var ( return } + // Setup some build environment variables. + os.Setenv("ROFL_MANIFEST", manifest.SourceFileName()) + os.Setenv("ROFL_DEPLOYMENT", deploymentName) + os.Setenv("ROFL_DEPLOYMENT_NETWORK", deployment.Network) + os.Setenv("ROFL_DEPLOYMENT_PARATIME", deployment.ParaTime) + os.Setenv("ROFL_TMPDIR", tmpDir) + + runScript(manifest, buildRofl.ScriptBuildPre) + switch manifest.TEE { case buildRofl.TEETypeSGX: // SGX. @@ -102,6 +111,8 @@ var ( return } + runScript(manifest, buildRofl.ScriptBuildPost) + // Write the bundle out. outFn := fmt.Sprintf("%s.%s.orc", manifest.Name, deploymentName) if outputFn != "" { diff --git a/cmd/rofl/build/scripts.go b/cmd/rofl/build/scripts.go new file mode 100644 index 00000000..a49deecd --- /dev/null +++ b/cmd/rofl/build/scripts.go @@ -0,0 +1,33 @@ +package build + +import ( + "fmt" + "os" + "os/exec" + + "github.com/spf13/cobra" + + buildRofl "github.com/oasisprotocol/cli/build/rofl" +) + +// runScripts executes the specified build script using the current build environment. +func runScript(manifest *buildRofl.Manifest, name string) { + script, ok := manifest.Scripts[name] + if !ok { + return + } + + fmt.Printf("Running script '%s'...\n", name) + + cmd := exec.Command( //nolint: gosec + os.Getenv("SHELL"), + "-c", + script, + ) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + cobra.CheckErr(fmt.Errorf("script '%s' failed to execute: %w", name, err)) + } +} From 30992cbdc8aaf4d29ddd352074ee39eb4126ad35 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Thu, 16 Jan 2025 14:07:23 +0100 Subject: [PATCH 11/20] feat(cmd/rofl): Add support for persistent storage --- cmd/rofl/build/container.go | 4 ++-- cmd/rofl/build/tdx.go | 29 ++++++++++++++++++----------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go index e6991667..3bc15ae0 100644 --- a/cmd/rofl/build/container.go +++ b/cmd/rofl/build/container.go @@ -13,9 +13,9 @@ const ( artifactContainerRuntime = "rofl-container runtime" artifactContainerCompose = "compose.yaml" - defaultContainerStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage2-podman.tar.bz2#d84e0ca961fb0913b73a50ed90eb743af24b3d0acecdbe2594650e2801b41171" + defaultContainerStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/stage2-podman.tar.bz2#4239991c742c189f9f6949e597276e9ae5972768c4bfb5108190565e2addf47e" - defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.1.0/rofl-containers#89d533f8c2c0a8015fdc269ae350ab5b6a271c3aa6b17dd2c1ea49b3ffff2e06" + defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.2.0/rofl-containers#d6f60ce41a33e8240396596037094816cdcd40abd1813d6dc26988d557bceccd" defaultContainerComposeURI = "compose.yaml" ) diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 22f37266..786a61a7 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -22,9 +22,9 @@ const ( artifactKernel = "kernel" artifactStage2 = "stage 2 template" - defaultFirmwareURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/ovmf.tdx.fd#db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f" - defaultKernelURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage1.bin#029255ff97cd0e6e3be04372578e7c980a8b8c0138b8153afc047cca98fe6008" - defaultStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.0/stage2-basic.tar.bz2#2dfbc01d62744052afa95feb737d5a0d6a68e2b58d71743751c4e3fc5faf4d36" + defaultFirmwareURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/ovmf.tdx.fd#db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f" + defaultKernelURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/stage1.bin#b2a7e4b92b6d41c5ac21b3291d740a3fc07551af96faba64439f6628101e2096" + defaultStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/stage2-basic.tar.bz2#f0f2fab9747584258ed9292417bea4e4df75fb8d5e0ecd1653269d65914d1e8a" ) var ( @@ -231,12 +231,12 @@ func tdxBundleComponent( }, } - tmpStorageKind := buildRofl.StorageKindNone + storageKind := buildRofl.StorageKindNone if manifest.Resources.Storage != nil { - tmpStorageKind = manifest.Resources.Storage.Kind + storageKind = manifest.Resources.Storage.Kind } - switch tmpStorageKind { + switch storageKind { case buildRofl.StorageKindNone: case buildRofl.StorageKindRAM: comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, @@ -252,18 +252,25 @@ func tdxBundleComponent( return err } - if tmpStorageKind == buildRofl.StorageKindDiskPersistent { - // TODO: For persistent disk, configure Stage2Persist flag and storage mode. - return fmt.Errorf("persistent disk not yet supported, use 'disk-ephemeral'") + var storageMode string + switch storageKind { + case buildRofl.StorageKindDiskPersistent: + // Persistent storage needs to be set up by stage 2. + storageMode = "custom" + + // TODO: (Oasis Core 25.0+) Set comp.TDX.Stage2Persist = true + case buildRofl.StorageKindDiskEphemeral: + // Ephemeral storage can be set up by stage 1 directly. + storageMode = "disk" } comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, - "oasis.stage2.storage_mode=disk", + fmt.Sprintf("oasis.stage2.storage_mode=%s", storageMode), fmt.Sprintf("oasis.stage2.storage_size=%d", storageSize/sectorSize), fmt.Sprintf("oasis.stage2.storage_offset=%d", storageOffset/sectorSize), ) default: - return fmt.Errorf("unsupported storage mode: %s", tmpStorageKind) + return fmt.Errorf("unsupported storage mode: %s", storageKind) } // TODO: (Oasis Core 25.0+) Use qcow2 image format to support sparse files. From 819e8f13649c2a5d38790134d087fcadcaa46c9e Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Thu, 16 Jan 2025 14:08:14 +0100 Subject: [PATCH 12/20] feat(cmd/rofl): Separate init from create --- build/rofl/manifest.go | 28 +++++++++---- build/rofl/manifest_test.go | 4 +- cmd/rofl/build/build.go | 14 ++++--- cmd/rofl/common/manifest.go | 5 ++- cmd/rofl/mgmt.go | 84 ++++++++++++++++++++++--------------- 5 files changed, 86 insertions(+), 49 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 01b63de5..6df57062 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -162,6 +162,16 @@ func (m *Manifest) SourceFileName() string { return m.sourceFn } +// Save serializes the manifest and writes it to the file returned by `SourceFileName`, overwriting +// any previous manifest. +func (m *Manifest) Save() error { + data, err := yaml.Marshal(m) + if err != nil { + return err + } + return os.WriteFile(m.SourceFileName(), data, 0o644) //nolint: gosec +} + // DefaultDeploymentName is the name of the default deployment that must always be defined and is // used in case no deployment is passed. const DefaultDeploymentName = "default" @@ -169,7 +179,7 @@ const DefaultDeploymentName = "default" // Deployment describes a single ROFL app deployment. type Deployment struct { // AppID is the Bech32-encoded ROFL app ID. - AppID string `yaml:"app_id" json:"app_id"` + AppID string `yaml:"app_id,omitempty" json:"app_id,omitempty"` // Network is the identifier of the network to deploy to. Network string `yaml:"network" json:"network"` // ParaTime is the identifier of the paratime to deploy to. @@ -184,12 +194,11 @@ type Deployment struct { // Validate validates the manifest for correctness. func (d *Deployment) Validate() error { - if len(d.AppID) == 0 { - return fmt.Errorf("app ID cannot be empty") - } - var appID rofl.AppID - if err := appID.UnmarshalText([]byte(d.AppID)); err != nil { - return fmt.Errorf("malformed app ID: %w", err) + if len(d.AppID) > 0 { + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(d.AppID)); err != nil { + return fmt.Errorf("malformed app ID: %w", err) + } } if d.Network == "" { return fmt.Errorf("network cannot be empty") @@ -200,6 +209,11 @@ func (d *Deployment) Validate() error { return nil } +// HasAppID returns true iff the deployment has an application identifier set. +func (d *Deployment) HasAppID() bool { + return len(d.AppID) > 0 +} + // TrustRootConfig is the trust root configuration. type TrustRootConfig struct { // Height is the consensus layer block height where to take the trust root. diff --git a/build/rofl/manifest_test.go b/build/rofl/manifest_test.go index 11368ac1..77ed2d01 100644 --- a/build/rofl/manifest_test.go +++ b/build/rofl/manifest_test.go @@ -56,12 +56,12 @@ func TestManifestValidation(t *testing.T) { err = m.Validate() require.ErrorContains(err, "must define at least the 'default' deployment") - // Missing app ID in deployment. + // Missing network in deployment. m.Deployments = map[string]*Deployment{ "default": {}, } err = m.Validate() - require.ErrorContains(err, "bad deployment 'default': app ID cannot be empty") + require.ErrorContains(err, "bad deployment 'default': network cannot be empty") // Invalid app ID. m.Deployments["default"].AppID = "foo" diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index a716835c..b0177063 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -8,7 +8,6 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" - "gopkg.in/yaml.v3" coreCommon "github.com/oasisprotocol/oasis-core/go/common" "github.com/oasisprotocol/oasis-core/go/common/cbor" @@ -45,7 +44,7 @@ var ( Run: func(_ *cobra.Command, _ []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) - manifest, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + manifest, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, true) fmt.Println("Building a ROFL application...") fmt.Printf("Deployment: %s\n", deploymentName) @@ -143,11 +142,16 @@ var ( // Ask the user to update the manifest manually. fmt.Println("Update the manifest with the following identities to use the new app:") fmt.Println() + fmt.Printf("deployments:\n") + fmt.Printf(" %s:\n", deploymentName) + fmt.Printf(" policy:\n") + fmt.Printf(" enclaves:\n") for _, enclaveID := range eids { data, _ := enclaveID.MarshalText() - fmt.Printf("- \"%s\"\n", string(data)) + fmt.Printf(" - \"%s\"\n", string(data)) } fmt.Println() + fmt.Println("Next time you can also use the --update-manifest flag to apply changes.") case true: // Update the manifest with the given enclave identities, overwriting existing ones. deployment.Policy.Enclaves = make([]sgx.EnclaveIdentity, 0, len(eids)) @@ -155,9 +159,7 @@ var ( deployment.Policy.Enclaves = append(deployment.Policy.Enclaves, *eid) } - // Serialize manifest and write it to file. - data, _ := yaml.Marshal(manifest) - if err = os.WriteFile(manifest.SourceFileName(), data, 0o644); err != nil { //nolint: gosec + if err = manifest.Save(); err != nil { cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err)) } } diff --git a/cmd/rofl/common/manifest.go b/cmd/rofl/common/manifest.go index d6bb9122..4482c53c 100644 --- a/cmd/rofl/common/manifest.go +++ b/cmd/rofl/common/manifest.go @@ -14,9 +14,12 @@ import ( // selection. // // In case there is an error in loading the manifest, it aborts the application. -func LoadManifestAndSetNPA(cfg *config.Config, npa *common.NPASelection, deployment string) (*rofl.Manifest, *rofl.Deployment) { +func LoadManifestAndSetNPA(cfg *config.Config, npa *common.NPASelection, deployment string, needAppID bool) (*rofl.Manifest, *rofl.Deployment) { manifest, d, err := MaybeLoadManifestAndSetNPA(cfg, npa, deployment) cobra.CheckErr(err) + if needAppID && !d.HasAppID() { + cobra.CheckErr(fmt.Errorf("deployment '%s' does not have an app ID set, maybe you need to run `oasis rofl create`", deployment)) + } return manifest, d } diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 1363b4c5..57dc0780 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "github.com/spf13/cobra" flag "github.com/spf13/pflag" @@ -36,11 +37,12 @@ var ( appTEE string appKind string deploymentName string + doUpdate bool initCmd = &cobra.Command{ - Use: "init [--tee TEE] [--kind KIND]", - Short: "Create a new ROFL app and initialize the manifest", - Args: cobra.ExactArgs(1), + Use: "init [] [--tee TEE] [--kind KIND]", + Short: "Initialize a ROFL app manifest", + Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -57,7 +59,15 @@ var ( } // TODO: Support an interactive mode. - appName := args[0] + var appName string + if len(args) > 0 { + appName = args[0] + } else { + // Infer from current directory. + wd, err := os.Getwd() + cobra.CheckErr(err) + appName = filepath.Base(wd) + } // Fail in case there is an existing manifest. if buildRofl.ManifestExists() { cobra.CheckErr("refusing to overwrite existing manifest") @@ -73,7 +83,6 @@ var ( // Generate manifest and a default policy which does not accept any enclaves. deployment := &buildRofl.Deployment{ - AppID: rofl.NewAppIDGlobalName("").String(), // Temporary for initial validation. Network: npa.NetworkName, ParaTime: npa.ParaTimeName, Admin: npa.AccountName, @@ -104,7 +113,7 @@ var ( Memory: 512, CPUCount: 1, Storage: &buildRofl.StorageConfig{ - Kind: buildRofl.StorageKindDiskEphemeral, + Kind: buildRofl.StorageKindDiskPersistent, Size: 512, }, }, @@ -125,32 +134,15 @@ var ( fmt.Printf(" ParaTime: %s\n", deployment.ParaTime) fmt.Printf(" Admin: %s\n", deployment.Admin) - idScheme, ok := identifierSchemes[scheme] - if !ok { - cobra.CheckErr(fmt.Errorf("unknown scheme %s", scheme)) - } - - // Register a new ROFL application to determine the identifier. - tx := rofl.NewCreateTx(nil, &rofl.Create{ - Policy: *deployment.Policy, - Scheme: idScheme, - }) - - acc := common.LoadAccount(cfg, npa.AccountName) - sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, nil) - cobra.CheckErr(err) - - var appID rofl.AppID - common.BroadcastTransaction(ctx, npa.ParaTime, conn, sigTx, meta, &appID) - deployment.AppID = appID.String() - - fmt.Printf("Created ROFL application: %s\n", appID) - // Serialize manifest and write it to file. + const manifestFn = "rofl.yaml" data, _ := yaml.Marshal(manifest) - if err = os.WriteFile("rofl.yml", data, 0o644); err != nil { //nolint: gosec + if err = os.WriteFile(manifestFn, data, 0o644); err != nil { //nolint: gosec cobra.CheckErr(fmt.Errorf("failed to write manifest: %w", err)) } + + fmt.Printf("Created manifest in '%s'.\n", manifestFn) + fmt.Printf("Run `oasis rofl create --update-manifest` to register your ROFL app and configure an app ID.\n") }, } @@ -163,11 +155,15 @@ var ( npa := common.GetNPASelection(cfg) txCfg := common.GetTransactionConfig() - var policy *rofl.AppAuthPolicy + var ( + policy *rofl.AppAuthPolicy + manifest *buildRofl.Manifest + deployment *buildRofl.Deployment + ) if len(args) > 0 { policy = loadPolicy(args[0]) } else { - _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + manifest, deployment = roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, false) policy = deployment.Policy } @@ -208,6 +204,27 @@ var ( } fmt.Printf("Created ROFL application: %s\n", appID) + + if deployment != nil { + switch doUpdate { + case false: + // Ask the user to update the manifest manually. + fmt.Println("Update the manifest with the following app identifier to use the new app:") + fmt.Println() + fmt.Printf("deployments:\n") + fmt.Printf(" %s:\n", deploymentName) + fmt.Printf(" app_id: %s\n", appID) + fmt.Println() + fmt.Println("Next time you can also use the --update-manifest flag to apply changes.") + case true: + // Update the manifest with the given enclave identities, overwriting existing ones. + deployment.AppID = appID.String() + + if err = manifest.Save(); err != nil { + cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err)) + } + } + } }, } @@ -228,7 +245,7 @@ var ( rawAppID = args[0] policy = loadPolicy(policyFn) } else { - _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, true) rawAppID = deployment.AppID if adminAddress == "" && deployment.Admin != "" { @@ -305,7 +322,7 @@ var ( if len(args) > 0 { rawAppID = args[0] } else { - _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, true) rawAppID = deployment.AppID } var appID rofl.AppID @@ -354,7 +371,7 @@ var ( if len(args) > 0 { rawAppID = args[0] } else { - _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName) + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, true) rawAppID = deployment.AppID } var appID rofl.AppID @@ -435,6 +452,7 @@ func init() { createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) createCmd.Flags().AddFlagSet(deploymentFlags) createCmd.Flags().StringVar(&scheme, "scheme", "cn", "app ID generation scheme: creator+round+index [cri] or creator+nonce [cn]") + createCmd.Flags().BoolVar(&doUpdate, "update-manifest", false, "automatically update the manifest") updateCmd.Flags().AddFlagSet(common.SelectorFlags) updateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) From ae8920c96648d6133abd631015b2bc66a72a780a Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Thu, 16 Jan 2025 14:14:30 +0100 Subject: [PATCH 13/20] feat(cmd/rofl): Verify integrity of cached artifacts --- cmd/rofl/build/artifacts.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/rofl/build/artifacts.go b/cmd/rofl/build/artifacts.go index 7e44418c..b4a30547 100644 --- a/cmd/rofl/build/artifacts.go +++ b/cmd/rofl/build/artifacts.go @@ -63,7 +63,16 @@ func maybeDownloadArtifact(kind, uri string) string { switch { case err == nil: // Already exists in cache. - // TODO: Verify checksum and discard if invalid. + if knownHash != "" { + h := sha256.New() + if _, err = io.Copy(h, f); err != nil { + cobra.CheckErr(fmt.Errorf("failed to verify cached %s artifact: %w", kind, err)) + } + artifactHash := fmt.Sprintf("%x", h.Sum(nil)) + if artifactHash != knownHash { + cobra.CheckErr(fmt.Errorf("corrupted cached %s artifact file '%s' (expected: %s got: %s)", kind, cacheFn, knownHash, artifactHash)) + } + } f.Close() fmt.Printf(" (using cached artifact)\n") From f8934ff44b4260df2bf359c00dd7bbf7eba660c6 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Thu, 16 Jan 2025 14:25:52 +0100 Subject: [PATCH 14/20] feat(cmd/rofl): Add bundle-post script --- build/rofl/manifest.go | 5 +++-- cmd/rofl/build/build.go | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 6df57062..04099f19 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -34,8 +34,9 @@ const ( // Well-known scripts. const ( - ScriptBuildPre = "build-pre" - ScriptBuildPost = "build-post" + ScriptBuildPre = "build-pre" + ScriptBuildPost = "build-post" + ScriptBundlePost = "bundle-post" ) // Manifest is the ROFL app manifest that configures various aspects of the app in a single place. diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index b0177063..8b6e3b53 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -77,7 +77,7 @@ var ( // Setup some build environment variables. os.Setenv("ROFL_MANIFEST", manifest.SourceFileName()) - os.Setenv("ROFL_DEPLOYMENT", deploymentName) + os.Setenv("ROFL_DEPLOYMENT_NAME", deploymentName) os.Setenv("ROFL_DEPLOYMENT_NETWORK", deployment.Network) os.Setenv("ROFL_DEPLOYMENT_PARATIME", deployment.ParaTime) os.Setenv("ROFL_TMPDIR", tmpDir) @@ -132,6 +132,15 @@ var ( return } + // Setup some post-bundle environment variables. + os.Setenv("ROFL_BUNDLE", outFn) + for idx, enclaveID := range eids { + data, _ := enclaveID.MarshalText() + os.Setenv(fmt.Sprintf("ROFL_ENCLAVE_ID_%d", idx), string(data)) + } + + runScript(manifest, buildRofl.ScriptBundlePost) + // Override the update manifest flag in case the policy does not exist. if deployment.Policy == nil { doUpdate = false From 8fc716b42efa6e9dd6ed6857e0d9e264f2f155ea Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Fri, 17 Jan 2025 13:35:09 +0100 Subject: [PATCH 15/20] feat(cmd/rofl): Pad rootfs partition to allow for growth during upgrades --- cmd/rofl/build/artifacts.go | 16 ++++++++++++++++ cmd/rofl/build/tdx.go | 22 ++++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/cmd/rofl/build/artifacts.go b/cmd/rofl/build/artifacts.go index b4a30547..aa188aa2 100644 --- a/cmd/rofl/build/artifacts.go +++ b/cmd/rofl/build/artifacts.go @@ -364,6 +364,22 @@ func concatFiles(a, b string) error { return err } +// padWithEmptySpace pads the given file with empty space to make it the given size. See +// `appendEmptySpace` for details. +func padWithEmptySpace(fn string, size uint64) error { + fi, err := os.Stat(fn) + if err != nil { + return err + } + + currentSize := uint64(fi.Size()) + if currentSize >= size { + return nil + } + _, err = appendEmptySpace(fn, size-currentSize, 1) + return err +} + // appendEmptySpace appends empty space to the given file. If the filesystem supports sparse files, // this should not actually take any extra space. // diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 786a61a7..36d817b2 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -244,26 +244,32 @@ func tdxBundleComponent( fmt.Sprintf("oasis.stage2.storage_size=%d", manifest.Resources.Storage.Size*1024*1024), ) case buildRofl.StorageKindDiskEphemeral, buildRofl.StorageKindDiskPersistent: - // Allocate some space after regular stage2. - const sectorSize = 512 - storageSize := manifest.Resources.Storage.Size * 1024 * 1024 - storageOffset, err := appendEmptySpace(stage2.fn, storageSize, sectorSize) - if err != nil { - return err - } - var storageMode string switch storageKind { case buildRofl.StorageKindDiskPersistent: // Persistent storage needs to be set up by stage 2. storageMode = "custom" + // Add some sparse padding to allow for growth of the root partition during upgrades. + // Note that this will not actually take any space so it could be arbitrarily large. + if err := padWithEmptySpace(stage2.fn, 256*1024*1024); err != nil { + return err + } + // TODO: (Oasis Core 25.0+) Set comp.TDX.Stage2Persist = true case buildRofl.StorageKindDiskEphemeral: // Ephemeral storage can be set up by stage 1 directly. storageMode = "disk" } + // Allocate some space after regular stage2. + const sectorSize = 512 + storageSize := manifest.Resources.Storage.Size * 1024 * 1024 + storageOffset, err := appendEmptySpace(stage2.fn, storageSize, sectorSize) + if err != nil { + return err + } + comp.TDX.ExtraKernelOptions = append(comp.TDX.ExtraKernelOptions, fmt.Sprintf("oasis.stage2.storage_mode=%s", storageMode), fmt.Sprintf("oasis.stage2.storage_size=%d", storageSize/sectorSize), From 1fd79c15ca04acb816088d019964f332bfd64221 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Tue, 21 Jan 2025 09:47:21 +0100 Subject: [PATCH 16/20] feat(cmd/rofl): Add support for secret management --- build/rofl/manifest.go | 9 ++ build/rofl/secrets.go | 107 ++++++++++++++++++++++ cmd/rofl/build/container.go | 4 +- cmd/rofl/build/tdx.go | 6 +- cmd/rofl/mgmt.go | 178 +++++++++++++++++++++++++++++++++++- cmd/rofl/rofl.go | 1 + 6 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 build/rofl/secrets.go diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 04099f19..d6a99ead 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -191,6 +191,10 @@ type Deployment struct { TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"` // Policy is the ROFL app policy. Policy *rofl.AppAuthPolicy `yaml:"policy,omitempty" json:"policy,omitempty"` + // Metadata contains custom metadata. + Metadata map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"` + // Secrets contains encrypted secrets. + Secrets []*SecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"` } // Validate validates the manifest for correctness. @@ -207,6 +211,11 @@ func (d *Deployment) Validate() error { if d.ParaTime == "" { return fmt.Errorf("paratime cannot be empty") } + for _, s := range d.Secrets { + if err := s.Validate(); err != nil { + return fmt.Errorf("bad secret: %w", err) + } + } return nil } diff --git a/build/rofl/secrets.go b/build/rofl/secrets.go new file mode 100644 index 00000000..47d12738 --- /dev/null +++ b/build/rofl/secrets.go @@ -0,0 +1,107 @@ +package rofl + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/oasisprotocol/curve25519-voi/primitives/x25519" + "github.com/oasisprotocol/deoxysii" + "github.com/oasisprotocol/oasis-core/go/common/cbor" + mraeDeoxysii "github.com/oasisprotocol/oasis-core/go/common/crypto/mrae/deoxysii" +) + +// SecretConfig is the configuration of a given secret. +type SecretConfig struct { + // Name is the name of the secret. + Name string `yaml:"name" json:"name"` + // PublicName is the public name of the secret. It will be visible to everyone on-chain, but is + // otherwise ignored. + PublicName string `yaml:"public_name,omitempty" json:"public_name,omitempty"` + // Value is the Base64-encoded encrypted value. + Value string `yaml:"value" json:"value"` +} + +// Validate validates the secret configuration for correctness. +func (s *SecretConfig) Validate() error { + if s == nil { + return fmt.Errorf("secret cannot be nil") + } + if s.Name == "" { + return fmt.Errorf("name cannot be empty") + } + if len(s.Value) == 0 { + return fmt.Errorf("value cannot be empty") + } + if _, err := base64.StdEncoding.DecodeString(s.Value); err != nil { + return fmt.Errorf("malformed value: %w", err) + } + return nil +} + +// SecretEnvelope is the envelope used for storing encrypted secrets. +type SecretEnvelope struct { + // Pk is the ephemeral public key used for X25519. + Pk x25519.PublicKey `json:"pk"` + // Nonce. + Nonce [deoxysii.NonceSize]byte `json:"nonce"` + // Name is the encrypted secret name. + Name []byte `json:"name"` + // Value is the encrypted secret value. + Value []byte `json:"value"` +} + +// EncryptSecret encrypts the given secret given its plain-text name and value together with the +// secrets encryption key (SEK) obtained for the given application. Returns the Base64-encoded +// value that can be used in the configuration. +func EncryptSecret(name string, value []byte, sek x25519.PublicKey) (string, error) { + // Generate ephemeral X25519 key pair. + pk, sk, err := x25519.GenerateKey(rand.Reader) + if err != nil { + return "", fmt.Errorf("failed to generate ephemeral X25519 key pair: %w", err) + } + + // Generate random nonce. + var nonce [deoxysii.NonceSize]byte + if _, err := rand.Read(nonce[:]); err != nil { + return "", fmt.Errorf("failed to generate random nonce: %w", err) + } + + // Seal plain-text secret name and value. + encName := mraeDeoxysii.Box.Seal(nil, nonce[:], []byte(name), []byte("name"), &sek, sk) + encValue := mraeDeoxysii.Box.Seal(nil, nonce[:], value, []byte("value"), &sek, sk) + + envelope := SecretEnvelope{ + Pk: *pk, + Nonce: nonce, + Name: encName, + Value: encValue, + } + data := cbor.Marshal(envelope) + return base64.StdEncoding.EncodeToString(data), nil +} + +// PrepareSecrets transforms the secrets configuration into a format suitable for updating the ROFL +// app configuration. +// +// Panics in case the configuration is malformed. +func PrepareSecrets(cfg []*SecretConfig) map[string][]byte { + if len(cfg) == 0 { + return nil + } + + out := make(map[string][]byte) + for _, sc := range cfg { + name := sc.Name + if sc.PublicName != "" { + name = sc.PublicName + } + + data, err := base64.StdEncoding.DecodeString(sc.Value) + if err != nil { + panic(err) // Should not happen as the configuration has been validated. + } + out[name] = data + } + return out +} diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go index 3bc15ae0..17d30f8b 100644 --- a/cmd/rofl/build/container.go +++ b/cmd/rofl/build/container.go @@ -13,9 +13,9 @@ const ( artifactContainerRuntime = "rofl-container runtime" artifactContainerCompose = "compose.yaml" - defaultContainerStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/stage2-podman.tar.bz2#4239991c742c189f9f6949e597276e9ae5972768c4bfb5108190565e2addf47e" + defaultContainerStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.3/stage2-podman.tar.bz2#827531546f3db6b0945ece7ddab4e10d648eaa3ba1c146b7889d7cb9cbf0b507" - defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.2.0/rofl-containers#d6f60ce41a33e8240396596037094816cdcd40abd1813d6dc26988d557bceccd" + defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.3.3/rofl-containers#b7f025e3bb844a4ce044fa3a2503f6854e5e2d2d5ec22be919c582e57cf5d6ab" defaultContainerComposeURI = "compose.yaml" ) diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 36d817b2..7d51e18e 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -22,9 +22,9 @@ const ( artifactKernel = "kernel" artifactStage2 = "stage 2 template" - defaultFirmwareURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/ovmf.tdx.fd#db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f" - defaultKernelURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/stage1.bin#b2a7e4b92b6d41c5ac21b3291d740a3fc07551af96faba64439f6628101e2096" - defaultStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.1/stage2-basic.tar.bz2#f0f2fab9747584258ed9292417bea4e4df75fb8d5e0ecd1653269d65914d1e8a" + defaultFirmwareURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.3/ovmf.tdx.fd#db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f" + defaultKernelURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.3/stage1.bin#539f25c66a27b2ca3c6b4d3333b88c64e531fcc96776c37a12c9ce06dd7fbac9" + defaultStage2TemplateURI = "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.3.3/stage2-basic.tar.bz2#72c84d2566959799fdd98fae08c143a8572a5a09ee426be376f9a8bbd1675f2b" ) var ( diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 57dc0780..2cbf40b3 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "os" "path/filepath" @@ -33,6 +34,7 @@ var ( policyFn string scheme string adminAddress string + pubName string appTEE string appKind string @@ -229,7 +231,7 @@ var ( } updateCmd = &cobra.Command{ - Use: "update [ --policy --admin
]", + Use: "update []", Short: "Update an existing ROFL application", Args: cobra.MaximumNArgs(1), Run: func(_ *cobra.Command, args []string) { @@ -240,6 +242,8 @@ var ( var ( rawAppID string policy *rofl.AppAuthPolicy + metadata map[string]string + secrets map[string][]byte ) if len(args) > 0 { rawAppID = args[0] @@ -252,6 +256,8 @@ var ( adminAddress = "self" } policy = deployment.Policy + metadata = deployment.Metadata + secrets = buildRofl.PrepareSecrets(deployment.Secrets) } var appID rofl.AppID if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { @@ -284,8 +290,10 @@ var ( } updateBody := rofl.Update{ - ID: appID, - Policy: *policy, + ID: appID, + Policy: *policy, + Metadata: metadata, + Secrets: secrets, } // Update administrator address. @@ -397,6 +405,21 @@ var ( } stakedAmnt := helpers.FormatParaTimeDenomination(npa.ParaTime, appCfg.Stake) fmt.Printf("Staked amount: %s\n", stakedAmnt) + + if len(appCfg.Metadata) > 0 { + fmt.Printf("Metadata:\n") + for key, value := range appCfg.Metadata { + fmt.Printf(" %s: %s\n", key, value) + } + } + + if len(appCfg.Secrets) > 0 { + fmt.Printf("Secrets:\n") + for key, value := range appCfg.Secrets { + fmt.Printf(" %s: [%d bytes]\n", key, len(value)) + } + } + fmt.Printf("Policy:\n") policyJSON, _ := json.MarshalIndent(appCfg.Policy, " ", " ") fmt.Printf(" %s\n", string(policyJSON)) @@ -418,6 +441,145 @@ var ( } }, } + + secretCmd = &cobra.Command{ + Use: "secret", + Short: "Encrypted secret management commands", + } + + secretSetCmd = &cobra.Command{ + Use: "set |- [--public-name ]", + Short: "Encrypt the given secret into the manifest, reading the value from file or stdin", + Args: cobra.ExactArgs(2), + Run: func(_ *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + secretName := args[0] + secretFn := args[1] + + manifest, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, true) + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(deployment.AppID)); err != nil { + cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) + } + + // Establish connection with the target network. + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + + appCfg, err := conn.Runtime(npa.ParaTime).ROFL.App(ctx, client.RoundLatest, appID) + cobra.CheckErr(err) + + // Read secret. + var secretValue []byte + if secretFn == "-" { + secretValue, err = io.ReadAll(os.Stdin) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to read secrets from standard input: %w", err)) + } + } else { + secretValue, err = os.ReadFile(secretFn) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to read secrets from file: %w", err)) + } + } + + // Encrypt the secret. + encValue, err := buildRofl.EncryptSecret(secretName, secretValue, appCfg.SEK) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to encrypt secret: %w", err)) + } + + secretCfg := buildRofl.SecretConfig{ + Name: secretName, + Value: encValue, + } + if pubName != "" { + secretCfg.PublicName = pubName + } + for _, sc := range deployment.Secrets { + if sc.Name == secretName { + cobra.CheckErr(fmt.Errorf("secret named '%s' already exists for deployment '%s'", secretName, deploymentName)) + } + } + deployment.Secrets = append(deployment.Secrets, &secretCfg) + + // Update manifest. + if err = manifest.Save(); err != nil { + cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err)) + } + }, + } + + secretGetCmd = &cobra.Command{ + Use: "get ", + Short: "Show metadata about the given secret", + Args: cobra.ExactArgs(1), + Run: func(_ *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + secretName := args[0] + + _, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, true) + var secret *buildRofl.SecretConfig + for _, sc := range deployment.Secrets { + if sc.Name != secretName { + continue + } + secret = sc + break + } + if secret == nil { + cobra.CheckErr(fmt.Errorf("secret named '%s' does not exist for deployment '%s'", secretName, deploymentName)) + return // Lint doesn't know that cobra.CheckErr never returns. + } + + fmt.Printf("Name: %s\n", secret.Name) + if secret.PublicName != "" { + fmt.Printf("Public name: %s\n", secret.PublicName) + } + fmt.Printf("Size: %d bytes\n", len(secret.Value)) + }, + } + + secretRmCmd = &cobra.Command{ + Use: "rm ", + Short: "Remove the given secret from the manifest", + Args: cobra.ExactArgs(1), + Run: func(_ *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + secretName := args[0] + + manifest, deployment := roflCommon.LoadManifestAndSetNPA(cfg, npa, deploymentName, true) + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(deployment.AppID)); err != nil { + cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) + } + + var ( + newSecrets []*buildRofl.SecretConfig + found bool + ) + for _, sc := range deployment.Secrets { + if sc.Name == secretName { + found = true + continue + } + newSecrets = append(newSecrets, sc) + } + if !found { + cobra.CheckErr(fmt.Errorf("secret named '%s' does not exist for deployment '%s'", secretName, deploymentName)) + } + deployment.Secrets = newSecrets + + // Update manifest. + if err := manifest.Save(); err != nil { + cobra.CheckErr(fmt.Errorf("failed to update manifest: %w", err)) + } + }, + } ) func loadPolicy(fn string) *rofl.AppAuthPolicy { @@ -465,4 +627,14 @@ func init() { showCmd.Flags().AddFlagSet(common.SelectorFlags) showCmd.Flags().AddFlagSet(deploymentFlags) + + secretSetCmd.Flags().AddFlagSet(deploymentFlags) + secretSetCmd.Flags().StringVar(&pubName, "public-name", "", "public secret name") + secretCmd.AddCommand(secretSetCmd) + + secretGetCmd.Flags().AddFlagSet(deploymentFlags) + secretCmd.AddCommand(secretGetCmd) + + secretRmCmd.Flags().AddFlagSet(deploymentFlags) + secretCmd.AddCommand(secretRmCmd) } diff --git a/cmd/rofl/rofl.go b/cmd/rofl/rofl.go index bd6435f2..36fb9a63 100644 --- a/cmd/rofl/rofl.go +++ b/cmd/rofl/rofl.go @@ -21,4 +21,5 @@ func init() { Cmd.AddCommand(trustRootCmd) Cmd.AddCommand(build.Cmd) Cmd.AddCommand(identityCmd) + Cmd.AddCommand(secretCmd) } From 4c8eb0a5f889ab8bd5579395fd93eb03202e3963 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Tue, 21 Jan 2025 11:26:58 +0100 Subject: [PATCH 17/20] feat(cmd/rofl): Move debug flag to manifest --- build/rofl/manifest.go | 2 ++ cmd/rofl/build/build.go | 34 ++++++++-------------------------- cmd/rofl/build/container.go | 1 - cmd/rofl/build/sgx.go | 3 +-- cmd/rofl/build/tdx.go | 3 +-- cmd/rofl/mgmt.go | 9 +++++++++ docs/rofl.md | 6 +----- 7 files changed, 22 insertions(+), 36 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index d6a99ead..a782c8ad 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -187,6 +187,8 @@ type Deployment struct { ParaTime string `yaml:"paratime" json:"paratime"` // Admin is the identifier of the admin account. Admin string `yaml:"admin,omitempty" json:"admin,omitempty"` + // Debug is a flag denoting whether this is a debuggable deployment. + Debug bool `yaml:"debug,omitempty" json:"debug,omitempty"` // TrustRoot is the optional trust root configuration. TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"` // Policy is the ROFL app policy. diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index 8b6e3b53..83642d6c 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -13,7 +13,6 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-core/go/common/sgx" "github.com/oasisprotocol/oasis-core/go/common/version" - consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" "github.com/oasisprotocol/oasis-core/go/runtime/bundle" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" @@ -27,7 +26,6 @@ import ( const ( buildModeProduction = "production" buildModeUnsafe = "unsafe" - buildModeAuto = "auto" ) var ( @@ -50,12 +48,20 @@ var ( fmt.Printf("Deployment: %s\n", deploymentName) fmt.Printf("Network: %s\n", deployment.Network) fmt.Printf("ParaTime: %s\n", deployment.ParaTime) + fmt.Printf("Debug: %v\n", deployment.Debug) fmt.Printf("App ID: %s\n", deployment.AppID) fmt.Printf("Name: %s\n", manifest.Name) fmt.Printf("Version: %s\n", manifest.Version) fmt.Printf("TEE: %s\n", manifest.TEE) fmt.Printf("Kind: %s\n", manifest.Kind) + switch deployment.Debug { + case true: + buildMode = buildModeUnsafe + case false: + buildMode = buildModeProduction + } + // Prepare temporary build directory. tmpDir, err := os.MkdirTemp("", "oasis-build") if err != nil { @@ -176,29 +182,6 @@ var ( } ) -func detectBuildMode(npa *common.NPASelection) { - // Configure build mode. In case auto is selected and not offline, query the network. If - // autodetection fails, default to production mode. - switch { - case buildMode == buildModeAuto && !offline: - ctx := context.Background() - conn, err := connection.Connect(ctx, npa.Network) - if err != nil { - cobra.CheckErr(fmt.Errorf("unable to autodetect build mode, please provide --mode flag manually (failed to connect to GRPC endpoint: %w)", err)) - } - - params, err := conn.Consensus().Registry().ConsensusParameters(ctx, consensus.HeightLatest) - if err != nil { - cobra.CheckErr(fmt.Errorf("unable to autodetect build mode, please provide --mode flag manually (failed to get consensus parameters: %w)", err)) - } - - if params.DebugAllowTestRuntimes { - buildMode = buildModeUnsafe - } - default: - } -} - func setupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection) { // Configure app ID. os.Setenv("ROFL_APP_ID", deployment.AppID) @@ -272,7 +255,6 @@ func fetchTrustRoot(npa *common.NPASelection, cfg *buildRofl.TrustRootConfig) (s func init() { buildFlags := flag.NewFlagSet("", flag.ContinueOnError) - buildFlags.StringVar(&buildMode, "mode", "auto", "build mode [production, unsafe, auto]") buildFlags.BoolVar(&offline, "offline", false, "do not perform any operations requiring network access") buildFlags.StringVar(&outputFn, "output", "", "output bundle filename") buildFlags.BoolVar(&doUpdate, "update-manifest", false, "automatically update the manifest") diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go index 17d30f8b..104a669d 100644 --- a/cmd/rofl/build/container.go +++ b/cmd/rofl/build/container.go @@ -45,7 +45,6 @@ func tdxBuildContainer( ) tdxOverrideArtifacts(manifest, wantedArtifacts) artifacts := tdxFetchArtifacts(wantedArtifacts) - detectBuildMode(npa) // Use the pre-built container runtime. initPath := artifacts[artifactContainerRuntime] diff --git a/cmd/rofl/build/sgx.go b/cmd/rofl/build/sgx.go index 5bb1747a..b20ea73b 100644 --- a/cmd/rofl/build/sgx.go +++ b/cmd/rofl/build/sgx.go @@ -32,7 +32,6 @@ func sgxBuild( ) { fmt.Println("Building an SGX-based Rust ROFL application...") - detectBuildMode(npa) features := sgxSetupBuildEnv(deployment, npa) // First build for the default target. @@ -218,7 +217,7 @@ func sgxSetupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection setupBuildEnv(deployment, npa) switch buildMode { - case buildModeProduction, buildModeAuto: + case buildModeProduction: // Production builds. fmt.Println("Building in production mode.") diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index 7d51e18e..5a4db56a 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -47,7 +47,6 @@ func tdxBuildRaw( fmt.Println("Building a TDX-based Rust ROFL application...") - detectBuildMode(npa) tdxSetupBuildEnv(deployment, npa) // Obtain package metadata. @@ -308,7 +307,7 @@ func tdxSetupBuildEnv(deployment *buildRofl.Deployment, npa *common.NPASelection setupBuildEnv(deployment, npa) switch buildMode { - case buildModeProduction, buildModeAuto: + case buildModeProduction: // Production builds. fmt.Println("Building in production mode.") diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 2cbf40b3..2d3e929d 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -83,11 +83,19 @@ var ( height, err := common.GetActualHeight(ctx, conn.Consensus()) cobra.CheckErr(err) + // Determine debug mode. + var debugMode bool + params, err := conn.Consensus().Registry().ConsensusParameters(ctx, height) + if err == nil { + debugMode = params.DebugAllowTestRuntimes + } + // Generate manifest and a default policy which does not accept any enclaves. deployment := &buildRofl.Deployment{ Network: npa.NetworkName, ParaTime: npa.ParaTimeName, Admin: npa.AccountName, + Debug: debugMode, Policy: &rofl.AppAuthPolicy{ Quotes: quote.Policy{ PCS: &pcs.QuotePolicy{ @@ -134,6 +142,7 @@ var ( fmt.Printf("Deployment '%s':\n", buildRofl.DefaultDeploymentName) fmt.Printf(" Network: %s\n", deployment.Network) fmt.Printf(" ParaTime: %s\n", deployment.ParaTime) + fmt.Printf(" Debug: %v\n", deployment.Debug) fmt.Printf(" Admin: %s\n", deployment.Admin) // Serialize manifest and write it to file. diff --git a/docs/rofl.md b/docs/rofl.md index 6713087d..5bf8db8d 100644 --- a/docs/rofl.md +++ b/docs/rofl.md @@ -22,16 +22,12 @@ The `build` command will execute a series of build commands depending on the target Trusted Execution Environment (TEE) and produce the Oasis Runtime Container (ORC) bundle. -Building a ROFL bundle requires a ROFL app manifest (`rofl.yml`) to be present +Building a ROFL bundle requires a ROFL app manifest (`rofl.yaml`) to be present in the current working directory. All information about what kind of ROFL app to build is specified in the manifest. Additionally, the following flags are available: -- `--mode` specifies a `production` (enabled SGX attestations suitable for the - Mainnet and Testnet) or `unsafe` build (using mocked SGX for debugging - and testing). The default behavior is set to `auto` which, based on the - selected [Network and ParaTime][npa], determines the build mode. - `--output` the filename of the output ORC bundle. Defaults to the package name inside `Cargo.toml` and the `.orc` extension. From 3cf278cb742116b5285c0f13ece84c68dbdc0f4e Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Tue, 21 Jan 2025 13:46:01 +0100 Subject: [PATCH 18/20] feat(cmd/rofl): Fix init to create app directory if needed --- cmd/rofl/mgmt.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 2d3e929d..524bd80d 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -60,16 +60,20 @@ var ( cobra.CheckErr("offline mode currently not supported") } - // TODO: Support an interactive mode. - var appName string + // Determine the application directory. + appPath := "." if len(args) > 0 { - appName = args[0] - } else { - // Infer from current directory. - wd, err := os.Getwd() + appPath = args[0] + } + appPath, err := filepath.Abs(appPath) + cobra.CheckErr(err) + appName := filepath.Base(appPath) + if err = os.MkdirAll(appPath, 0o755); err != nil { cobra.CheckErr(err) - appName = filepath.Base(wd) } + err = os.Chdir(appPath) + cobra.CheckErr(err) + // Fail in case there is an existing manifest. if buildRofl.ManifestExists() { cobra.CheckErr("refusing to overwrite existing manifest") From 753c930654834be484479706330c23c0def8684d Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Wed, 22 Jan 2025 10:25:37 +0100 Subject: [PATCH 19/20] feat(cmd/rofl): Reduce default manifest indentation --- build/rofl/manifest.go | 16 +++++++++++++--- cmd/rofl/mgmt.go | 7 +++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index a782c8ad..19492231 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -16,8 +16,8 @@ import ( // ManifestFileNames are the manifest file names that are tried when loading the manifest. var ManifestFileNames = []string{ - "rofl.yml", "rofl.yaml", + "rofl.yml", } // Supported ROFL app kinds. @@ -165,12 +165,22 @@ func (m *Manifest) SourceFileName() string { // Save serializes the manifest and writes it to the file returned by `SourceFileName`, overwriting // any previous manifest. +// +// If no previous source filename is available, a default one is set. func (m *Manifest) Save() error { - data, err := yaml.Marshal(m) + if m.sourceFn == "" { + m.sourceFn = ManifestFileNames[0] + } + + f, err := os.Create(m.sourceFn) if err != nil { return err } - return os.WriteFile(m.SourceFileName(), data, 0o644) //nolint: gosec + defer f.Close() + + enc := yaml.NewEncoder(f) + enc.SetIndent(2) + return enc.Encode(m) } // DefaultDeploymentName is the name of the default deployment that must always be defined and is diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 524bd80d..5c82be40 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -150,13 +150,12 @@ var ( fmt.Printf(" Admin: %s\n", deployment.Admin) // Serialize manifest and write it to file. - const manifestFn = "rofl.yaml" - data, _ := yaml.Marshal(manifest) - if err = os.WriteFile(manifestFn, data, 0o644); err != nil { //nolint: gosec + err = manifest.Save() + if err != nil { cobra.CheckErr(fmt.Errorf("failed to write manifest: %w", err)) } - fmt.Printf("Created manifest in '%s'.\n", manifestFn) + fmt.Printf("Created manifest in '%s'.\n", manifest.SourceFileName()) fmt.Printf("Run `oasis rofl create --update-manifest` to register your ROFL app and configure an app ID.\n") }, } From 64231c6fcceb65e3cc50ea032e0892299a991c0c Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Wed, 22 Jan 2025 10:45:28 +0100 Subject: [PATCH 20/20] feat(cmd/rofl): Create an empty compose.yaml on init --- cmd/rofl/mgmt.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 5c82be40..66dbf196 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -149,6 +149,15 @@ var ( fmt.Printf(" Debug: %v\n", deployment.Debug) fmt.Printf(" Admin: %s\n", deployment.Admin) + // For container app kind also create an en empty compose.yaml file if it doesn't exist. + if appKind == buildRofl.AppKindContainer { + var f *os.File + f, err = os.OpenFile("compose.yaml", os.O_RDONLY|os.O_CREATE, 0o644) + if err == nil { + f.Close() + } + } + // Serialize manifest and write it to file. err = manifest.Save() if err != nil {