podman
570 строк · 19.1 Кб
1package mkcw
2
3import (
4"archive/tar"
5"bytes"
6"compress/gzip"
7"encoding/binary"
8"encoding/json"
9"errors"
10"fmt"
11"io"
12"io/fs"
13"os"
14"os/exec"
15"path/filepath"
16"strconv"
17"strings"
18"time"
19
20"github.com/containers/buildah/internal/tmpdir"
21"github.com/containers/buildah/pkg/overlay"
22"github.com/containers/luksy"
23"github.com/containers/storage/pkg/idtools"
24"github.com/containers/storage/pkg/mount"
25"github.com/containers/storage/pkg/system"
26"github.com/docker/docker/pkg/ioutils"
27"github.com/docker/go-units"
28digest "github.com/opencontainers/go-digest"
29v1 "github.com/opencontainers/image-spec/specs-go/v1"
30"github.com/sirupsen/logrus"
31)
32
33const minimumImageSize = 10 * 1024 * 1024
34
35// ArchiveOptions includes optional settings for generating an archive.
36type ArchiveOptions struct {
37// If supplied, we'll register the workload with this server.
38// Practically necessary if DiskEncryptionPassphrase is not set, in
39// which case we'll generate one and throw it away after.
40AttestationURL string
41
42// Used to measure the environment. If left unset (0, ""), defaults will be applied.
43CPUs int
44Memory int
45
46// Can be manually set. If left unset ("", false, nil), reasonable values will be used.
47TempDir string
48TeeType TeeType
49IgnoreAttestationErrors bool
50ImageSize int64
51WorkloadID string
52Slop string
53DiskEncryptionPassphrase string
54FirmwareLibrary string
55Logger *logrus.Logger
56GraphOptions []string // passed in from a storage Store, probably
57ExtraImageContent map[string]string
58}
59
60type chainRetrievalError struct {
61stderr string
62err error
63}
64
65func (c chainRetrievalError) Error() string {
66if trimmed := strings.TrimSpace(c.stderr); trimmed != "" {
67return fmt.Sprintf("retrieving SEV certificate chain: sevctl: %v: %v", strings.TrimSpace(c.stderr), c.err)
68}
69return fmt.Sprintf("retrieving SEV certificate chain: sevctl: %v", c.err)
70}
71
72// Archive generates a WorkloadConfig for a specified directory and produces a
73// tar archive of a container image's rootfs with the expected contents.
74func Archive(rootfsPath string, ociConfig *v1.Image, options ArchiveOptions) (io.ReadCloser, WorkloadConfig, error) {
75const (
76teeDefaultCPUs = 2
77teeDefaultMemory = 512
78teeDefaultFilesystem = "ext4"
79teeDefaultTeeType = SNP
80)
81
82if rootfsPath == "" {
83return nil, WorkloadConfig{}, fmt.Errorf("required path not specified")
84}
85logger := options.Logger
86if logger == nil {
87logger = logrus.StandardLogger()
88}
89
90teeType := options.TeeType
91if teeType == "" {
92teeType = teeDefaultTeeType
93}
94cpus := options.CPUs
95if cpus == 0 {
96cpus = teeDefaultCPUs
97}
98memory := options.Memory
99if memory == 0 {
100memory = teeDefaultMemory
101}
102filesystem := teeDefaultFilesystem
103workloadID := options.WorkloadID
104if workloadID == "" {
105digestInput := rootfsPath + filesystem + time.Now().String()
106workloadID = digest.Canonical.FromString(digestInput).Encoded()
107}
108workloadConfig := WorkloadConfig{
109Type: teeType,
110WorkloadID: workloadID,
111CPUs: cpus,
112Memory: memory,
113AttestationURL: options.AttestationURL,
114}
115if options.TempDir == "" {
116options.TempDir = tmpdir.GetTempDir()
117}
118
119// Do things which are specific to the type of TEE we're building for.
120var chainBytes []byte
121var chainBytesFile string
122var chainInfo fs.FileInfo
123switch teeType {
124default:
125return nil, WorkloadConfig{}, fmt.Errorf("don't know how to generate TeeData for TEE type %q", teeType)
126case SEV, SEV_NO_ES:
127// If we need a certificate chain, get it.
128chain, err := os.CreateTemp(options.TempDir, "chain")
129if err != nil {
130return nil, WorkloadConfig{}, err
131}
132chain.Close()
133defer func() {
134if err := os.Remove(chain.Name()); err != nil {
135logger.Warnf("error removing temporary file %q: %v", chain.Name(), err)
136}
137}()
138logrus.Debugf("sevctl export -f %s", chain.Name())
139cmd := exec.Command("sevctl", "export", "-f", chain.Name())
140var stdout, stderr bytes.Buffer
141cmd.Stdout, cmd.Stderr = &stdout, &stderr
142if err := cmd.Run(); err != nil {
143if !options.IgnoreAttestationErrors {
144return nil, WorkloadConfig{}, chainRetrievalError{stderr.String(), err}
145}
146logger.Warn(chainRetrievalError{stderr.String(), err}.Error())
147}
148if chainBytes, err = os.ReadFile(chain.Name()); err != nil {
149chainBytes = []byte{}
150}
151var teeData SevWorkloadData
152if len(chainBytes) > 0 {
153chainBytesFile = "sev.chain"
154chainInfo, err = os.Stat(chain.Name())
155if err != nil {
156return nil, WorkloadConfig{}, err
157}
158teeData.VendorChain = "/" + chainBytesFile
159}
160encodedTeeData, err := json.Marshal(teeData)
161if err != nil {
162return nil, WorkloadConfig{}, fmt.Errorf("encoding tee data: %w", err)
163}
164workloadConfig.TeeData = string(encodedTeeData)
165case SNP:
166teeData := SnpWorkloadData{
167Generation: "milan",
168}
169encodedTeeData, err := json.Marshal(teeData)
170if err != nil {
171return nil, WorkloadConfig{}, fmt.Errorf("encoding tee data: %w", err)
172}
173workloadConfig.TeeData = string(encodedTeeData)
174}
175
176// We're going to want to add some content to the rootfs, so set up an
177// overlay that uses it as a lower layer so that we can write to it.
178st, err := system.Stat(rootfsPath)
179if err != nil {
180return nil, WorkloadConfig{}, fmt.Errorf("reading information about the container root filesystem: %w", err)
181}
182// Create a temporary directory to hold all of this. Use tmpdir.GetTempDir()
183// instead of the passed-in location, which a crafty caller might have put in an
184// overlay filesystem in storage because there tends to be more room there than
185// in, say, /var/tmp, and the plaintext disk image, which we put in the passed-in
186// location, can get quite large.
187rootfsParentDir, err := os.MkdirTemp(tmpdir.GetTempDir(), "buildah-rootfs")
188if err != nil {
189return nil, WorkloadConfig{}, fmt.Errorf("setting up parent for container root filesystem: %w", err)
190}
191defer func() {
192if err := os.RemoveAll(rootfsParentDir); err != nil {
193logger.Warnf("cleaning up parent for container root filesystem: %v", err)
194}
195}()
196// Create a mountpoint for the new overlay, which we'll use as the rootfs.
197rootfsDir := filepath.Join(rootfsParentDir, "rootfs")
198if err := idtools.MkdirAndChown(rootfsDir, fs.FileMode(st.Mode()), idtools.IDPair{UID: int(st.UID()), GID: int(st.GID())}); err != nil {
199return nil, WorkloadConfig{}, fmt.Errorf("creating mount target for container root filesystem: %w", err)
200}
201defer func() {
202if err := os.Remove(rootfsDir); err != nil {
203logger.Warnf("removing mount target for container root filesystem: %v", err)
204}
205}()
206// Create a directory to hold all of the overlay package's working state.
207tempDir := filepath.Join(rootfsParentDir, "tmp")
208if err = os.Mkdir(tempDir, 0o700); err != nil {
209return nil, WorkloadConfig{}, err
210}
211// Create some working state in there.
212overlayTempDir, err := overlay.TempDir(tempDir, int(st.UID()), int(st.GID()))
213if err != nil {
214return nil, WorkloadConfig{}, fmt.Errorf("setting up mount of container root filesystem: %w", err)
215}
216defer func() {
217if err := overlay.RemoveTemp(overlayTempDir); err != nil {
218logger.Warnf("cleaning up mount of container root filesystem: %v", err)
219}
220}()
221// Create a mount point using that working state.
222rootfsMount, err := overlay.Mount(overlayTempDir, rootfsPath, rootfsDir, 0, 0, options.GraphOptions)
223if err != nil {
224return nil, WorkloadConfig{}, fmt.Errorf("setting up support for overlay of container root filesystem: %w", err)
225}
226defer func() {
227if err := overlay.Unmount(overlayTempDir); err != nil {
228logger.Warnf("unmounting support for overlay of container root filesystem: %v", err)
229}
230}()
231// Follow through on the overlay or bind mount, whatever the overlay package decided
232// to leave to us to do.
233rootfsMountOptions := strings.Join(rootfsMount.Options, ",")
234logrus.Debugf("mounting %q to %q as %q with options %v", rootfsMount.Source, rootfsMount.Destination, rootfsMount.Type, rootfsMountOptions)
235if err := mount.Mount(rootfsMount.Source, rootfsMount.Destination, rootfsMount.Type, rootfsMountOptions); err != nil {
236return nil, WorkloadConfig{}, fmt.Errorf("mounting overlay of container root filesystem: %w", err)
237}
238defer func() {
239logrus.Debugf("unmounting %q", rootfsMount.Destination)
240if err := mount.Unmount(rootfsMount.Destination); err != nil {
241logger.Warnf("unmounting overlay of container root filesystem: %v", err)
242}
243}()
244// Pretend that we didn't have to do any of the preceding.
245rootfsPath = rootfsDir
246
247// Write extra content to the rootfs, creating intermediate directories if necessary.
248for location, content := range options.ExtraImageContent {
249err := func() error {
250if err := idtools.MkdirAllAndChownNew(filepath.Dir(filepath.Join(rootfsPath, location)), 0o755, idtools.IDPair{UID: int(st.UID()), GID: int(st.GID())}); err != nil {
251return fmt.Errorf("ensuring %q is present in container root filesystem: %w", filepath.Dir(location), err)
252}
253output, err := os.OpenFile(filepath.Join(rootfsPath, location), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
254if err != nil {
255return fmt.Errorf("preparing to write %q to container root filesystem: %w", location, err)
256}
257defer output.Close()
258input, err := os.Open(content)
259if err != nil {
260return err
261}
262defer input.Close()
263if _, err := io.Copy(output, input); err != nil {
264return fmt.Errorf("copying contents of %q to %q in container root filesystem: %w", content, location, err)
265}
266if err := output.Chown(int(st.UID()), int(st.GID())); err != nil {
267return fmt.Errorf("setting owner of %q in the container root filesystem: %w", location, err)
268}
269if err := output.Chmod(0o644); err != nil {
270return fmt.Errorf("setting permissions on %q in the container root filesystem: %w", location, err)
271}
272return nil
273}()
274if err != nil {
275return nil, WorkloadConfig{}, err
276}
277}
278
279// Write part of the config blob where the krun init process will be
280// looking for it. The oci2cw tool used `buildah inspect` output, but
281// init is just looking for fields that have the right names in any
282// object, and the image's config will have that, so let's try encoding
283// it directly.
284krunConfigPath := filepath.Join(rootfsPath, ".krun_config.json")
285krunConfigBytes, err := json.Marshal(ociConfig)
286if err != nil {
287return nil, WorkloadConfig{}, fmt.Errorf("creating .krun_config from image configuration: %w", err)
288}
289if err := ioutils.AtomicWriteFile(krunConfigPath, krunConfigBytes, 0o600); err != nil {
290return nil, WorkloadConfig{}, fmt.Errorf("saving krun config: %w", err)
291}
292
293// Encode the workload config, in case it fails for any reason.
294cleanedUpWorkloadConfig := workloadConfig
295switch cleanedUpWorkloadConfig.Type {
296default:
297return nil, WorkloadConfig{}, fmt.Errorf("don't know how to canonicalize TEE type %q", cleanedUpWorkloadConfig.Type)
298case SEV, SEV_NO_ES:
299cleanedUpWorkloadConfig.Type = SEV
300case SNP:
301cleanedUpWorkloadConfig.Type = SNP
302}
303workloadConfigBytes, err := json.Marshal(cleanedUpWorkloadConfig)
304if err != nil {
305return nil, WorkloadConfig{}, err
306}
307
308// Make sure we have the passphrase to use for encrypting the disk image.
309diskEncryptionPassphrase := options.DiskEncryptionPassphrase
310if diskEncryptionPassphrase == "" {
311diskEncryptionPassphrase, err = GenerateDiskEncryptionPassphrase()
312if err != nil {
313return nil, WorkloadConfig{}, err
314}
315}
316
317// If we weren't told how big the image should be, get a rough estimate
318// of the input data size, then add a hedge to it.
319imageSize := slop(options.ImageSize, options.Slop)
320if imageSize == 0 {
321var sourceSize int64
322if err := filepath.WalkDir(rootfsPath, func(path string, d fs.DirEntry, err error) error {
323if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, os.ErrPermission) {
324return err
325}
326info, err := d.Info()
327if err != nil && !errors.Is(err, os.ErrNotExist) && !errors.Is(err, os.ErrPermission) {
328return err
329}
330sourceSize += info.Size()
331return nil
332}); err != nil {
333return nil, WorkloadConfig{}, err
334}
335imageSize = slop(sourceSize, options.Slop)
336}
337if imageSize%4096 != 0 {
338imageSize += (4096 - (imageSize % 4096))
339}
340if imageSize < minimumImageSize {
341imageSize = minimumImageSize
342}
343
344// Create a file to use as the unencrypted version of the disk image.
345plain, err := os.CreateTemp(options.TempDir, "plain.img")
346if err != nil {
347return nil, WorkloadConfig{}, err
348}
349removePlain := true
350defer func() {
351if removePlain {
352if err := os.Remove(plain.Name()); err != nil {
353logger.Warnf("removing temporary file %q: %v", plain.Name(), err)
354}
355}
356}()
357
358// Lengthen the plaintext disk image file.
359if err := plain.Truncate(imageSize); err != nil {
360plain.Close()
361return nil, WorkloadConfig{}, err
362}
363plainInfo, err := plain.Stat()
364plain.Close()
365if err != nil {
366return nil, WorkloadConfig{}, err
367}
368
369// Format the disk image with the filesystem contents.
370if _, stderr, err := MakeFS(rootfsPath, plain.Name(), filesystem); err != nil {
371if strings.TrimSpace(stderr) != "" {
372return nil, WorkloadConfig{}, fmt.Errorf("%s: %w", strings.TrimSpace(stderr), err)
373}
374return nil, WorkloadConfig{}, err
375}
376
377// If we're registering the workload, we can do that now.
378if workloadConfig.AttestationURL != "" {
379if err := SendRegistrationRequest(workloadConfig, diskEncryptionPassphrase, options.FirmwareLibrary, options.IgnoreAttestationErrors, logger); err != nil {
380return nil, WorkloadConfig{}, err
381}
382}
383
384// Try to encrypt on the fly.
385pipeReader, pipeWriter := io.Pipe()
386removePlain = false
387go func() {
388var err error
389defer func() {
390if err := os.Remove(plain.Name()); err != nil {
391logger.Warnf("removing temporary file %q: %v", plain.Name(), err)
392}
393if err != nil {
394pipeWriter.CloseWithError(err)
395} else {
396pipeWriter.Close()
397}
398}()
399plain, err := os.Open(plain.Name())
400if err != nil {
401logrus.Errorf("opening unencrypted disk image %q: %v", plain.Name(), err)
402return
403}
404defer plain.Close()
405tw := tar.NewWriter(pipeWriter)
406defer tw.Flush()
407
408// Write /entrypoint
409var decompressedEntrypoint bytes.Buffer
410decompressor, err := gzip.NewReader(bytes.NewReader(entrypointCompressedBytes))
411if err != nil {
412logrus.Errorf("decompressing copy of entrypoint: %v", err)
413return
414}
415defer decompressor.Close()
416if _, err = io.Copy(&decompressedEntrypoint, decompressor); err != nil {
417logrus.Errorf("decompressing copy of entrypoint: %v", err)
418return
419}
420entrypointHeader, err := tar.FileInfoHeader(plainInfo, "")
421if err != nil {
422logrus.Errorf("building header for entrypoint: %v", err)
423return
424}
425entrypointHeader.Name = "entrypoint"
426entrypointHeader.Mode = 0o755
427entrypointHeader.Uname, entrypointHeader.Gname = "", ""
428entrypointHeader.Uid, entrypointHeader.Gid = 0, 0
429entrypointHeader.Size = int64(decompressedEntrypoint.Len())
430if err = tw.WriteHeader(entrypointHeader); err != nil {
431logrus.Errorf("writing header for %q: %v", entrypointHeader.Name, err)
432return
433}
434if _, err = io.Copy(tw, &decompressedEntrypoint); err != nil {
435logrus.Errorf("writing %q: %v", entrypointHeader.Name, err)
436return
437}
438
439// Write /sev.chain
440if chainInfo != nil {
441chainHeader, err := tar.FileInfoHeader(chainInfo, "")
442if err != nil {
443logrus.Errorf("building header for %q: %v", chainInfo.Name(), err)
444return
445}
446chainHeader.Name = chainBytesFile
447chainHeader.Mode = 0o600
448chainHeader.Uname, chainHeader.Gname = "", ""
449chainHeader.Uid, chainHeader.Gid = 0, 0
450chainHeader.Size = int64(len(chainBytes))
451if err = tw.WriteHeader(chainHeader); err != nil {
452logrus.Errorf("writing header for %q: %v", chainHeader.Name, err)
453return
454}
455if _, err = tw.Write(chainBytes); err != nil {
456logrus.Errorf("writing %q: %v", chainHeader.Name, err)
457return
458}
459}
460
461// Write /krun-sev.json.
462workloadConfigHeader, err := tar.FileInfoHeader(plainInfo, "")
463if err != nil {
464logrus.Errorf("building header for %q: %v", plainInfo.Name(), err)
465return
466}
467workloadConfigHeader.Name = "krun-sev.json"
468workloadConfigHeader.Mode = 0o600
469workloadConfigHeader.Uname, workloadConfigHeader.Gname = "", ""
470workloadConfigHeader.Uid, workloadConfigHeader.Gid = 0, 0
471workloadConfigHeader.Size = int64(len(workloadConfigBytes))
472if err = tw.WriteHeader(workloadConfigHeader); err != nil {
473logrus.Errorf("writing header for %q: %v", workloadConfigHeader.Name, err)
474return
475}
476if _, err = tw.Write(workloadConfigBytes); err != nil {
477logrus.Errorf("writing %q: %v", workloadConfigHeader.Name, err)
478return
479}
480
481// Write /tmp.
482tmpHeader, err := tar.FileInfoHeader(plainInfo, "")
483if err != nil {
484logrus.Errorf("building header for %q: %v", plainInfo.Name(), err)
485return
486}
487tmpHeader.Name = "tmp/"
488tmpHeader.Typeflag = tar.TypeDir
489tmpHeader.Mode = 0o1777
490tmpHeader.Uname, tmpHeader.Gname = "", ""
491tmpHeader.Uid, tmpHeader.Gid = 0, 0
492tmpHeader.Size = 0
493if err = tw.WriteHeader(tmpHeader); err != nil {
494logrus.Errorf("writing header for %q: %v", tmpHeader.Name, err)
495return
496}
497
498// Now figure out the footer that we'll append to the encrypted disk.
499var footer bytes.Buffer
500lengthBuffer := make([]byte, 8)
501footer.Write(workloadConfigBytes)
502footer.WriteString("KRUN")
503binary.LittleEndian.PutUint64(lengthBuffer, uint64(len(workloadConfigBytes)))
504footer.Write(lengthBuffer)
505
506// Start encrypting and write /disk.img.
507header, encrypt, blockSize, err := luksy.EncryptV1([]string{diskEncryptionPassphrase}, "")
508paddingBoundary := int64(4096)
509paddingNeeded := (paddingBoundary - ((int64(len(header)) + imageSize + int64(footer.Len())) % paddingBoundary)) % paddingBoundary
510diskHeader := workloadConfigHeader
511diskHeader.Name = "disk.img"
512diskHeader.Mode = 0o600
513diskHeader.Size = int64(len(header)) + imageSize + paddingNeeded + int64(footer.Len())
514if err = tw.WriteHeader(diskHeader); err != nil {
515logrus.Errorf("writing archive header for disk.img: %v", err)
516return
517}
518if _, err = io.Copy(tw, bytes.NewReader(header)); err != nil {
519logrus.Errorf("writing encryption header for disk.img: %v", err)
520return
521}
522encryptWrapper := luksy.EncryptWriter(encrypt, tw, blockSize)
523if _, err = io.Copy(encryptWrapper, plain); err != nil {
524logrus.Errorf("encrypting disk.img: %v", err)
525return
526}
527encryptWrapper.Close()
528if _, err = tw.Write(make([]byte, paddingNeeded)); err != nil {
529logrus.Errorf("writing padding for disk.img: %v", err)
530return
531}
532if _, err = io.Copy(tw, &footer); err != nil {
533logrus.Errorf("writing footer for disk.img: %v", err)
534return
535}
536tw.Close()
537}()
538
539return pipeReader, workloadConfig, nil
540}
541
542func slop(size int64, slop string) int64 {
543if slop == "" {
544return size * 5 / 4
545}
546for _, factor := range strings.Split(slop, "+") {
547factor = strings.TrimSpace(factor)
548if factor == "" {
549continue
550}
551if strings.HasSuffix(factor, "%") {
552percentage := strings.TrimSuffix(factor, "%")
553percent, err := strconv.ParseInt(percentage, 10, 8)
554if err != nil {
555logrus.Warnf("parsing percentage %q: %v", factor, err)
556} else {
557size *= (percent + 100)
558size /= 100
559}
560} else {
561more, err := units.RAMInBytes(factor)
562if err != nil {
563logrus.Warnf("parsing %q as a size: %v", factor, err)
564} else {
565size += more
566}
567}
568}
569return size
570}
571