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

@ -49,7 +49,10 @@ jobs:
validate-snp-report:
needs: fetch-snp-report
name: "Validate SNP report"
name: "Validate SNP report and update Attestation API"
permissions:
id-token: write
contents: read
runs-on: ubuntu-22.04
env:
SHELL: /bin/bash
@ -73,4 +76,18 @@ jobs:
- name: Verify report
shell: bash
run: go run ./hack/azure-snp-report-verify/verify.go "$(cat ./maa-report.jwt)"
run: go run ./hack/azure-snp-report-verify/verify.go --report "$(cat ./maa-report.jwt)" --export-path azure-snp-version.json
- name: Login to AWS
uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef # v2.0.0
with:
role-to-assume: arn:aws:iam::795746500882:role/GitHubConstellationImagePipeline
aws-region: eu-central-1
- name: Update Attestation API
shell: bash
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
go run ./hack/configapi/main.go --version-file azure-snp-version.json

View File

@ -16,6 +16,7 @@
/e2e @katexochen
/hack/azure-jump-host @malt3
/hack/azure-snp-report-verify @derpsteb
/hack/configapi @elchead
/hack/bazel-deps-mirror @malt3
/hack/check-licenses.sh @thomasten
/hack/clidocgen @thomasten

View File

@ -299,13 +299,13 @@ func (f fakeConfigFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ conf
), nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet, _ versionsapi.Version) (configapi.AzureSEVSNPVersionGet, error) {
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
AzureSEVSNPVersion: testCfg,
}, nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context, _ versionsapi.Version) (configapi.AzureSEVSNPVersionGet, error) {
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
AzureSEVSNPVersion: testCfg,
}, nil

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)
}
})
}
}

View File

@ -9,7 +9,7 @@ go_library(
deps = [
"//internal/api/attestationconfig",
"//internal/api/fetcher",
"//internal/api/versions",
"//internal/constants",
"//internal/sigstore",
],
)
@ -20,8 +20,6 @@ go_test(
embed = [":fetcher"],
deps = [
"//internal/api/attestationconfig",
"//internal/api/versions",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)

View File

@ -16,15 +16,17 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/fetcher"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
)
const cosignPublicKey = constants.CosignPublicKeyReleases
// AttestationConfigAPIFetcher fetches config API resources without authentication.
type AttestationConfigAPIFetcher interface {
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionGet, version versionsapi.Version) (attestationconfig.AzureSEVSNPVersionGet, error)
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionGet) (attestationconfig.AzureSEVSNPVersionGet, error)
FetchAzureSEVSNPVersionList(ctx context.Context, attestation attestationconfig.AzureSEVSNPVersionList) (attestationconfig.AzureSEVSNPVersionList, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context, version versionsapi.Version) (attestationconfig.AzureSEVSNPVersionGet, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context) (attestationconfig.AzureSEVSNPVersionGet, error)
}
// Fetcher fetches AttestationCfg API resources without authentication.
@ -48,16 +50,12 @@ func (f *Fetcher) FetchAzureSEVSNPVersionList(ctx context.Context, attestation a
}
// FetchAzureSEVSNPVersion fetches the version information from the config API.
func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, attestation attestationconfig.AzureSEVSNPVersionGet, version versionsapi.Version) (attestationconfig.AzureSEVSNPVersionGet, error) {
cosignPublicKey, err := sigstore.CosignPublicKeyForVersion(version)
func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionGet) (attestationconfig.AzureSEVSNPVersionGet, error) {
urlString, err := azureVersion.URL()
if err != nil {
return attestationconfig.AzureSEVSNPVersionGet{}, fmt.Errorf("get public key for config: %w", err)
return azureVersion, err
}
urlString, err := attestation.URL()
if err != nil {
return attestationconfig.AzureSEVSNPVersionGet{}, err
}
fetchedVersion, err := fetcher.Fetch(ctx, f.HTTPClient, attestation)
fetchedVersion, err := fetcher.Fetch(ctx, f.HTTPClient, azureVersion)
if err != nil {
return fetchedVersion, fmt.Errorf("fetch version %s: %w", fetchedVersion.Version, err)
}
@ -71,7 +69,7 @@ func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, attestation attes
return fetchedVersion, fmt.Errorf("fetch version %s signature: %w", fetchedVersion.Version, err)
}
err = sigstore.CosignVerifier{}.VerifySignature(versionBytes, signature, cosignPublicKey)
err = sigstore.CosignVerifier{}.VerifySignature(versionBytes, signature, []byte(cosignPublicKey))
if err != nil {
return fetchedVersion, fmt.Errorf("verify version %s signature: %w", fetchedVersion.Version, err)
}
@ -79,14 +77,14 @@ func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, attestation attes
}
// FetchAzureSEVSNPVersionLatest returns the latest versions of the given type.
func (f *Fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context, version versionsapi.Version) (res attestationconfig.AzureSEVSNPVersionGet, err error) {
func (f *Fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res attestationconfig.AzureSEVSNPVersionGet, err error) {
var list attestationconfig.AzureSEVSNPVersionList
list, err = f.FetchAzureSEVSNPVersionList(ctx, list)
if err != nil {
return res, fmt.Errorf("fetching versions list: %w", err)
}
get := attestationconfig.AzureSEVSNPVersionGet{Version: list[0]} // get latest version (as sorted reversely alphanumerically)
get, err = f.FetchAzureSEVSNPVersion(ctx, get, version)
get, err = f.FetchAzureSEVSNPVersion(ctx, get)
if err != nil {
return res, fmt.Errorf("failed fetching version: %w", err)
}

View File

@ -15,9 +15,7 @@ import (
"testing"
configapi "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var testCfg = configapi.AzureSEVSNPVersionGet{
@ -36,7 +34,7 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
want configapi.AzureSEVSNPVersionGet
}{
"get version with valid signature": {
signature: []byte("MEUCIQDNn6wiSh9Nz9mtU9RvxvfkH3fNDFGeqopjTIRoBNkyrAIgSsKgdYNQXvPevaLWmmpnj/9WcgrltAQ+KfI+bQfklAo="),
signature: []byte("MEQCIBPEbYg89MIQuaGStLhKGLGMKvKFoYCaAniDLwoIwulqAiB+rj7KMaMOMGxmUsjI7KheCXSNM8NzN+tuDw6AywI75A=="), // signed with release key
want: testCfg,
},
"fail with invalid signature": {
@ -51,13 +49,10 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
signature: tc.signature,
},
}
require := require.New(t)
version, err := versionsapi.NewVersionFromShortPath("stream/debug/v9.9.9", versionsapi.VersionKindImage)
require.NoError(err)
fetcher := NewWithClient(client)
res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background())
assert := assert.New(t)
res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background(), version)
if tc.wantErr {
assert.Error(err)
} else {

View File

@ -53,7 +53,6 @@ go_test(
embed = [":config"],
deps = [
"//internal/api/attestationconfig",
"//internal/api/versions",
"//internal/attestation/measurements",
"//internal/cloud/cloudprovider",
"//internal/config/instancetypes",

View File

@ -13,7 +13,6 @@ import (
configapi "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
attestationconfigfetcher "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
@ -98,8 +97,8 @@ func (c AzureSEVSNP) EqualTo(old AttestationCfg) (bool, error) {
}
// FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them.
func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(fetcher attestationconfigfetcher.AttestationConfigAPIFetcher, version versionsapi.Version) error {
versions, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background(), version)
func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(fetcher attestationconfigfetcher.AttestationConfigAPIFetcher) error {
versions, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background())
if err != nil {
return err
}

View File

@ -32,7 +32,6 @@ import (
en_translations "github.com/go-playground/validator/v10/translations/en"
attestationconfigfetcher "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
@ -393,11 +392,7 @@ func New(fileHandler file.Handler, name string, fetcher attestationconfigfetcher
}
if azure := c.Attestation.AzureSEVSNP; azure != nil {
version, err := versionsapi.NewVersionFromShortPath(c.Image, versionsapi.VersionKindImage)
if err != nil {
return nil, err
}
if err := azure.FetchAndSetLatestVersionNumbers(fetcher, version); err != nil {
if err := azure.FetchAndSetLatestVersionNumbers(fetcher); err != nil {
return c, err
}
}

View File

@ -22,7 +22,6 @@ import (
"gopkg.in/yaml.v3"
configapi "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
@ -886,13 +885,13 @@ func (f fakeConfigFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ conf
), nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet, _ versionsapi.Version) (configapi.AzureSEVSNPVersionGet, error) {
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
AzureSEVSNPVersion: testCfg,
}, nil
}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context, _ versionsapi.Version) (configapi.AzureSEVSNPVersionGet, error) {
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionGet, error) {
return configapi.AzureSEVSNPVersionGet{
AzureSEVSNPVersion: testCfg,
}, nil