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..7e44418c 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)) @@ -50,33 +58,48 @@ func maybeDownloadArtifact(kind, uri, knownHash 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 @@ -192,6 +215,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 +232,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 +253,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, + // Execute mksquashfs. + cmd := exec.Command( + 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 +299,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 +326,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 +354,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..340666e3 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -2,15 +2,24 @@ package build import ( "context" + "encoding/base64" "fmt" + "os" "github.com/spf13/cobra" flag "github.com/spf13/pflag" + coreCommon "github.com/oasisprotocol/oasis-core/go/common" + "github.com/oasisprotocol/oasis-core/go/common/cbor" + "github.com/oasisprotocol/oasis-core/go/common/version" 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. @@ -28,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) + }, } ) @@ -54,6 +149,77 @@ func detectBuildMode(npa *common.NPASelection) { } } +func setupBuildEnv(manifest *buildRofl.Manifest, npa *common.NPASelection) { + // 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]") @@ -61,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 new file mode 100644 index 00000000..b06f2b73 --- /dev/null +++ b/cmd/rofl/build/container.go @@ -0,0 +1,72 @@ +package build + +import ( + "fmt" + + "github.com/oasisprotocol/oasis-core/go/runtime/bundle" + + buildRofl "github.com/oasisprotocol/cli/build/rofl" + "github.com/oasisprotocol/cli/cmd/common" +) + +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" + + defaultContainerRuntimeURI = "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers/v0.1.0/runtime" + + defaultContainerComposeURI = "compose.yaml" +) + +// 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 + } + + // 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...") + + return tdxBundleComponent(manifest, artifacts, bnd, stage2, extraKernelOpts) +} diff --git a/cmd/rofl/build/sgx.go b/cmd/rofl/build/sgx.go index 21251ab2..f4ab719a 100644 --- a/cmd/rofl/build/sgx.go +++ b/cmd/rofl/build/sgx.go @@ -11,174 +11,124 @@ 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" "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" - 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) - - 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() - - // 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{ - Name: pkgMeta.Name, - ID: npa.ParaTime.Namespace(), - }, - } - bnd.Manifest.Version, err = version.FromString(pkgMeta.Version) - if err != nil { - cobra.CheckErr(fmt.Errorf("unsupported package version format: %w", err)) - } - - fmt.Printf("Name: %s\n", bnd.Manifest.Name) - fmt.Printf("Version: %s\n", bnd.Manifest.Version) - - // 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. // @@ -259,7 +209,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. @@ -281,6 +233,7 @@ func sgxSetupBuildEnv() []string { 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"} @@ -289,13 +242,3 @@ func sgxSetupBuildEnv() []string { 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 1c2aaf43..44dabd93 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -7,15 +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" - cliConfig "github.com/oasisprotocol/cli/config" ) // TODO: Replace these URIs with a better mechanism for managing releases. @@ -24,204 +22,267 @@ 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.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 knownHashes = map[string]string{ - defaultFirmwareURI: "db47100a7d6a0c1f6983be224137c3f8d7cb09b63bb1c7a5ee7829d8e994a42f", - defaultKernelURI: "0c4a74af5e3860e1b9c79b38aff9de8c59aa92f14da715fbfd04a9362ee4cd59", - defaultStage2TemplateURI: "8cbc67e4a05b01e6fc257a3ef378db50ec230bc4c7aacbfb9abf0f5b17dcb8fd", -} - var ( - tdxFirmwareURI string - tdxFirmwareHash string - tdxKernelURI string - tdxKernelHash string - tdxStage2TemplateURI string - tdxStage2TemplateHash string - - tdxResourcesMemory uint64 - tdxResourcesCPUCount uint8 - - 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) - - if npa.ParaTime == nil { - cobra.CheckErr("no ParaTime selected") - } + tdxFirmwareURI = defaultFirmwareURI + tdxKernelURI = defaultKernelURI + tdxStage2TemplateURI = defaultStage2TemplateURI +) - // 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) - } +// 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() + 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")) - } - } + // 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") + } + } - // 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(), - }, - } - bnd.Manifest.Version, err = version.FromString(pkgMeta.Version) - if err != nil { - cobra.CheckErr(fmt.Errorf("unsupported package version format: %w", err)) - } + 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.Printf("Name: %s\n", bnd.Manifest.Name) - fmt.Printf("Version: %s\n", bnd.Manifest.Version) + stage2, err := tdxPrepareStage2(tmpDir, artifacts, initPath, nil) + if err != nil { + return err + } - 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)) - } + fmt.Println("Creating ORC bundle...") - // 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 { - cobra.CheckErr(fmt.Errorf("failed to create temporary stage 2 build directory: %w", err)) - } - defer os.RemoveAll(tmpDir) // TODO: This doesn't work because of cobra.CheckErr + return tdxBundleComponent(manifest, artifacts, bnd, stage2, nil) +} - 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)) - } +type artifact struct { + kind string + uri string +} - // 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)) - } +// tdxGetDefaultArtifacts returns the list of default TDX artifacts. +func tdxGetDefaultArtifacts() []*artifact { + return []*artifact{ + {artifactFirmware, tdxFirmwareURI}, + {artifactKernel, tdxKernelURI}, + {artifactStage2, tdxStage2TemplateURI}, + } +} - // Add runtime as init. - fmt.Println("Adding runtime as init...") - err = copyFile(initPath, filepath.Join(rootfsDir, "init"), 0o755) - cobra.CheckErr(err) - - // 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)) - } +// 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 +} - // 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)) - } +// 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: + } - // 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)) - } + if overrideURI == "" { + continue + } + artifact.uri = overrideURI + } +} - fmt.Println("Creating ORC bundle...") - - // Add the ROFL component. - firmwareName := "firmware.fd" - kernelName := "kernel.bin" - stage2Name := "stage2.img" - - comp := bundle.Component{ - Kind: component.ROFL, - Name: pkgMeta.Name, - TDX: &bundle.TDXMetadata{ - Firmware: firmwareName, - Kernel: kernelName, - Stage2Image: stage2Name, - ExtraKernelOptions: []string{ - "console=ttyS0", - fmt.Sprintf("oasis.stage2.roothash=%s", rootHash), - fmt.Sprintf("oasis.stage2.hash_offset=%d", rootfsSize), - }, - Resources: bundle.TDXResources{ - Memory: tdxResourcesMemory, - CPUCount: tdxResourcesCPUCount, - }, - }, - } - bnd.Manifest.Components = append(bnd.Manifest.Components, &comp) +type tdxStage2 struct { + fn string + rootHash string + fsSize int64 +} - if err = bnd.Manifest.Validate(); err != nil { - cobra.CheckErr(fmt.Errorf("failed to validate manifest: %w", err)) - } +// tdxPrepareStage2 prepares the stage 2 rootfs. +func tdxPrepareStage2(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...") + 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) + } - // 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)) - } + // 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) + } - // 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)) - } + // 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 + } + } - fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + // 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) + } + + return &tdxStage2{ + 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, +) error { + // Add the ROFL component. + firmwareName := "firmware.fd" + kernelName := "kernel.bin" + stage2Name := "stage2.img" + + comp := bundle.Component{ + Kind: component.ROFL, + Name: bnd.Manifest.Name, + TDX: &bundle.TDXMetadata{ + Firmware: firmwareName, + Kernel: kernelName, + Stage2Image: stage2Name, + ExtraKernelOptions: []string{ + "console=ttyS0", + fmt.Sprintf("oasis.stage2.roothash=%s", stage2.rootHash), + fmt.Sprintf("oasis.stage2.hash_offset=%d", stage2.fsSize), + }, + Resources: bundle.TDXResources{ + Memory: manifest.Resources.Memory, + CPUCount: manifest.Resources.CPUCount, + }, }, } -) + + 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", manifest.Resources.EphemeralStorage.Size*1024*1024), + ) + case buildRofl.EphemeralStorageKindDisk: + // Allocate some space after regular stage2. + const sectorSize = 512 + storageSize := manifest.Resources.EphemeralStorage.Size * 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", tmpStorageKind) + } + + // 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)) + } + + return 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. @@ -235,30 +296,15 @@ func tdxSetupBuildEnv() { } 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(&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") - - tdxCmd.Flags().AddFlagSet(common.SelectorNPFlags) - tdxCmd.Flags().AddFlagSet(tdxFlags) -} 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) diff --git a/docs/rofl.md b/docs/rofl.md index b90bc2e5..1b75ae1a 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/dapp/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