Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TPM PCR measurement precalculation #74

Merged
merged 4 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,61 @@ go install github.com/edgelesssys/uplosi@latest

Alternatively, you can download the binary from the [releases page](https://github.com/edgelesssys/uplosi/releases/latest).

# Usage
# Uploading OS Images

The main purpose of uplosi is to upload OS images to cloud providers.
Uploading images requires a [configuration file](#configuration) to be present in the current working directory.

## Usage

```shell-session
uplosi upload <image> [flags]
```

## Examples
### Examples

```shell-session
# edit uplosi.conf, then run
uplosi upload image.raw -i
```

## Flags
### Flags

- `--disable-variant-glob` string: list of variant name globs to disable
- `--enable-variant-glob` string: list of variant name globs to enable
- `-h`,`--help`: help for uplosi
- `-i`,`--increment-version`: increment version number after upload
- `-v`: version for uplosi

# Calculating TPM PCR Values

> [!WARNING]
> This command is highly experimental. It does not account for all PCRs and all possibilities of their measurements,
> is only tested in a very specific environment and should not be used in production use-cases.

Uplosi can also, from a given raw disk image, calculate TPM PCR values (Namely PCRs 4, 9, and 11)
ahead of the image boot to allow to craft remote attestation policies for images.
It requires `systemd-dissect` to be present in `$PATH`.

## Usage

```shell-session
sudo uplosi measurements <image> [flags]
```

### Examples

```shell-session
sudo uplosi measurements image.raw --output-file pcrs.json
```

### Flags

- `--output-file` string: path to a JSON file the output should be written to
- `--uki-path` string: path to the unified kernel image (UKI) within the ESP of the image (default: `/boot/EFI/BOOT/BOOTX64.EFI`)
- `-h`,`--help`: help for uplosi
- `-v`: version for uplosi

# Configuration

Uplosi requires configuration files in [TOML format](https://toml.io/en/) to be present in the user's workspace (CWD).
Expand Down
297 changes: 1 addition & 296 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,9 @@ SPDX-License-Identifier: Apache-2.0
package main

import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strconv"
"strings"

"github.com/BurntSushi/toml"
"github.com/edgelesssys/uplosi/aws"
"github.com/edgelesssys/uplosi/azure"
"github.com/edgelesssys/uplosi/config"
"github.com/edgelesssys/uplosi/gcp"
"github.com/edgelesssys/uplosi/openstack"
"github.com/spf13/cobra"
"golang.org/x/mod/semver"
)

const (
configName = "uplosi.conf"
configDir = "uplosi.conf.d"
)

var version = "0.0.0-dev"
Expand All @@ -44,281 +23,7 @@ func newRootCmd() *cobra.Command {
cmd.SetOut(os.Stdout)
cmd.InitDefaultVersionFlag()
cmd.AddCommand(newUploadCmd())
cmd.AddCommand(newMeasurementsCmd())

return cmd
}

func newUploadCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "upload <image>",
Short: "Upload an image to a cloud provider",
Args: cobra.ExactArgs(1),
RunE: runUpload,
}
cmd.Flags().BoolP("increment-version", "i", false, "increment version number after upload")
cmd.Flags().StringSlice("enable-variant-glob", []string{"*"}, "list of variant name globs to enable")
cmd.Flags().StringSlice("disable-variant-glob", nil, "list of variant name globs to disable")
cmd.Flags().StringP("config", "c", "", fmt.Sprintf("path to directory %s and %s resides in", configName, configDir))

return cmd
}

func runUpload(cmd *cobra.Command, args []string) error {
logger := log.New(cmd.ErrOrStderr(), "", log.LstdFlags)
imagePath := args[0]

flags, err := parseUploadFlags(cmd)
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}

conf, err := parseConfigFiles(flags.configPath)
if err != nil {
return fmt.Errorf("parsing config files: %w", err)
}

versionFiles := map[string][]byte{}
versionFileLookup := func(name string) ([]byte, error) {
if _, ok := versionFiles[name]; !ok {
ver, err := os.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("reading version file: %w", err)
}
versionFiles[name] = ver
}
return versionFiles[name], nil
}

allRefs := []string{}
err = conf.ForEach(
func(name string, cfg config.Config) error {
refs, err := uploadVariant(cmd.Context(), imagePath, name, cfg, logger)
if err != nil {
return err
}
allRefs = append(allRefs, refs...)
return nil
},
versionFileLookup,
func(name string) bool {
return filterGlobAny(flags.enableVariantGlobs, name)
},
func(name string) bool {
return !filterGlobAny(flags.disableVariantGlobs, name)
},
)
if err != nil {
return fmt.Errorf("uploading variants: %w", err)
}

for _, ref := range allRefs {
fmt.Println(ref)
}

if !flags.incrementVersion {
return nil
}
if len(versionFiles) == 0 {
return errors.New("increment-version flag set but no version files found")
}
for versionFileName, version := range versionFiles {
newVer, err := incrementSemver(strings.TrimSpace(string(version)))
if err != nil {
return fmt.Errorf("incrementing semver: %w", err)
}
if err := writeVersionFile(versionFileName, []byte(newVer)); err != nil {
return fmt.Errorf("writing version file: %w", err)
}
}
return nil
}

func uploadVariant(ctx context.Context, imagePath, variant string, config config.Config, logger *log.Logger) ([]string, error) {
var prepper Prepper
var upload Uploader
var err error

if len(variant) > 0 {
log.Println("Uploading variant", variant)
}

switch strings.ToLower(config.Provider) {
case "aws":
prepper = &aws.Prepper{}
upload, err = aws.NewUploader(config, logger)
if err != nil {
return nil, fmt.Errorf("creating aws uploader: %w", err)
}
case "azure":
prepper = &azure.Prepper{}
upload, err = azure.NewUploader(config, logger)
if err != nil {
return nil, fmt.Errorf("creating azure uploader: %w", err)
}
case "gcp":
prepper = &gcp.Prepper{}
upload, err = gcp.NewUploader(config, logger)
if err != nil {
return nil, fmt.Errorf("creating gcp uploader: %w", err)
}
case "openstack":
prepper = &openstack.Prepper{}
upload, err = openstack.NewUploader(config, logger)
if err != nil {
return nil, fmt.Errorf("creating openstack uploader: %w", err)
}
default:
return nil, fmt.Errorf("unknown provider: %s", config.Provider)
}

tmpDir, err := os.MkdirTemp("", "uplosi-")
if err != nil {
return nil, fmt.Errorf("creating temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)

imagePath, err = prepper.Prepare(ctx, imagePath, tmpDir)
if err != nil {
return nil, fmt.Errorf("preparing image: %w", err)
}
image, err := os.Open(imagePath)
if err != nil {
return nil, fmt.Errorf("opening image: %w", err)
}
defer image.Close()
imageFi, err := image.Stat()
if err != nil {
return nil, fmt.Errorf("getting image stats: %w", err)
}

refs, err := upload.Upload(ctx, image, imageFi.Size())
if err != nil {
return nil, fmt.Errorf("uploading image: %w", err)
}

return refs, nil
}

type uploadFlags struct {
incrementVersion bool
enableVariantGlobs []string
disableVariantGlobs []string
configPath string
}

func parseUploadFlags(cmd *cobra.Command) (*uploadFlags, error) {
incrementVersion, err := cmd.Flags().GetBool("increment-version")
if err != nil {
return nil, fmt.Errorf("getting increment-version flag: %w", err)
}
enableVariantGlobs, err := cmd.Flags().GetStringSlice("enable-variant-glob")
if err != nil {
return nil, fmt.Errorf("getting enable-variant-glob flag: %w", err)
}
disableVariantGlobs, err := cmd.Flags().GetStringSlice("disable-variant-glob")
if err != nil {
return nil, fmt.Errorf("getting disable-variant-glob flag: %w", err)
}
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return nil, fmt.Errorf("getting config flag: %w", err)
}
return &uploadFlags{
incrementVersion: incrementVersion,
enableVariantGlobs: enableVariantGlobs,
disableVariantGlobs: disableVariantGlobs,
configPath: configPath,
}, nil
}

func filterGlobAny(globs []string, name string) bool {
for _, glob := range globs {
if ok, _ := filepath.Match(glob, name); ok {
return true
}
}
return false
}

func readTOMLFile(path string, data any) error {
configFile, err := os.OpenFile(path, os.O_RDONLY, os.ModeAppend)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
defer configFile.Close()
if _, err := toml.NewDecoder(configFile).Decode(data); err != nil {
return fmt.Errorf("decoding file: %w", err)
}
return nil
}

func writeVersionFile(path string, data []byte) error {
versionFile, err := os.OpenFile(path, os.O_WRONLY, os.ModeAppend)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
defer versionFile.Close()
if _, err := versionFile.Write(data); err != nil {
return fmt.Errorf("writing file: %w", err)
}
return nil
}

type Prepper interface {
Prepare(ctx context.Context, imagePath, tmpDir string) (string, error)
}

type Uploader interface {
Upload(ctx context.Context, image io.ReadSeeker, size int64) (refs []string, retErr error)
}

func parseConfigFiles(configPath string) (*config.ConfigFile, error) {
configLocation := path.Join(configPath, configName)
configDirLocation := path.Join(configPath, configDir)

var conf config.ConfigFile
if err := readTOMLFile(configLocation, &conf); err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}

dirEntries, err := os.ReadDir(configDirLocation)
if os.IsNotExist(err) {
return &conf, nil
}
if err != nil {
return nil, fmt.Errorf("reading config dir: %w", err)
}
for _, dirEntry := range dirEntries {
var cfgOverlay config.ConfigFile
if dirEntry.IsDir() {
continue
}
if filepath.Ext(dirEntry.Name()) != ".conf" {
continue
}
if err := readTOMLFile(filepath.Join(configDir, dirEntry.Name()), &cfgOverlay); err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
if err := conf.Merge(cfgOverlay); err != nil {
return nil, fmt.Errorf("merging config: %w", err)
}
}
return &conf, nil
}

func incrementSemver(version string) (string, error) {
canonical := strings.TrimPrefix(semver.Canonical("v"+version), "v")
parts := strings.Split(canonical, ".")
if len(parts) != 3 {
return "", fmt.Errorf("splitting canonical version: %s, %v", canonical, parts)
}

patch := parts[2]
patchNum, err := strconv.Atoi(patch)
if err != nil {
return "", fmt.Errorf("converting patch number: %w", err)
}

patchNum++
return fmt.Sprintf("%s.%s.%d", parts[0], parts[1], patchNum), nil
}
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
version = "0.2.0";
src = ./.;
# this needs to be updated together with go.mod / go.sum
vendorHash = "sha256-1o30yZCx43FsFn+b6OQgRluJhBD9cSPXpXQv++IvxoI=";
vendorHash = "sha256-7Pe52of/RZZ8qzQS6roSduHAi6n61r61jkHkOVL6ylY=";

CGO_ENABLED = 0;

Expand Down
Loading