-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cmd/rofl): Add support for building TDX ROFL apps
- Loading branch information
Showing
7 changed files
with
594 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
package build | ||
|
||
import ( | ||
"archive/tar" | ||
"compress/bzip2" | ||
"crypto/sha256" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/adrg/xdg" | ||
"github.com/spf13/cobra" | ||
|
||
"github.com/oasisprotocol/oasis-core/go/common/crypto/hash" | ||
) | ||
|
||
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 { | ||
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. | ||
if url.Host == "" { | ||
return url.Path | ||
} | ||
|
||
// TODO: Prune cache. | ||
cacheHash := hash.NewFromBytes([]byte(uri)).Hex() | ||
cacheFn, err := xdg.CacheFile(filepath.Join("oasis", artifactCacheDir, cacheHash)) | ||
if err != nil { | ||
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() | ||
|
||
// Download the remote artifact. | ||
res, err := http.Get(uri) // #nosec G107 -- URL being user-controlled is the point. | ||
if err != nil { | ||
cobra.CheckErr(fmt.Errorf("failed to download %s artifact: %w", kind, err)) | ||
} | ||
|
||
// 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)) | ||
} | ||
|
||
// 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)) | ||
} | ||
} | ||
|
||
return cacheFn | ||
} | ||
|
||
// extractArchive extracts the given tar.bz2 archive into the target output directory. | ||
func extractArchive(fn, outputDir string) error { | ||
f, err := os.Open(fn) | ||
if err != nil { | ||
return fmt.Errorf("failed to open archive: %w", err) | ||
} | ||
defer f.Close() | ||
|
||
rd := tar.NewReader(bzip2.NewReader(f)) | ||
|
||
existingPaths := make(map[string]struct{}) | ||
cleanupPath := func(path string) (string, error) { | ||
// Sanitize path to ensure it doesn't escape to any parent directories. | ||
path = filepath.Clean(filepath.Join(outputDir, path)) | ||
if !strings.HasPrefix(path, outputDir) { | ||
return "", fmt.Errorf("malformed path in archive") | ||
} | ||
return path, nil | ||
} | ||
|
||
modTimes := make(map[string]time.Time) | ||
|
||
FILES: | ||
for { | ||
var header *tar.Header | ||
header, err = rd.Next() | ||
switch { | ||
case errors.Is(err, io.EOF): | ||
// We are done. | ||
break FILES | ||
case err != nil: | ||
// Failed to read archive. | ||
return fmt.Errorf("error reading archive: %w", err) | ||
case header == nil: | ||
// Bad archive. | ||
return fmt.Errorf("malformed archive") | ||
} | ||
|
||
path, err := cleanupPath(header.Name) | ||
if err != nil { | ||
return err | ||
} | ||
if _, ok := existingPaths[path]; ok { | ||
continue // Make sure we never handle a path twice. | ||
} | ||
existingPaths[path] = struct{}{} | ||
modTimes[path] = header.ModTime | ||
|
||
switch header.Typeflag { | ||
case tar.TypeDir: | ||
// Directory. | ||
if err = os.MkdirAll(path, header.FileInfo().Mode()); err != nil { | ||
return fmt.Errorf("failed to create directory: %w", err) | ||
} | ||
case tar.TypeLink: | ||
// Hard link. | ||
var linkPath string | ||
linkPath, err = cleanupPath(header.Linkname) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err = os.Link(linkPath, path); err != nil { | ||
return fmt.Errorf("failed to create hard link: %w", err) | ||
} | ||
case tar.TypeSymlink: | ||
// Symbolic link. | ||
if err = os.Symlink(header.Linkname, path); err != nil { | ||
return fmt.Errorf("failed to create soft link: %w", err) | ||
} | ||
case tar.TypeChar, tar.TypeBlock, tar.TypeFifo: | ||
// Device or FIFO node. | ||
if err = extractHandleSpecialNode(path, header); err != nil { | ||
return err | ||
} | ||
case tar.TypeReg: | ||
// Regular file. | ||
if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil { | ||
return fmt.Errorf("failed to create parent directory: %w", err) | ||
} | ||
|
||
var fh *os.File | ||
fh, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode()) | ||
if err != nil { | ||
return fmt.Errorf("failed to create file: %w", err) | ||
} | ||
if _, err = io.Copy(fh, rd); err != nil { // #nosec G110 | ||
fh.Close() | ||
return fmt.Errorf("failed to copy data: %w", err) | ||
} | ||
fh.Close() | ||
default: | ||
// Skip unsupported types. | ||
continue | ||
} | ||
} | ||
|
||
// Update all modification times at the end to ensure they are correct. | ||
for path, mtime := range modTimes { | ||
if err = extractChtimes(path, mtime, mtime); err != nil { | ||
return fmt.Errorf("failed to change file '%s' timestamps: %w", path, err) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// 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 { | ||
sf, err := os.Open(src) | ||
if err != nil { | ||
return fmt.Errorf("failed to open '%s': %w", src, err) | ||
} | ||
defer sf.Close() | ||
|
||
df, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) | ||
if err != nil { | ||
return fmt.Errorf("failed to create '%s': %w", dst, err) | ||
} | ||
defer df.Close() | ||
|
||
_, err = io.Copy(df, sf) | ||
return err | ||
} | ||
|
||
// computeDirSize computes the size of the given directory. | ||
func computeDirSize(path string) (int64, error) { | ||
var size int64 | ||
err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { | ||
fi, err := d.Info() | ||
if err != nil { | ||
return err | ||
} | ||
size += fi.Size() | ||
return nil | ||
}) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return size, nil | ||
} | ||
|
||
// createExt4Fs creates an ext4 filesystem in the given file using directory dir to populate it. | ||
func createExt4Fs(fn, dir string) (int64, error) { | ||
// 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( // #nosec G204 | ||
"mkfs.ext4", | ||
"-E", "root_owner=0:0", | ||
"-d", dir, | ||
fn, | ||
fmt.Sprintf("%dK", fsSize), | ||
) | ||
var out strings.Builder | ||
cmd.Stderr = &out | ||
if err = cmd.Run(); err != nil { | ||
return 0, fmt.Errorf("%w\n%s", err, out.String()) | ||
} | ||
return fsSize, nil | ||
} | ||
|
||
// createVerityHashTree creates the verity Merkle hash tree and returns the root hash. | ||
func createVerityHashTree(fsFn, hashFn string) (string, error) { | ||
rootHashFn := hashFn + ".roothash" | ||
|
||
cmd := exec.Command( // #nosec G204 | ||
"veritysetup", "format", | ||
"--data-block-size=4096", | ||
"--hash-block-size=4096", | ||
"--root-hash-file="+rootHashFn, | ||
fsFn, | ||
hashFn, | ||
) | ||
if err := cmd.Run(); err != nil { | ||
return "", err | ||
} | ||
|
||
data, err := os.ReadFile(rootHashFn) | ||
if err != nil { | ||
return "", fmt.Errorf("") | ||
} | ||
return string(data), nil | ||
} | ||
|
||
// concatFiles appends the contents of file b to a. | ||
func concatFiles(a, b string) error { | ||
df, err := os.OpenFile(a, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) | ||
if err != nil { | ||
return err | ||
} | ||
defer df.Close() | ||
|
||
sf, err := os.Open(b) | ||
if err != nil { | ||
return err | ||
} | ||
defer sf.Close() | ||
|
||
_, err = io.Copy(df, sf) | ||
return err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
//go:build !unix | ||
|
||
package build | ||
|
||
import ( | ||
"archive/tar" | ||
"os" | ||
"time" | ||
) | ||
|
||
func extractHandleSpecialNode(path string, header *tar.Header) error { | ||
return nil | ||
} | ||
|
||
func extractChtimes(path string, atime, mtime time.Time) error { | ||
return os.Chtimes(path, atime, mtime) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
//go:build unix | ||
|
||
package build | ||
|
||
import ( | ||
"archive/tar" | ||
"time" | ||
|
||
"golang.org/x/sys/unix" | ||
) | ||
|
||
func extractHandleSpecialNode(path string, header *tar.Header) error { | ||
mode := uint32(header.Mode & 0o7777) | ||
switch header.Typeflag { | ||
case tar.TypeBlock: | ||
mode |= unix.S_IFBLK | ||
case tar.TypeChar: | ||
mode |= unix.S_IFCHR | ||
case tar.TypeFifo: | ||
mode |= unix.S_IFIFO | ||
} | ||
|
||
return unix.Mknod(path, mode, int(unix.Mkdev(uint32(header.Devmajor), uint32(header.Devminor)))) | ||
} | ||
|
||
func extractChtimes(path string, atime, mtime time.Time) error { | ||
atv := unix.NsecToTimeval(atime.UnixNano()) | ||
mtv := unix.NsecToTimeval(mtime.UnixNano()) | ||
return unix.Lutimes(path, []unix.Timeval{atv, mtv}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,4 +62,5 @@ func init() { | |
|
||
Cmd.PersistentFlags().AddFlagSet(globalFlags) | ||
Cmd.AddCommand(sgxCmd) | ||
Cmd.AddCommand(tdxCmd) | ||
} |
Oops, something went wrong.