Skip to content

Commit 8bbcd58

Browse files
committed
feat(cmd/rofl): Add support for building TDX ROFL apps
1 parent effdba1 commit 8bbcd58

File tree

7 files changed

+614
-4
lines changed

7 files changed

+614
-4
lines changed

cmd/rofl/build/artifacts.go

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
package build
2+
3+
import (
4+
"archive/tar"
5+
"compress/bzip2"
6+
"crypto/sha256"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"io/fs"
11+
"net/http"
12+
"net/url"
13+
"os"
14+
"os/exec"
15+
"path/filepath"
16+
"strings"
17+
"time"
18+
19+
"github.com/adrg/xdg"
20+
"github.com/spf13/cobra"
21+
22+
"github.com/oasisprotocol/oasis-core/go/common/crypto/hash"
23+
)
24+
25+
const artifactCacheDir = "build_cache"
26+
27+
// maybeDownloadArtifact downloads the given artifact and optionally verifies its integrity against
28+
// the provided hash.
29+
func maybeDownloadArtifact(kind, uri, knownHash string) string {
30+
fmt.Printf("Downloading %s artifact...\n", kind)
31+
fmt.Printf(" URI: %s\n", uri)
32+
if knownHash != "" {
33+
fmt.Printf(" Hash: %s\n", knownHash)
34+
}
35+
36+
url, err := url.Parse(uri)
37+
if err != nil {
38+
cobra.CheckErr(fmt.Errorf("failed to parse %s artifact URL: %w", kind, err))
39+
}
40+
41+
// In case the URI represents a local file, just return it.
42+
if url.Host == "" {
43+
return url.Path
44+
}
45+
46+
// TODO: Prune cache.
47+
cacheHash := hash.NewFromBytes([]byte(uri)).Hex()
48+
cacheFn, err := xdg.CacheFile(filepath.Join("oasis", artifactCacheDir, cacheHash))
49+
if err != nil {
50+
cobra.CheckErr(fmt.Errorf("failed to create cache directory for %s artifact: %w", kind, err))
51+
}
52+
53+
f, err := os.Create(cacheFn)
54+
if err != nil {
55+
cobra.CheckErr(fmt.Errorf("failed to create file for %s artifact: %w", kind, err))
56+
}
57+
defer f.Close()
58+
59+
// Download the remote artifact.
60+
res, err := http.Get(uri) //nolint:gosec,noctx
61+
if err != nil {
62+
cobra.CheckErr(fmt.Errorf("failed to download %s artifact: %w", kind, err))
63+
}
64+
defer res.Body.Close()
65+
66+
// Compute the SHA256 hash while downloading the artifact.
67+
h := sha256.New()
68+
rd := io.TeeReader(res.Body, h)
69+
70+
if _, err = io.Copy(f, rd); err != nil {
71+
cobra.CheckErr(fmt.Errorf("failed to download %s artifact: %w", kind, err))
72+
}
73+
74+
// Verify integrity if available.
75+
if knownHash != "" {
76+
artifactHash := fmt.Sprintf("%x", h.Sum(nil))
77+
if artifactHash != knownHash {
78+
cobra.CheckErr(fmt.Errorf("hash mismatch for %s artifact (expected: %s got: %s)", kind, knownHash, artifactHash))
79+
}
80+
}
81+
82+
return cacheFn
83+
}
84+
85+
// extractArchive extracts the given tar.bz2 archive into the target output directory.
86+
func extractArchive(fn, outputDir string) error {
87+
f, err := os.Open(fn)
88+
if err != nil {
89+
return fmt.Errorf("failed to open archive: %w", err)
90+
}
91+
defer f.Close()
92+
93+
rd := tar.NewReader(bzip2.NewReader(f))
94+
95+
existingPaths := make(map[string]struct{})
96+
cleanupPath := func(path string) (string, error) {
97+
// Sanitize path to ensure it doesn't escape to any parent directories.
98+
path = filepath.Clean(filepath.Join(outputDir, path))
99+
if !strings.HasPrefix(path, outputDir) {
100+
return "", fmt.Errorf("malformed path in archive")
101+
}
102+
return path, nil
103+
}
104+
105+
modTimes := make(map[string]time.Time)
106+
107+
FILES:
108+
for {
109+
var header *tar.Header
110+
header, err = rd.Next()
111+
switch {
112+
case errors.Is(err, io.EOF):
113+
// We are done.
114+
break FILES
115+
case err != nil:
116+
// Failed to read archive.
117+
return fmt.Errorf("error reading archive: %w", err)
118+
case header == nil:
119+
// Bad archive.
120+
return fmt.Errorf("malformed archive")
121+
}
122+
123+
var path string
124+
path, err = cleanupPath(header.Name)
125+
if err != nil {
126+
return err
127+
}
128+
if _, ok := existingPaths[path]; ok {
129+
continue // Make sure we never handle a path twice.
130+
}
131+
existingPaths[path] = struct{}{}
132+
modTimes[path] = header.ModTime
133+
134+
switch header.Typeflag {
135+
case tar.TypeDir:
136+
// Directory.
137+
if err = os.MkdirAll(path, header.FileInfo().Mode()); err != nil {
138+
return fmt.Errorf("failed to create directory: %w", err)
139+
}
140+
case tar.TypeLink:
141+
// Hard link.
142+
var linkPath string
143+
linkPath, err = cleanupPath(header.Linkname)
144+
if err != nil {
145+
return err
146+
}
147+
148+
if err = os.Link(linkPath, path); err != nil {
149+
return fmt.Errorf("failed to create hard link: %w", err)
150+
}
151+
case tar.TypeSymlink:
152+
// Symbolic link.
153+
if err = os.Symlink(header.Linkname, path); err != nil {
154+
return fmt.Errorf("failed to create soft link: %w", err)
155+
}
156+
case tar.TypeChar, tar.TypeBlock, tar.TypeFifo:
157+
// Device or FIFO node.
158+
if err = extractHandleSpecialNode(path, header); err != nil {
159+
return err
160+
}
161+
case tar.TypeReg:
162+
// Regular file.
163+
if err = os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
164+
return fmt.Errorf("failed to create parent directory: %w", err)
165+
}
166+
167+
var fh *os.File
168+
fh, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode())
169+
if err != nil {
170+
return fmt.Errorf("failed to create file: %w", err)
171+
}
172+
if _, err = io.Copy(fh, rd); err != nil { //nolint:gosec
173+
fh.Close()
174+
return fmt.Errorf("failed to copy data: %w", err)
175+
}
176+
fh.Close()
177+
default:
178+
// Skip unsupported types.
179+
continue
180+
}
181+
}
182+
183+
// Update all modification times at the end to ensure they are correct.
184+
for path, mtime := range modTimes {
185+
if err = extractChtimes(path, mtime, mtime); err != nil {
186+
return fmt.Errorf("failed to change file '%s' timestamps: %w", path, err)
187+
}
188+
}
189+
190+
return nil
191+
}
192+
193+
// copyFile copies the file at path src to a file at path dst using the given mode.
194+
func copyFile(src, dst string, mode os.FileMode) error {
195+
sf, err := os.Open(src)
196+
if err != nil {
197+
return fmt.Errorf("failed to open '%s': %w", src, err)
198+
}
199+
defer sf.Close()
200+
201+
df, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
202+
if err != nil {
203+
return fmt.Errorf("failed to create '%s': %w", dst, err)
204+
}
205+
defer df.Close()
206+
207+
_, err = io.Copy(df, sf)
208+
return err
209+
}
210+
211+
// computeDirSize computes the size of the given directory.
212+
func computeDirSize(path string) (int64, error) {
213+
var size int64
214+
err := filepath.WalkDir(path, func(path string, d fs.DirEntry, derr error) error {
215+
if derr != nil {
216+
return derr
217+
}
218+
fi, err := d.Info()
219+
if err != nil {
220+
return err
221+
}
222+
size += fi.Size()
223+
return nil
224+
})
225+
if err != nil {
226+
return 0, err
227+
}
228+
return size, nil
229+
}
230+
231+
// createExt4Fs creates an ext4 filesystem in the given file using directory dir to populate it.
232+
//
233+
// Returns the size of the created filesystem image in bytes.
234+
func createExt4Fs(fn, dir string) (int64, error) {
235+
// Compute filesystem size in bytes.
236+
fsSize, err := computeDirSize(dir)
237+
if err != nil {
238+
return 0, err
239+
}
240+
fsSize /= 1024 // Convert to kilobytes.
241+
fsSize = (fsSize * 150) / 100 // Scale by overhead factor of 1.5.
242+
243+
// Execute mkfs.ext4.
244+
cmd := exec.Command( //nolint:gosec
245+
"mkfs.ext4",
246+
"-E", "root_owner=0:0",
247+
"-d", dir,
248+
fn,
249+
fmt.Sprintf("%dK", fsSize),
250+
)
251+
var out strings.Builder
252+
cmd.Stderr = &out
253+
if err = cmd.Run(); err != nil {
254+
return 0, fmt.Errorf("%w\n%s", err, out.String())
255+
}
256+
257+
// Measure the size of the resulting image.
258+
fi, err := os.Stat(fn)
259+
if err != nil {
260+
return 0, err
261+
}
262+
return fi.Size(), nil
263+
}
264+
265+
// createVerityHashTree creates the verity Merkle hash tree and returns the root hash.
266+
func createVerityHashTree(fsFn, hashFn string) (string, error) {
267+
rootHashFn := hashFn + ".roothash"
268+
269+
cmd := exec.Command( //nolint:gosec
270+
"veritysetup", "format",
271+
"--data-block-size=4096",
272+
"--hash-block-size=4096",
273+
"--root-hash-file="+rootHashFn,
274+
fsFn,
275+
hashFn,
276+
)
277+
if err := cmd.Run(); err != nil {
278+
return "", err
279+
}
280+
281+
data, err := os.ReadFile(rootHashFn)
282+
if err != nil {
283+
return "", fmt.Errorf("")
284+
}
285+
return string(data), nil
286+
}
287+
288+
// concatFiles appends the contents of file b to a.
289+
func concatFiles(a, b string) error {
290+
df, err := os.OpenFile(a, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
291+
if err != nil {
292+
return err
293+
}
294+
defer df.Close()
295+
296+
sf, err := os.Open(b)
297+
if err != nil {
298+
return err
299+
}
300+
defer sf.Close()
301+
302+
_, err = io.Copy(df, sf)
303+
return err
304+
}

cmd/rofl/build/artifacts_other.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build !unix
2+
3+
package build
4+
5+
import (
6+
"archive/tar"
7+
"os"
8+
"time"
9+
)
10+
11+
func extractHandleSpecialNode(path string, header *tar.Header) error {
12+
return nil
13+
}
14+
15+
func extractChtimes(path string, atime, mtime time.Time) error {
16+
return os.Chtimes(path, atime, mtime)
17+
}

cmd/rofl/build/artifacts_unix.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//go:build unix
2+
3+
package build
4+
5+
import (
6+
"archive/tar"
7+
"time"
8+
9+
"golang.org/x/sys/unix"
10+
)
11+
12+
func extractHandleSpecialNode(path string, header *tar.Header) error {
13+
mode := uint32(header.Mode & 0o7777)
14+
switch header.Typeflag {
15+
case tar.TypeBlock:
16+
mode |= unix.S_IFBLK
17+
case tar.TypeChar:
18+
mode |= unix.S_IFCHR
19+
case tar.TypeFifo:
20+
mode |= unix.S_IFIFO
21+
}
22+
23+
return unix.Mknod(path, mode, int(unix.Mkdev(uint32(header.Devmajor), uint32(header.Devminor))))
24+
}
25+
26+
func extractChtimes(path string, atime, mtime time.Time) error {
27+
atv := unix.NsecToTimeval(atime.UnixNano())
28+
mtv := unix.NsecToTimeval(mtime.UnixNano())
29+
return unix.Lutimes(path, []unix.Timeval{atv, mtv})
30+
}

cmd/rofl/build/build.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,5 @@ func init() {
6262

6363
Cmd.PersistentFlags().AddFlagSet(globalFlags)
6464
Cmd.AddCommand(sgxCmd)
65+
Cmd.AddCommand(tdxCmd)
6566
}

0 commit comments

Comments
 (0)