blob: 3c482276d893c8d16c0e7464a4f4b701d4683d95 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Tim Windelschmidt9f21f532024-05-07 15:14:20 +02004package launch
Serge Bazanski2b6dc312024-06-04 17:44:55 +02005
6import (
7 "context"
8 "fmt"
9 "log"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "sort"
14 "strings"
15
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020016 "source.monogon.dev/osbase/test/launch"
Serge Bazanski2b6dc312024-06-04 17:44:55 +020017)
18
19// A TPMFactory manufactures virtual TPMs using swtpm.
20//
21// A factory has an assigned state directory into which it will write per-factory
22// data (like CA certificates and keys). Each manufactured TPM also has a state
23// directory, which is first generated on manufacturing, and then passed to an
24// swtpm instance.
25type TPMFactory struct {
26 stateDir string
27}
28
29// NewTPMFactory creates a new TPM factory at a given state path. The state path
30// is a directory used to persist TPM factory data. It will be created if needed,
31// and can be reused across TPM factories (but not used in parallel).
32func NewTPMFactory(stateDir string) (*TPMFactory, error) {
33 if err := os.MkdirAll(stateDir, 0744); err != nil {
34 return nil, fmt.Errorf("could not create state directory: %w", err)
35 }
36
37 f := &TPMFactory{
38 stateDir: stateDir,
39 }
40
41 if err := os.MkdirAll(f.caDir(), 0700); err != nil {
42 return nil, fmt.Errorf("could not create CA state directory: %w", err)
43 }
44 err := writeSWTPMConfig(f.localCAConfPath(), map[string]string{
45 "statedir": f.caDir(),
46 "signingkey": filepath.Join(f.caDir(), "signkey.pem"),
47 "issuercert": filepath.Join(f.caDir(), "issuercert.pem"),
48 "certserial": filepath.Join(f.caDir(), "certserial"),
49 })
50 if err != nil {
51 return nil, err
52 }
53 return f, nil
54}
55
56func (f *TPMFactory) caDir() string {
57 return filepath.Join(f.stateDir, "ca")
58}
59
60func (f *TPMFactory) localCAConfPath() string {
61 return filepath.Join(f.caDir(), "swtpm-localca.conf")
62}
63
64func (f *TPMFactory) localCAOptionsPath() string {
65 return filepath.Join(f.caDir(), "swtpm-localca.options")
66}
67
68func (f *TPMFactory) swtpmConfPath() string {
69 return filepath.Join(f.stateDir, "swtpm.conf")
70}
71
72// writeSWTPMConfig serializes a key/value config file for swtpm tools into a
73// path.
74func writeSWTPMConfig(path string, data map[string]string) error {
75 var keys []string
76 for k := range data {
77 keys = append(keys, k)
78 }
79 sort.Strings(keys)
80
81 f, err := os.Create(path)
82 if err != nil {
83 return err
84 }
85 defer f.Close()
86 for _, k := range keys {
87 if _, err := fmt.Fprintf(f, "%s = %s\n", k, data[k]); err != nil {
88 return err
89 }
90 }
91 return nil
92}
93
94// A TPMPlatform defines a platform that a TPM is part of. This will usually be
95// some kind of device, in this case a virtual device.
96type TPMPlatform struct {
97 Manufacturer string
98 Version string
99 Model string
100}
101
102// Manufacture builds a new TPM for a given platform at a path. The path points
103// to a directory that will be created if it doens't exist yet, and can be passed
104// to swtpm to actually emulate the created TPM.
105func (f *TPMFactory) Manufacture(ctx context.Context, path string, platform *TPMPlatform) error {
106 launch.Log("Starting to manufacture TPM for %s... (%+v)", path, platform)
107
108 // Path to state file. Used to make sure Manufacture runs only once.
109 permall := filepath.Join(path, "tpm2-00.permall")
110
111 if _, err := os.Stat(permall); err == nil {
112 launch.Log("Skipping manufacturing TPM for %s, already exists", path)
113 return nil
114 }
Serge Bazanski2b6dc312024-06-04 17:44:55 +0200115 // Prepare swtpm-localca.options.
116 options := []string{
117 "--platform-manufacturer " + platform.Manufacturer,
118 "--platform-version " + platform.Version,
119 "--platform-model " + platform.Model,
120 "",
121 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000122 err := os.WriteFile(f.localCAOptionsPath(), []byte(strings.Join(options, "\n")), 0600)
Serge Bazanski2b6dc312024-06-04 17:44:55 +0200123 if err != nil {
124 return fmt.Errorf("could not write local options: %w", err)
125 }
126
127 // Prepare swptm.conf.
128 err = writeSWTPMConfig(f.swtpmConfPath(), map[string]string{
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000129 "create_certs_tool": xSwtpmLocalCAPath,
Serge Bazanski2b6dc312024-06-04 17:44:55 +0200130 "create_certs_tool_config": f.localCAConfPath(),
131 "create_certs_tool_options": f.localCAOptionsPath(),
132 })
133 if err != nil {
134 return fmt.Errorf("could not write swtpm.conf: %w", err)
135 }
136
137 if err := os.MkdirAll(path, 0700); err != nil {
138 return fmt.Errorf("could not make output path: %w", err)
139 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000140 cmd := exec.CommandContext(ctx, xSwtpmSetupPath,
141 "--tpm", fmt.Sprintf("%s socket", xSwtpmPath),
Serge Bazanski2b6dc312024-06-04 17:44:55 +0200142 "--tpmstate", path,
143 "--create-ek-cert",
144 "--create-platform-cert",
145 "--allow-signing",
146 "--tpm2",
147 "--display",
148 "--pcr-banks", "sha1,sha256,sha384,sha512",
149 "--config", f.swtpmConfPath())
Tim Windelschmidt82e6af72024-07-23 00:05:42 +0000150 cmd.Env = append(cmd.Env, fmt.Sprintf("PATH=%s:%s", filepath.Dir(xSwtpmCertPath), filepath.Dir(xCerttoolPath)))
Serge Bazanski2b6dc312024-06-04 17:44:55 +0200151 cmd.Env = append(cmd.Env, "MONOGON_LIBTPMS_ACKNOWLEDGE_UNSAFE=yes")
152 if out, err := cmd.CombinedOutput(); err != nil {
153 log.Printf("Manufacturing TPM for %s failed: swtm_setup: %s", path, out)
154 return fmt.Errorf("swtpm_setup failed: %w", err)
155 }
156
157 if _, err := os.Stat(permall); os.IsNotExist(err) {
158 log.Printf("Manufacturing TPM for %s failed: state file did not get created", path)
159 return fmt.Errorf("%s did not get created during TPM manufacture", permall)
160 }
161
162 launch.Log("Successfully manufactured TPM for %s", path)
163 return nil
164}