config: automatically upload new Azure SNP versions to API + sign version with release key (#1854)

* sign version with release key and remove version from fetcher interface
* extend azure-reporter GH action to upload updated version values to the Attestation API
This commit is contained in:
Adrian Stobbe 2023-06-02 12:10:22 +02:00 committed by GitHub
parent 18da9b8128
commit a813760f96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 214 additions and 76 deletions

View file

@ -46,12 +46,13 @@ func (i *IsolationTEE) PrintSVNs() {
func main() {
configAPIExportPath := flag.String("export-path", "azure-sev-snp-version.json", "Path to the exported config API file.")
maaJWT := flag.String("report", "", "MAA JWT report to verify")
flag.Parse()
if len(os.Args) != 2 {
fmt.Println("Usage:", os.Args[0], "<maa-jwt>")
if *maaJWT == "" {
fmt.Println("Must provide --report")
return
}
report, err := getTEEReport(os.Args[1])
report, err := getTEEReport(*maaJWT)
if err != nil {
panic(err)
}

View file

@ -1,4 +1,5 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "cmd",
@ -8,7 +9,18 @@ go_library(
deps = [
"//internal/api/attestationconfig",
"//internal/api/attestationconfig/client",
"//internal/api/attestationconfig/fetcher",
"//internal/staticupload",
"@com_github_spf13_cobra//:cobra",
],
)
go_test(
name = "cmd_test",
srcs = ["root_test.go"],
embed = [":cmd"],
deps = [
"//internal/api/attestationconfig",
"@com_github_stretchr_testify//assert",
],
)

View file

@ -10,27 +10,32 @@ import (
"encoding/json"
"fmt"
"os"
"reflect"
"time"
configapi "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
attestationconfigclient "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/spf13/cobra"
)
const (
awsRegion = "eu-central-1"
awsBucket = "cdn-constellation-backend"
invalidDefault = 0
envAwsKeyID = "AWS_ACCESS_KEY_ID"
envAwsKey = "AWS_ACCESS_KEY"
awsRegion = "eu-central-1"
awsBucket = "cdn-constellation-backend"
invalidDefault = 0
envAwsKeyID = "AWS_ACCESS_KEY_ID"
envAwsKey = "AWS_ACCESS_KEY"
envCosignPwd = "COSIGN_PASSWORD"
envCosignPrivateKey = "COSIGN_PRIVATE_KEY"
)
var (
versionFilePath string
// Cosign credentials.
cosignPwd string
privateKeyPath string
cosignPwd string
privateKey string
)
// Execute executes the root command.
@ -41,58 +46,102 @@ func Execute() error {
// newRootCmd creates the root command.
func newRootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "AWS_ACCESS_KEY_ID=$ID AWS_ACCESS_KEY=$KEY upload -b 2 -t 0 -s 6 -m 93 --cosign-pwd $PWD --private-key $FILE_PATH",
Use: "COSIGN_PASSWORD=$CPWD COSIGN_PRIVATE_KEY=$CKEY AWS_ACCESS_KEY_ID=$ID AWS_ACCESS_KEY=$KEY upload --version-file $FILE",
Short: "Upload a set of versions specific to the azure-sev-snp attestation variant to the config api.",
Long: "Upload a set of versions specific to the azure-sev-snp attestation variant to the config api. Please authenticate with AWS through your preferred method (e.g. environment variables, CLI) to be able to upload to S3.",
RunE: runCmd,
Long: fmt.Sprintf("Upload a set of versions specific to the azure-sev-snp attestation variant to the config api. Please authenticate with AWS through your preferred method (e.g. environment variables, CLI) to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.", envCosignPrivateKey, envCosignPwd),
PreRunE: envCheck,
RunE: runCmd,
}
rootCmd.PersistentFlags().StringVarP(&versionFilePath, "version-file", "f", "", "File path to the version json file.")
rootCmd.PersistentFlags().StringVar(&cosignPwd, "cosign-pwd", "", "Cosign password used to decrpyt the private key.")
rootCmd.PersistentFlags().StringVar(&privateKeyPath, "private-key", "", "File path of private key used to sign the payload.")
must(enforceRequiredFlags(rootCmd, "version-file", "cosign-pwd", "private-key"))
must(enforceRequiredFlags(rootCmd, "version-file"))
return rootCmd
}
func envCheck(_ *cobra.Command, _ []string) error {
if os.Getenv(envCosignPrivateKey) == "" || os.Getenv(envCosignPwd) == "" {
return fmt.Errorf("please set both %s and %s environment variables", envCosignPrivateKey, envCosignPwd)
}
cosignPwd = os.Getenv(envCosignPwd)
privateKey = os.Getenv(envCosignPrivateKey)
return nil
}
func runCmd(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
cfg := staticupload.Config{
Bucket: awsBucket,
Region: awsRegion,
}
privateKey, err := os.ReadFile(privateKeyPath)
if err != nil {
return fmt.Errorf("reading private key: %w", err)
}
versionBytes, err := os.ReadFile(versionFilePath)
if err != nil {
return fmt.Errorf("reading version file: %w", err)
}
var versions configapi.AzureSEVSNPVersion
err = json.Unmarshal(versionBytes, &versions)
if err != nil {
var inputVersion attestationconfig.AzureSEVSNPVersion
if err = json.Unmarshal(versionBytes, &inputVersion); err != nil {
return fmt.Errorf("unmarshalling version file: %w", err)
}
sut, sutClose, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), privateKey)
latestAPIVersion, err := fetcher.New().FetchAzureSEVSNPVersionLatest(ctx)
if err != nil {
return fmt.Errorf("creating repo: %w", err)
return fmt.Errorf("fetching latest version: %w", err)
}
defer func() {
if err := sutClose(ctx); err != nil {
fmt.Printf("closing repo: %v\n", err)
}
}()
if err := sut.UploadAzureSEVSNP(ctx, versions, time.Now()); err != nil {
return fmt.Errorf("uploading version: %w", err)
isNewer, err := isInputNewerThanLatestAPI(inputVersion, latestAPIVersion.AzureSEVSNPVersion)
if err != nil {
return fmt.Errorf("comparing versions: %w", err)
}
if isNewer {
fmt.Printf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion)
sut, sutClose, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), []byte(privateKey))
defer func() {
if err := sutClose(ctx); err != nil {
fmt.Printf("closing repo: %v\n", err)
}
}()
if err != nil {
return fmt.Errorf("creating repo: %w", err)
}
if err := sut.UploadAzureSEVSNP(ctx, inputVersion, time.Now()); err != nil {
return fmt.Errorf("uploading version: %w", err)
}
cmd.Printf("Successfully uploaded new Azure SEV-SNP version: %+v\n", inputVersion)
} else {
cmd.Printf("Input version: %+v is not newer than latest API version: %+v\n", inputVersion, latestAPIVersion)
}
cmd.Printf("Successfully uploaded new Azure SEV-SNP version: %+v\n", versions)
return nil
}
// isInputNewerThanLatestAPI compares all version fields with the latest API version and returns true if any input field is newer.
func isInputNewerThanLatestAPI(input, latest attestationconfig.AzureSEVSNPVersion) (bool, error) {
inputValues := reflect.ValueOf(input)
latestValues := reflect.ValueOf(latest)
fields := reflect.TypeOf(input)
num := fields.NumField()
// validate that no input field is smaller than latest
for i := 0; i < num; i++ {
field := fields.Field(i)
inputValue := inputValues.Field(i).Uint()
latestValue := latestValues.Field(i).Uint()
if inputValue < latestValue {
return false, fmt.Errorf("input %s version: %d is older than latest API version: %d", field.Name, inputValue, latestValue)
} else if inputValue > latestValue {
return true, nil
}
}
// check if any input field is greater than latest
for i := 0; i < num; i++ {
inputValue := inputValues.Field(i).Uint()
latestValue := latestValues.Field(i).Uint()
if inputValue > latestValue {
return true, nil
}
}
return false, nil
}
func enforceRequiredFlags(cmd *cobra.Command, flags ...string) error {
for _, flag := range flags {
if err := cmd.MarkPersistentFlagRequired(flag); err != nil {

View file

@ -0,0 +1,75 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/stretchr/testify/assert"
)
var testCfg = attestationconfig.AzureSEVSNPVersion{
Microcode: 93,
TEE: 0,
SNP: 6,
Bootloader: 2,
}
func TestIsInputNewerThanLatestAPI(t *testing.T) {
testCases := map[string]struct {
latest attestationconfig.AzureSEVSNPVersion
input attestationconfig.AzureSEVSNPVersion
expect bool
errMsg string
}{
"input is older than latest": {
input: func(c attestationconfig.AzureSEVSNPVersion) attestationconfig.AzureSEVSNPVersion {
c.Microcode--
return c
}(testCfg),
latest: testCfg,
expect: false,
errMsg: "input Microcode version: 92 is older than latest API version: 93",
},
"input has greater and smaller version field than latest": {
input: func(c attestationconfig.AzureSEVSNPVersion) attestationconfig.AzureSEVSNPVersion {
c.Microcode++
c.Bootloader--
return c
}(testCfg),
latest: testCfg,
expect: false,
errMsg: "input Bootloader version: 1 is older than latest API version: 2",
},
"input is newer than latest": {
input: func(c attestationconfig.AzureSEVSNPVersion) attestationconfig.AzureSEVSNPVersion {
c.TEE++
return c
}(testCfg),
latest: testCfg,
expect: true,
},
"input is equal to latest": {
input: testCfg,
latest: testCfg,
expect: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
isNewer, err := isInputNewerThanLatestAPI(tc.input, tc.latest)
assert := assert.New(t)
if tc.errMsg != "" {
assert.EqualError(err, tc.errMsg)
} else {
assert.NoError(err)
assert.Equal(tc.expect, isNewer)
}
})
}
}