From 99a88c033cf048d9fc6ffdb56e3c7d922f817312 Mon Sep 17 00:00:00 2001 From: Adrian Stobbe Date: Mon, 5 Jun 2023 16:10:44 +0200 Subject: [PATCH] api: use new signature JSON format (#1872) * use new impl for client.UploadAzureSEVSNP * fix: fetcher must parse new signature format * version-file is not persistentflag * fix fetcher tests --- hack/configapi/cmd/root.go | 31 ++--- internal/api/attestationconfig/azure.go | 4 +- .../api/attestationconfig/client/BUILD.bazel | 1 - .../api/attestationconfig/client/client.go | 126 ++++++------------ .../api/attestationconfig/fetcher/fetcher.go | 47 ++----- .../attestationconfig/fetcher/fetcher_test.go | 9 +- internal/api/client/client.go | 2 +- 7 files changed, 77 insertions(+), 143 deletions(-) diff --git a/hack/configapi/cmd/root.go b/hack/configapi/cmd/root.go index d54f78a00..0ffcc21e5 100644 --- a/hack/configapi/cmd/root.go +++ b/hack/configapi/cmd/root.go @@ -14,8 +14,8 @@ import ( "time" "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" + attestationconfigapiclient "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client" + attestationconfigapifetcher "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher" "github.com/edgelesssys/constellation/v2/internal/logger" "go.uber.org/zap" @@ -35,6 +35,7 @@ const ( var ( versionFilePath string + force bool // Cosign credentials. cosignPwd string privateKey string @@ -55,8 +56,9 @@ func newRootCmd() *cobra.Command { PreRunE: envCheck, RunE: runCmd, } - rootCmd.PersistentFlags().StringVarP(&versionFilePath, "version-file", "f", "", "File path to the version json file.") - must(enforcePersistentRequiredFlags(rootCmd, "version-file")) + rootCmd.Flags().StringVarP(&versionFilePath, "version-file", "f", "", "File path to the version json file.") + rootCmd.Flags().BoolVar(&force, "force", false, "force to upload version regardless of comparison to latest API value.") + must(enforceRequiredFlags(rootCmd, "version-file")) rootCmd.AddCommand(newDeleteCmd()) return rootCmd } @@ -85,7 +87,7 @@ func runCmd(cmd *cobra.Command, _ []string) error { return fmt.Errorf("unmarshalling version file: %w", err) } - latestAPIVersion, err := fetcher.New().FetchAzureSEVSNPVersionLatest(ctx) + latestAPIVersion, err := attestationconfigapifetcher.New().FetchAzureSEVSNPVersionLatest(ctx) if err != nil { return fmt.Errorf("fetching latest version: %w", err) } @@ -94,9 +96,13 @@ func runCmd(cmd *cobra.Command, _ []string) error { if err != nil { return fmt.Errorf("comparing versions: %w", err) } - if isNewer { - cmd.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), false, log()) + if isNewer || force { + if force { + cmd.Println("Forcing upload of new version") + } else { + cmd.Printf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion) + } + sut, sutClose, err := attestationconfigapiclient.New(ctx, cfg, []byte(cosignPwd), []byte(privateKey), false, log()) defer func() { if err := sutClose(ctx); err != nil { cmd.Printf("closing repo: %v\n", err) @@ -153,15 +159,6 @@ func enforceRequiredFlags(cmd *cobra.Command, flags ...string) error { return nil } -func enforcePersistentRequiredFlags(cmd *cobra.Command, flags ...string) error { - for _, flag := range flags { - if err := cmd.MarkPersistentFlagRequired(flag); err != nil { - return err - } - } - return nil -} - func must(err error) { if err != nil { panic(err) diff --git a/internal/api/attestationconfig/azure.go b/internal/api/attestationconfig/azure.go index 6f50ccc64..16f6a1a09 100644 --- a/internal/api/attestationconfig/azure.go +++ b/internal/api/attestationconfig/azure.go @@ -42,7 +42,7 @@ type AzureSEVSNPVersionSignature struct { // JSONPath returns the path to the JSON file for the request to the config api. func (s AzureSEVSNPVersionSignature) JSONPath() string { - return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), s.Version, ".sig") + return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), s.Version+".sig") } // URL returns the URL for the request to the config api. @@ -53,7 +53,7 @@ func (s AzureSEVSNPVersionSignature) URL() (string, error) { // ValidateRequest validates the request. func (s AzureSEVSNPVersionSignature) ValidateRequest() error { if !strings.HasSuffix(s.Version, ".json") { - return fmt.Errorf("version has no .json suffix") + return fmt.Errorf("%s version has no .json suffix", s.Version) } return nil } diff --git a/internal/api/attestationconfig/client/BUILD.bazel b/internal/api/attestationconfig/client/BUILD.bazel index 4fe7615e1..c14bb3346 100644 --- a/internal/api/attestationconfig/client/BUILD.bazel +++ b/internal/api/attestationconfig/client/BUILD.bazel @@ -10,7 +10,6 @@ go_library( "//internal/api/attestationconfig", "//internal/api/attestationconfig/fetcher", "//internal/api/client", - "//internal/constants", "//internal/logger", "//internal/sigstore", "//internal/staticupload", diff --git a/internal/api/attestationconfig/client/client.go b/internal/api/attestationconfig/client/client.go index 46b68c725..1eefa75ca 100644 --- a/internal/api/attestationconfig/client/client.go +++ b/internal/api/attestationconfig/client/client.go @@ -15,7 +15,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher" apiclient "github.com/edgelesssys/constellation/v2/internal/api/client" - "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/staticupload" @@ -48,69 +47,30 @@ func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []b return repo, clientClose, nil } -func (a Client) uploadAzureSEVSNP(versions attestationconfig.AzureSEVSNPVersion, versionNames []string, date time.Time) (res []putCmd, err error) { - dateStr := date.Format("2006-01-02-15-04") + ".json" - - res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: versions}}) - - versionBytes, err := json.Marshal(versions) - if err != nil { - return res, err - } - signature, err := a.createSignature(versionBytes, dateStr) - if err != nil { - return res, err - } - res = append(res, putCmd{signature}) - newVersions := addVersion(versionNames, dateStr) - res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionList(newVersions)}) - return -} - // UploadAzureSEVSNP uploads the latest version numbers of the Azure SEVSNP. func (a Client) UploadAzureSEVSNP(ctx context.Context, version attestationconfig.AzureSEVSNPVersion, date time.Time) error { - variant := variant.AzureSEVSNP{} - - dateStr := date.Format("2006-01-02-15-04") + ".json" - err := apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: version}) + versions, err := a.List(ctx, variant.AzureSEVSNP{}) + if err != nil { + return fmt.Errorf("fetch version list: %w", err) + } + ops, err := a.uploadAzureSEVSNP(version, versions, date) if err != nil { return err } - - versionBytes, err := json.Marshal(version) - if err != nil { - return err - } - filePath := fmt.Sprintf("%s/%s/%s", constants.CDNAttestationConfigPrefixV1, variant.String(), dateStr) - err = a.createAndUploadSignature(ctx, versionBytes, filePath) - if err != nil { - return err - } - - return a.addVersionToList(ctx, variant, dateStr) + return executeAllCmds(ctx, a.s3Client, ops) } -func (a Client) createSignature(content []byte, dateStr string) (res attestationconfig.AzureSEVSNPVersionSignature, err error) { - signature, err := a.signer.Sign(content) +// DeleteAzureSEVSNPVersion deletes the given version (without .json suffix) from the API. +func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error { + versions, err := a.List(ctx, variant.AzureSEVSNP{}) if err != nil { - return res, fmt.Errorf("sign version file: %w", err) + return fmt.Errorf("fetch version list: %w", err) } - return attestationconfig.AzureSEVSNPVersionSignature{ - Signature: signature, - Version: dateStr, - }, nil -} - -// createAndUploadSignature signs the given content and uploads the signature to the given filePath with the .sig suffix. -func (a Client) createAndUploadSignature(ctx context.Context, content []byte, filePath string) error { - signature, err := a.createSignature(content, filePath) + ops, err := a.deleteAzureSEVSNPVersion(versions, versionStr) if err != nil { return err } - if err := apiclient.Update(ctx, a.s3Client, signature); err != nil { - return fmt.Errorf("upload signature: %w", err) - } - return nil + return executeAllCmds(ctx, a.s3Client, ops) } // List returns the list of versions for the given attestation type. @@ -125,15 +85,7 @@ func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string return nil, fmt.Errorf("unsupported attestation type: %s", attestation) } -// DeleteList empties the list of versions for the given attestation type. -func (a Client) DeleteList(ctx context.Context, attestation variant.Variant) error { - if attestation.Equal(variant.AzureSEVSNP{}) { - return apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList{}) - } - return fmt.Errorf("unsupported attestation type: %s", attestation) -} - -func (a Client) deleteAzureSEVSNPVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr string) (ops []crudOPNew, err error) { +func (a Client) deleteAzureSEVSNPVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr string) (ops []crudCmd, err error) { versionStr = versionStr + ".json" ops = append(ops, deleteCmd{ apiObject: attestationconfig.AzureSEVSNPVersionAPI{ @@ -157,33 +109,34 @@ func (a Client) deleteAzureSEVSNPVersion(versions attestationconfig.AzureSEVSNPV return ops, nil } -// DeleteAzureSEVSNPVersion deletes the given version (without .json suffix) from the API. -func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error { - versions, err := a.List(ctx, variant.AzureSEVSNP{}) +func (a Client) uploadAzureSEVSNP(versions attestationconfig.AzureSEVSNPVersion, versionNames []string, date time.Time) (res []crudCmd, err error) { + dateStr := date.Format("2006-01-02-15-04") + ".json" + + res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: versions}}) + + versionBytes, err := json.Marshal(versions) if err != nil { - return fmt.Errorf("fetch version list: %w", err) + return res, err } - ops, err := a.deleteAzureSEVSNPVersion(versions, versionStr) + signature, err := a.createSignature(versionBytes, dateStr) if err != nil { - return err + return res, err } - for _, op := range ops { - if err := op.Execute(ctx, a.s3Client); err != nil { - return fmt.Errorf("execute operation %+v: %w", op, err) - } - } - return nil + res = append(res, putCmd{signature}) + newVersions := addVersion(versionNames, dateStr) + res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionList(newVersions)}) + return } -func (a Client) addVersionToList(ctx context.Context, attestation variant.Variant, fname string) error { - versions, err := a.List(ctx, attestation) +func (a Client) createSignature(content []byte, dateStr string) (res attestationconfig.AzureSEVSNPVersionSignature, err error) { + signature, err := a.signer.Sign(content) if err != nil { - return err + return res, fmt.Errorf("sign version file: %w", err) } - versions = append(versions, fname) - versions = variant.RemoveDuplicate(versions) - sort.Sort(sort.Reverse(sort.StringSlice(versions))) - return apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList(versions)) + return attestationconfig.AzureSEVSNPVersionSignature{ + Signature: signature, + Version: dateStr, + }, nil } func removeVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr string) (removedVersions attestationconfig.AzureSEVSNPVersionList, err error) { @@ -200,6 +153,10 @@ func removeVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr return nil, fmt.Errorf("version %s not found in list %v", versionStr, versions) } +type crudCmd interface { + Execute(ctx context.Context, c *apiclient.Client) error +} + type deleteCmd struct { apiObject apiclient.APIObject } @@ -216,8 +173,13 @@ func (p putCmd) Execute(ctx context.Context, c *apiclient.Client) error { return apiclient.Update(ctx, c, p.apiObject) } -type crudOPNew interface { - Execute(ctx context.Context, c *apiclient.Client) error +func executeAllCmds(ctx context.Context, client *apiclient.Client, cmds []crudCmd) error { + for _, cmd := range cmds { + if err := cmd.Execute(ctx, client); err != nil { + return fmt.Errorf("execute operation %+v: %w", cmd, err) + } + } + return nil } func addVersion(versions []string, newVersion string) []string { diff --git a/internal/api/attestationconfig/fetcher/fetcher.go b/internal/api/attestationconfig/fetcher/fetcher.go index 417733543..3d26ec260 100644 --- a/internal/api/attestationconfig/fetcher/fetcher.go +++ b/internal/api/attestationconfig/fetcher/fetcher.go @@ -10,9 +10,6 @@ import ( "context" "encoding/json" "fmt" - "io" - "net/http" - "net/url" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig" "github.com/edgelesssys/constellation/v2/internal/api/fetcher" @@ -51,27 +48,25 @@ 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, azureVersion attestationconfig.AzureSEVSNPVersionAPI) (attestationconfig.AzureSEVSNPVersionAPI, error) { - urlString, err := azureVersion.URL() - if err != nil { - return azureVersion, err - } fetchedVersion, err := fetcher.Fetch(ctx, f.HTTPClient, azureVersion) if err != nil { return fetchedVersion, fmt.Errorf("fetch version %s: %w", fetchedVersion.Version, err) } versionBytes, err := json.Marshal(fetchedVersion) if err != nil { - return fetchedVersion, fmt.Errorf("marshal version for verify %s: %w", fetchedVersion.Version, err) + return fetchedVersion, fmt.Errorf("marshal version for verify %s: %w", azureVersion.Version, err) } - signature, err := fetchBytesFromRawURL(ctx, fmt.Sprintf("%s.sig", urlString), f.HTTPClient) + signature, err := fetcher.Fetch(ctx, f.HTTPClient, attestationconfig.AzureSEVSNPVersionSignature{ + Version: azureVersion.Version, + }) if err != nil { - return fetchedVersion, fmt.Errorf("fetch version %s signature: %w", fetchedVersion.Version, err) + return fetchedVersion, fmt.Errorf("fetch version %s signature: %w", azureVersion.Version, err) } - err = sigstore.CosignVerifier{}.VerifySignature(versionBytes, signature, []byte(cosignPublicKey)) + err = sigstore.CosignVerifier{}.VerifySignature(versionBytes, signature.Signature, []byte(cosignPublicKey)) if err != nil { - return fetchedVersion, fmt.Errorf("verify version %s signature: %w", fetchedVersion.Version, err) + return fetchedVersion, fmt.Errorf("verify version %s signature: %w", azureVersion.Version, err) } return fetchedVersion, nil } @@ -86,33 +81,7 @@ func (f *Fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res attest get := attestationconfig.AzureSEVSNPVersionAPI{Version: list[0]} // get latest version (as sorted reversely alphanumerically) get, err = f.FetchAzureSEVSNPVersion(ctx, get) if err != nil { - return res, fmt.Errorf("failed fetching version: %w", err) + return res, fmt.Errorf("fetching version: %w", err) } return get, nil } - -func fetchBytesFromRawURL(ctx context.Context, urlString string, client fetcher.HTTPClient) ([]byte, error) { - url, err := url.Parse(urlString) - if err != nil { - return nil, fmt.Errorf("parse version url %s: %w", urlString, err) - } - return getFromURL(ctx, client, url) -} - -// getFromURL fetches the content from the given URL and returns the content as a byte slice. -func getFromURL(ctx context.Context, client fetcher.HTTPClient, sourceURL *url.URL) ([]byte, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL.String(), http.NoBody) - if err != nil { - return nil, err - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("http status code: %d", resp.StatusCode) - } - return io.ReadAll(resp.Body) -} diff --git a/internal/api/attestationconfig/fetcher/fetcher_test.go b/internal/api/attestationconfig/fetcher/fetcher_test.go index 362dee832..f8f492482 100644 --- a/internal/api/attestationconfig/fetcher/fetcher_test.go +++ b/internal/api/attestationconfig/fetcher/fetcher_test.go @@ -93,7 +93,14 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err } else if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/2021-01-01-01-01.json.sig" { res := &http.Response{} - res.Body = io.NopCloser(bytes.NewReader(f.signature)) + obj := configapi.AzureSEVSNPVersionSignature{ + Signature: f.signature, + } + bt, err := json.Marshal(obj) + if err != nil { + return nil, err + } + res.Body = io.NopCloser(bytes.NewReader(bt)) res.StatusCode = http.StatusOK return res, nil diff --git a/internal/api/client/client.go b/internal/api/client/client.go index 77fd75271..65f3f9f48 100644 --- a/internal/api/client/client.go +++ b/internal/api/client/client.go @@ -184,7 +184,7 @@ type APIObject interface { JSONPath() string } -// Fetch fetches the given apiObject from the public Constellation CDN. +// Fetch fetches the given apiObject from the public Constellation API. func Fetch[T APIObject](ctx context.Context, c *Client, obj T) (T, error) { if err := obj.ValidateRequest(); err != nil { return *new(T), fmt.Errorf("validating request for %T: %w", obj, err)