1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8
9 "github.com/google/go-containerregistry/pkg/authn"
10 "github.com/google/go-containerregistry/pkg/name"
11 v1 "github.com/google/go-containerregistry/pkg/v1"
12 "github.com/google/go-containerregistry/pkg/v1/daemon"
13 "github.com/google/go-containerregistry/pkg/v1/remote"
14 "github.com/google/go-containerregistry/pkg/v1/tarball"
15 log "github.com/sirupsen/logrus"
16 "github.com/urfave/cli"
17
18 "github.com/Microsoft/hcsshim/ext4/dmverity"
19 "github.com/Microsoft/hcsshim/ext4/tar2ext4"
20 )
21
22 const usage = `dmverity-vhd is a command line tool for creating LCOW layer VHDs with dm-verity hashes.`
23
24 const (
25 usernameFlag = "username"
26 passwordFlag = "password"
27 imageFlag = "image"
28 verboseFlag = "verbose"
29 outputDirFlag = "out-dir"
30 dockerFlag = "docker"
31 tarballFlag = "tarball"
32 hashDeviceVhdFlag = "hash-dev-vhd"
33 maxVHDSize = dmverity.RecommendedVHDSizeGB
34 )
35
36 func init() {
37 log.SetFormatter(&log.TextFormatter{
38 DisableTimestamp: true,
39 })
40
41 log.SetOutput(os.Stdout)
42
43 log.SetLevel(log.WarnLevel)
44 }
45
46 func main() {
47 cli.VersionFlag = cli.BoolFlag{
48 Name: "version",
49 }
50
51 app := cli.NewApp()
52 app.Name = "dmverity-vhd"
53 app.Commands = []cli.Command{
54 createVHDCommand,
55 rootHashVHDCommand,
56 }
57 app.Usage = usage
58 app.Flags = []cli.Flag{
59 cli.BoolFlag{
60 Name: verboseFlag + ",v",
61 Usage: "Optional: verbose output",
62 },
63 cli.BoolFlag{
64 Name: dockerFlag + ",d",
65 Usage: "Optional: use local docker daemon",
66 },
67 cli.StringFlag{
68 Name: tarballFlag + ",t",
69 Usage: "Optional: path to tarball containing image info",
70 },
71 }
72
73 if err := app.Run(os.Args); err != nil {
74 _, _ = fmt.Fprintln(os.Stderr, err)
75 os.Exit(1)
76 }
77 }
78
79 func fetchImageLayers(ctx *cli.Context) (layers []v1.Layer, err error) {
80 image := ctx.String(imageFlag)
81 tarballPath := ctx.GlobalString(tarballFlag)
82 ref, err := name.ParseReference(image)
83 if err != nil {
84 return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
85 }
86
87 dockerDaemon := ctx.GlobalBool(dockerFlag)
88
89
90 if dockerDaemon && tarballPath != "" {
91 return nil, errors.New("cannot use both docker and tarball for image source")
92 }
93
94
95 var img v1.Image
96 if tarballPath != "" {
97
98 var imageNameAndTag name.Tag
99 imageNameAndTag, err = name.NewTag(image)
100 if err != nil {
101 return nil, fmt.Errorf("failed to failed to create a tag to search tarball for %s: %w", image, err)
102 }
103
104 img, err = tarball.ImageFromPath(tarballPath, &imageNameAndTag)
105 } else if dockerDaemon {
106
107
108 var opts []daemon.Option
109 opt := daemon.WithUnbufferedOpener()
110 opts = append(opts, opt)
111
112 img, err = daemon.Image(ref, opts...)
113 } else {
114 var remoteOpts []remote.Option
115 if ctx.IsSet(usernameFlag) && ctx.IsSet(passwordFlag) {
116 auth := authn.Basic{
117 Username: ctx.String(usernameFlag),
118 Password: ctx.String(passwordFlag),
119 }
120 authConf, err := auth.Authorization()
121 if err != nil {
122 return nil, fmt.Errorf("failed to set remote: %w", err)
123 }
124 log.Debug("using basic auth")
125 authOpt := remote.WithAuth(authn.FromConfig(*authConf))
126 remoteOpts = append(remoteOpts, authOpt)
127 }
128
129 img, err = remote.Image(ref, remoteOpts...)
130 }
131 if err != nil {
132 return nil, fmt.Errorf("unable to fetch image %q, make sure it exists: %w", image, err)
133 }
134 conf, _ := img.ConfigName()
135 log.Debugf("Image id: %s", conf.String())
136 return img.Layers()
137 }
138
139 var createVHDCommand = cli.Command{
140 Name: "create",
141 Usage: "creates LCOW layer VHDs inside the output directory with dm-verity super block and merkle tree appended at the end",
142 Flags: []cli.Flag{
143 cli.StringFlag{
144 Name: imageFlag + ",i",
145 Usage: "Required: container image reference",
146 Required: true,
147 },
148 cli.StringFlag{
149 Name: outputDirFlag + ",o",
150 Usage: "Required: output directory path",
151 Required: true,
152 },
153 cli.StringFlag{
154 Name: usernameFlag + ",u",
155 Usage: "Optional: custom registry username",
156 },
157 cli.StringFlag{
158 Name: passwordFlag + ",p",
159 Usage: "Optional: custom registry password",
160 },
161 cli.BoolFlag{
162 Name: hashDeviceVhdFlag + ",hdv",
163 Usage: "Optional: save hash-device as a VHD",
164 },
165 },
166 Action: func(ctx *cli.Context) error {
167 verbose := ctx.GlobalBool(verboseFlag)
168 if verbose {
169 log.SetLevel(log.DebugLevel)
170 }
171
172 layers, err := fetchImageLayers(ctx)
173 if err != nil {
174 return fmt.Errorf("failed to fetch image layers: %w", err)
175 }
176
177 outDir := ctx.String(outputDirFlag)
178 if _, err := os.Stat(outDir); os.IsNotExist(err) {
179 log.Debugf("creating output directory %q", outDir)
180 if err := os.MkdirAll(outDir, 0755); err != nil {
181 return fmt.Errorf("failed to create output directory %s: %w", outDir, err)
182 }
183 }
184
185 log.Debug("creating layer VHDs with dm-verity")
186 for layerNumber, layer := range layers {
187 if err := createVHD(layerNumber, layer, ctx.String(outputDirFlag), ctx.Bool(hashDeviceVhdFlag)); err != nil {
188 return err
189 }
190 }
191 return nil
192 },
193 }
194
195 func createVHD(layerNumber int, layer v1.Layer, outDir string, verityHashDev bool) error {
196 diffID, err := layer.DiffID()
197 if err != nil {
198 return fmt.Errorf("failed to read layer diff: %w", err)
199 }
200
201 log.WithFields(log.Fields{
202 "layerNumber": layerNumber,
203 "layerDiff": diffID.String(),
204 }).Debug("converting tar to layer VHD")
205
206 rc, err := layer.Uncompressed()
207 if err != nil {
208 return fmt.Errorf("failed to uncompress layer %s: %w", diffID.String(), err)
209 }
210 defer rc.Close()
211
212 vhdPath := filepath.Join(outDir, diffID.Hex+".vhd")
213 out, err := os.Create(vhdPath)
214 if err != nil {
215 return fmt.Errorf("failed to create layer vhd file %s: %w", vhdPath, err)
216 }
217 defer out.Close()
218
219 opts := []tar2ext4.Option{
220 tar2ext4.ConvertWhiteout,
221 tar2ext4.MaximumDiskSize(maxVHDSize),
222 }
223 if !verityHashDev {
224 opts = append(opts, tar2ext4.AppendDMVerity)
225 }
226 if err := tar2ext4.Convert(rc, out, opts...); err != nil {
227 return fmt.Errorf("failed to convert tar to ext4: %w", err)
228 }
229 if verityHashDev {
230 hashDevPath := filepath.Join(outDir, diffID.Hex+".hash-dev.vhd")
231 hashDev, err := os.Create(hashDevPath)
232 if err != nil {
233 return fmt.Errorf("failed to create hash device VHD file: %w", err)
234 }
235 defer hashDev.Close()
236
237 if err := dmverity.ComputeAndWriteHashDevice(out, hashDev); err != nil {
238 return err
239 }
240 if err := tar2ext4.ConvertToVhd(hashDev); err != nil {
241 return err
242 }
243 fmt.Fprintf(os.Stdout, "hash device created at %s\n", hashDevPath)
244 }
245 if err := tar2ext4.ConvertToVhd(out); err != nil {
246 return fmt.Errorf("failed to append VHD footer: %w", err)
247 }
248 fmt.Fprintf(os.Stdout, "Layer VHD created at %s\n", vhdPath)
249 return nil
250 }
251
252 var rootHashVHDCommand = cli.Command{
253 Name: "roothash",
254 Usage: "compute root hashes for each LCOW layer VHD",
255 Flags: []cli.Flag{
256 cli.StringFlag{
257 Name: imageFlag + ",i",
258 Usage: "Required: container image reference",
259 Required: true,
260 },
261 cli.StringFlag{
262 Name: usernameFlag + ",u",
263 Usage: "Optional: custom registry username",
264 },
265 cli.StringFlag{
266 Name: passwordFlag + ",p",
267 Usage: "Optional: custom registry password",
268 },
269 },
270 Action: func(ctx *cli.Context) error {
271 verbose := ctx.GlobalBool(verboseFlag)
272 if verbose {
273 log.SetLevel(log.DebugLevel)
274 }
275
276 layers, err := fetchImageLayers(ctx)
277 if err != nil {
278 return fmt.Errorf("failed to fetch image layers: %w", err)
279 }
280 log.Debugf("%d layers found", len(layers))
281
282 convertFunc := func(layer v1.Layer) (string, error) {
283 rc, err := layer.Uncompressed()
284 if err != nil {
285 return "", err
286 }
287 defer rc.Close()
288
289 hash, err := tar2ext4.ConvertAndComputeRootDigest(rc)
290 if err != nil {
291 return "", err
292 }
293 return hash, err
294 }
295
296 for layerNumber, layer := range layers {
297 diffID, err := layer.DiffID()
298 if err != nil {
299 return fmt.Errorf("failed to read layer diff: %w", err)
300 }
301 log.WithFields(log.Fields{
302 "layerNumber": layerNumber,
303 "layerDiff": diffID.String(),
304 }).Debug("uncompressed layer")
305
306 hash, err := convertFunc(layer)
307 if err != nil {
308 return fmt.Errorf("failed to compute root digest: %w", err)
309 }
310 fmt.Fprintf(os.Stdout, "Layer %d root hash: %s\n", layerNumber, hash)
311 }
312 return nil
313 },
314 }
315
View as plain text