mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-26 07:16:08 -05:00
api: add functions to transparently handle signatures upon API interaction (#2142)
This commit is contained in:
parent
002c3a9a32
commit
dac690656e
@ -80,6 +80,7 @@ go_library(
|
||||
"//internal/retry",
|
||||
"//internal/semver",
|
||||
"//internal/sigstore",
|
||||
"//internal/sigstore/keyselect",
|
||||
"//internal/versions",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"//verify/verifyproto",
|
||||
@ -162,6 +163,7 @@ go_test(
|
||||
"//internal/license",
|
||||
"//internal/logger",
|
||||
"//internal/semver",
|
||||
"//internal/sigstore",
|
||||
"//internal/versions",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"//verify/verifyproto",
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -69,11 +70,11 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error {
|
||||
cfm := &configFetchMeasurementsCmd{log: log, canFetchMeasurements: featureset.CanFetchMeasurements}
|
||||
|
||||
fetcher := attestationconfigapi.NewFetcherWithClient(http.DefaultClient)
|
||||
return cfm.configFetchMeasurements(cmd, sigstore.CosignVerifier{}, rekor, fileHandler, fetcher, http.DefaultClient)
|
||||
return cfm.configFetchMeasurements(cmd, sigstore.NewCosignVerifier, rekor, fileHandler, fetcher, http.DefaultClient)
|
||||
}
|
||||
|
||||
func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
||||
cmd *cobra.Command, cosign cosignVerifier, rekor rekorVerifier,
|
||||
cmd *cobra.Command, newCosignVerifier cosignVerifierConstructor, rekor rekorVerifier,
|
||||
fileHandler file.Handler, fetcher attestationconfigapi.Fetcher, client *http.Client,
|
||||
) error {
|
||||
flags, err := cfm.parseFetchMeasurementsFlags(cmd)
|
||||
@ -117,6 +118,15 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
||||
return err
|
||||
}
|
||||
|
||||
publicKey, err := keyselect.CosignPublicKeyForVersion(imageVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
cosign, err := newCosignVerifier(publicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating cosign verifier: %w", err)
|
||||
}
|
||||
|
||||
var fetchedMeasurements measurements.M
|
||||
var hash string
|
||||
if flags.insecure {
|
||||
@ -147,7 +157,7 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
||||
return fmt.Errorf("fetching and verifying measurements: %w", err)
|
||||
}
|
||||
cfm.log.Debugf("Fetched and verified measurements, hash is %s", hash)
|
||||
if err := sigstore.VerifyWithRekor(cmd.Context(), imageVersion, rekor, hash); err != nil {
|
||||
if err := sigstore.VerifyWithRekor(cmd.Context(), publicKey, rekor, hash); err != nil {
|
||||
cmd.PrintErrf("Ignoring Rekor related error: %v\n", err)
|
||||
cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!")
|
||||
}
|
||||
@ -244,6 +254,4 @@ type rekorVerifier interface {
|
||||
VerifyEntry(context.Context, string, string) error
|
||||
}
|
||||
|
||||
type cosignVerifier interface {
|
||||
VerifySignature(content, signature, publicKey []byte) error
|
||||
}
|
||||
type cosignVerifierConstructor func([]byte) (sigstore.Verifier, error)
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -102,12 +103,11 @@ func TestParseFetchMeasurementsFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateURLs(t *testing.T) {
|
||||
ver := versionsapi.Version{
|
||||
Ref: "foo",
|
||||
Stream: "nightly",
|
||||
Version: "v7.7.7",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
require := require.New(t)
|
||||
|
||||
ver, err := versionsapi.NewVersion("foo", "nightly", "v7.7.7", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
testCases := map[string]struct {
|
||||
conf *config.Config
|
||||
flags *fetchMeasurementsFlags
|
||||
@ -234,39 +234,43 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
||||
})
|
||||
|
||||
testCases := map[string]struct {
|
||||
cosign cosignVerifier
|
||||
cosign cosignVerifierConstructor
|
||||
rekor rekorVerifier
|
||||
insecureFlag bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
cosign: &stubCosignVerifier{},
|
||||
cosign: newStubCosignVerifier,
|
||||
rekor: singleUUIDVerifier(),
|
||||
},
|
||||
"success without cosign": {
|
||||
insecureFlag: true,
|
||||
cosign: &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
cosign: func(_ []byte) (sigstore.Verifier, error) {
|
||||
return &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
}, nil
|
||||
},
|
||||
rekor: singleUUIDVerifier(),
|
||||
},
|
||||
"failing search should not result in error": {
|
||||
cosign: &stubCosignVerifier{},
|
||||
cosign: newStubCosignVerifier,
|
||||
rekor: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{},
|
||||
SearchByHashError: assert.AnError,
|
||||
},
|
||||
},
|
||||
"failing verify should not result in error": {
|
||||
cosign: &stubCosignVerifier{},
|
||||
cosign: newStubCosignVerifier,
|
||||
rekor: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
|
||||
VerifyEntryError: assert.AnError,
|
||||
},
|
||||
},
|
||||
"signature verification failure": {
|
||||
cosign: &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
cosign: func(_ []byte) (sigstore.Verifier, error) {
|
||||
return &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
}, nil
|
||||
},
|
||||
rekor: singleUUIDVerifier(),
|
||||
wantErr: true,
|
||||
|
@ -307,7 +307,7 @@ func validateCLIandConstellationVersionAreEqual(cliVersion semver.Semver, imageV
|
||||
return fmt.Errorf("parsing image version: %w", err)
|
||||
}
|
||||
|
||||
semImage, err := semver.New(parsedImageVersion.Version)
|
||||
semImage, err := semver.New(parsedImageVersion.Version())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing image semantical version: %w", err)
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
|
||||
consemver "github.com/edgelesssys/constellation/v2/internal/semver"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect"
|
||||
"github.com/edgelesssys/constellation/v2/internal/versions"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
||||
"github.com/spf13/afero"
|
||||
@ -86,7 +87,6 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error {
|
||||
verListFetcher: versionfetcher,
|
||||
fileHandler: fileHandler,
|
||||
client: http.DefaultClient,
|
||||
cosign: sigstore.CosignVerifier{},
|
||||
rekor: rekor,
|
||||
flags: flags,
|
||||
cliVersion: constants.BinaryVersion(),
|
||||
@ -301,7 +301,7 @@ func sortedMapKeys[T any](a map[string]T) []string {
|
||||
func filterImageUpgrades(currentVersion string, newVersions []versionsapi.Version) []versionsapi.Version {
|
||||
newImages := []versionsapi.Version{}
|
||||
for i := range newVersions {
|
||||
if err := compatibility.IsValidUpgrade(currentVersion, newVersions[i].Version); err != nil {
|
||||
if err := compatibility.IsValidUpgrade(currentVersion, newVersions[i].Version()); err != nil {
|
||||
continue
|
||||
}
|
||||
newImages = append(newImages, newVersions[i])
|
||||
@ -338,7 +338,6 @@ type versionCollector struct {
|
||||
verListFetcher versionListFetcher
|
||||
fileHandler file.Handler
|
||||
client *http.Client
|
||||
cosign cosignVerifier
|
||||
rekor rekorVerifier
|
||||
flags upgradeCheckFlags
|
||||
versionsapi versionFetcher
|
||||
@ -346,11 +345,30 @@ type versionCollector struct {
|
||||
log debugLog
|
||||
}
|
||||
|
||||
func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, images []versionsapi.Version) (map[string]measurements.M, error) {
|
||||
func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, versions []versionsapi.Version) (map[string]measurements.M, error) {
|
||||
// get expected measurements for each image
|
||||
upgrades, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, v.cosign, v.rekor, csp, attestationVariant, images, v.log)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching measurements for compatible images: %w", err)
|
||||
upgrades := make(map[string]measurements.M)
|
||||
for _, version := range versions {
|
||||
v.log.Debugf("Fetching measurements for image: %s", version)
|
||||
shortPath := version.ShortPath()
|
||||
|
||||
publicKey, err := keyselect.CosignPublicKeyForVersion(version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
cosign, err := sigstore.NewCosignVerifier(publicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting public key: %w", err)
|
||||
}
|
||||
|
||||
measurements, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, cosign, v.rekor, csp, attestationVariant, version, v.log)
|
||||
if err != nil {
|
||||
if _, err := fmt.Fprintf(v.writer, "Skipping compatible image %q: %s\n", shortPath, err); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
upgrades[shortPath] = measurements
|
||||
}
|
||||
v.log.Debugf("Compatible image measurements are %v", upgrades)
|
||||
|
||||
@ -609,49 +627,41 @@ func getCurrentKubernetesVersion(ctx context.Context, checker upgradeChecker) (s
|
||||
}
|
||||
|
||||
// getCompatibleImageMeasurements retrieves the expected measurements for each image.
|
||||
func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, client *http.Client, cosign cosignVerifier, rekor rekorVerifier,
|
||||
csp cloudprovider.Provider, attestationVariant variant.Variant, versions []versionsapi.Version, log debugLog,
|
||||
) (map[string]measurements.M, error) {
|
||||
upgrades := make(map[string]measurements.M)
|
||||
for _, version := range versions {
|
||||
log.Debugf("Fetching measurements for image: %s", version)
|
||||
shortPath := version.ShortPath()
|
||||
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fetchedMeasurements measurements.M
|
||||
log.Debugf("Fetching for measurement url: %s", measurementsURL)
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, client, cosign,
|
||||
measurementsURL,
|
||||
signatureURL,
|
||||
version,
|
||||
csp,
|
||||
attestationVariant,
|
||||
)
|
||||
if err != nil {
|
||||
if _, err := fmt.Fprintf(writer, "Skipping compatible image %q: %s\n", shortPath, err); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err = sigstore.VerifyWithRekor(ctx, version, rekor, hash); err != nil {
|
||||
if _, err := fmt.Fprintf(writer, "Warning: Unable to verify '%s' in Rekor.\n", hash); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
if _, err := fmt.Fprintf(writer, "Make sure measurements are correct.\n"); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
upgrades[shortPath] = fetchedMeasurements
|
||||
|
||||
func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, client *http.Client, cosign sigstore.Verifier, rekor rekorVerifier,
|
||||
csp cloudprovider.Provider, attestationVariant variant.Variant, version versionsapi.Version, log debugLog,
|
||||
) (measurements.M, error) {
|
||||
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return upgrades, nil
|
||||
var fetchedMeasurements measurements.M
|
||||
log.Debugf("Fetching for measurement url: %s", measurementsURL)
|
||||
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, client, cosign,
|
||||
measurementsURL,
|
||||
signatureURL,
|
||||
version,
|
||||
csp,
|
||||
attestationVariant,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching measurements: %w", err)
|
||||
}
|
||||
|
||||
pubkey, err := keyselect.CosignPublicKeyForVersion(version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
|
||||
if err = sigstore.VerifyWithRekor(ctx, pubkey, rekor, hash); err != nil {
|
||||
if _, err := fmt.Fprintf(writer, "Warning: Unable to verify '%s' in Rekor.\nMake sure measurements are correct.\n", hash); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return fetchedMeasurements, nil
|
||||
}
|
||||
|
||||
type versionFetcher interface {
|
||||
|
@ -151,32 +151,23 @@ func TestGetCurrentImageVersion(t *testing.T) {
|
||||
|
||||
func TestGetCompatibleImageMeasurements(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
csp := cloudprovider.Azure
|
||||
attestationVariant := variant.AzureSEVSNP{}
|
||||
zero := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v0.0.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
one := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v1.0.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
images := []versionsapi.Version{zero, one}
|
||||
|
||||
versionZero, err := versionsapi.NewVersion("-", "stable", "v0.0.0", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
client := newTestClient(func(req *http.Request) *http.Response {
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json") {
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/image/measurements.json") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v0.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Body: io.NopCloser(strings.NewReader(`{"version": "v0.0.0","ref": "-","stream": "stable","list": [{"csp": "Azure","attestationVariant": "azure-sev-snp","measurements": {"0": {"expected": "0000000000000000000000000000000000000000000000000000000000000000","warnOnly": false}}}]}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json.sig") {
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/image/measurements.json.sig") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("MEQCIGRR7RaSMs892Ta06/Tz7LqPUxI05X4wQcP+nFFmZtmaAiBNl9X8mUKmUBfxg13LQBfmmpw6JwYQor5hOwM3NFVPAg==")),
|
||||
@ -184,21 +175,6 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json.sig") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("MEQCIFh8CVELp/Da2U2Jt404OXsUeDfqtrf3pqGRuvxnxhI8AiBTHF9tHEPwFedYG3Jgn2ELOxss+Ybc6135vEtClBrbpg==")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(strings.NewReader("Not found.")),
|
||||
@ -206,7 +182,7 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, &stubCosignVerifier{}, singleUUIDVerifier(), csp, attestationVariant, images, logger.NewTest(t))
|
||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, &stubCosignVerifier{}, singleUUIDVerifier(), csp, attestationVariant, versionZero, logger.NewTest(t))
|
||||
assert.NoError(err)
|
||||
|
||||
for _, measurement := range upgrades {
|
||||
@ -215,18 +191,13 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpgradeCheck(t *testing.T) {
|
||||
v2_3 := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v2.3.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
v2_5 := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v2.5.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
require := require.New(t)
|
||||
v2_3, err := versionsapi.NewVersion("-", "stable", "v2.3.0", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
v2_5, err := versionsapi.NewVersion("-", "stable", "v2.5.0", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
collector := stubVersionCollector{
|
||||
supportedServicesVersions: consemver.NewFromInt(2, 5, 0, ""),
|
||||
supportedImages: []versionsapi.Version{v2_3},
|
||||
@ -279,7 +250,6 @@ func TestUpgradeCheck(t *testing.T) {
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
||||
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), tc.csp)
|
||||
|
@ -6,7 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package cmd
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
)
|
||||
|
||||
// singleUUIDVerifier constructs a RekorVerifier that returns a single UUID and no errors,
|
||||
// and should work for most tests on the happy path.
|
||||
@ -39,6 +43,10 @@ type stubCosignVerifier struct {
|
||||
verifyError error
|
||||
}
|
||||
|
||||
func (v *stubCosignVerifier) VerifySignature(_, _, _ []byte) error {
|
||||
func newStubCosignVerifier(_ []byte) (sigstore.Verifier, error) {
|
||||
return &stubCosignVerifier{}, nil
|
||||
}
|
||||
|
||||
func (v *stubCosignVerifier) VerifySignature(_, _ []byte) error {
|
||||
return v.verifyError
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config,
|
||||
upgradeErrs := []error{}
|
||||
var upgradeErr *compatibility.InvalidUpgradeError
|
||||
|
||||
err = u.updateImage(&nodeVersion, imageReference, imageVersion.Version, force)
|
||||
err = u.updateImage(&nodeVersion, imageReference, imageVersion.Version(), force)
|
||||
switch {
|
||||
case errors.As(err, &upgradeErr):
|
||||
upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping image upgrades: %w", err))
|
||||
|
@ -67,9 +67,9 @@ func runEnvelopeMeasurements(cmd *cobra.Command, _ []string) error {
|
||||
}
|
||||
|
||||
enveloped := measurements.ImageMeasurementsV2{
|
||||
Ref: flags.version.Ref,
|
||||
Stream: flags.version.Stream,
|
||||
Version: flags.version.Version,
|
||||
Ref: flags.version.Ref(),
|
||||
Stream: flags.version.Stream(),
|
||||
Version: flags.version.Version(),
|
||||
List: []measurements.ImageMeasurementsV2Entry{
|
||||
{
|
||||
CSP: flags.csp,
|
||||
|
@ -43,9 +43,9 @@ func uploadImage(ctx context.Context, archiveC archivist, uploadC uploader, req
|
||||
}
|
||||
|
||||
imageInfo := versionsapi.ImageInfo{
|
||||
Ref: req.Version.Ref,
|
||||
Stream: req.Version.Stream,
|
||||
Version: req.Version.Version,
|
||||
Ref: req.Version.Ref(),
|
||||
Stream: req.Version.Stream(),
|
||||
Version: req.Version.Version(),
|
||||
List: imageReferences,
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,12 @@ func NewFetcher() Fetcher {
|
||||
|
||||
// NewFetcherWithClient returns a new fetcher with custom http client.
|
||||
func NewFetcherWithClient(client apifetcher.HTTPClient) Fetcher {
|
||||
return newFetcherWithClientAndVerifier(client, sigstore.CosignVerifier{})
|
||||
verifier, err := sigstore.NewCosignVerifier([]byte(cosignPublicKey))
|
||||
if err != nil {
|
||||
// This relies on an embedded public key. If this key can not be validated, there is no way to recover from this.
|
||||
panic(fmt.Errorf("creating cosign verifier: %w", err))
|
||||
}
|
||||
return newFetcherWithClientAndVerifier(client, verifier)
|
||||
}
|
||||
|
||||
func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifier sigstore.Verifier) Fetcher {
|
||||
@ -73,7 +78,7 @@ func (f *fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion Azur
|
||||
return fetchedVersion, fmt.Errorf("fetch version %s signature: %w", azureVersion.Version, err)
|
||||
}
|
||||
|
||||
err = f.verifier.VerifySignature(versionBytes, signature.Signature, []byte(cosignPublicKey))
|
||||
err = f.verifier.VerifySignature(versionBytes, signature.Signature)
|
||||
if err != nil {
|
||||
return fetchedVersion, fmt.Errorf("verify version %s signature: %w", azureVersion.Version, err)
|
||||
}
|
||||
|
@ -161,6 +161,6 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err
|
||||
|
||||
type dummyVerifier struct{}
|
||||
|
||||
func (s dummyVerifier) VerifySignature(_, _, _ []byte) error {
|
||||
func (s dummyVerifier) VerifySignature(_, _ []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ go_library(
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/logger",
|
||||
"//internal/sigstore",
|
||||
"//internal/staticupload",
|
||||
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
|
||||
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
|
||||
|
@ -33,12 +33,14 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/edgelesssys/constellation/v2/internal/staticupload"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -249,6 +251,50 @@ func Update(ctx context.Context, c *Client, obj APIObject) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignAndUpdate signs the given apiObject and updates the object and it's signature in the API.
|
||||
// This function should be used in favor of manually managing signatures.
|
||||
// The signing is specified as part of the signer argument.
|
||||
func SignAndUpdate(ctx context.Context, c *Client, obj APIObject, signer sigstore.Signer) error {
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling %T: %w", obj, err)
|
||||
}
|
||||
|
||||
dataSignature, err := signer.Sign(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign version file: %w", err)
|
||||
}
|
||||
|
||||
signature := signature{
|
||||
Signed: obj,
|
||||
Signature: dataSignature,
|
||||
}
|
||||
|
||||
if err := Update(ctx, c, obj); err != nil {
|
||||
return fmt.Errorf("updating %T: %w", obj, err)
|
||||
}
|
||||
|
||||
if err := Update(ctx, c, signature); err != nil {
|
||||
return fmt.Errorf("updating %T: %w", obj, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWithSignature deletes the given apiObject and it's signature from the public Constellation API.
|
||||
func DeleteWithSignature(ctx context.Context, c *Client, obj APIObject) error {
|
||||
if err := Delete(ctx, c, obj); err != nil {
|
||||
return fmt.Errorf("deleting %T: %w", obj, err)
|
||||
}
|
||||
|
||||
sig := signature{Signed: obj}
|
||||
if err := Delete(ctx, c, sig); err != nil {
|
||||
return fmt.Errorf("deleting %T: %w", sig, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes the given apiObject from the public Constellation API.
|
||||
func Delete(ctx context.Context, c *Client, obj APIObject) error {
|
||||
if err := obj.ValidateRequest(); err != nil {
|
||||
@ -303,3 +349,26 @@ type uploadClient interface {
|
||||
|
||||
// CloseFunc is a function that closes the client.
|
||||
type CloseFunc func(ctx context.Context) error
|
||||
|
||||
// signature wraps another APIObject and adds a signature to it.
|
||||
type signature struct {
|
||||
// Signed is the object that is signed.
|
||||
Signed APIObject
|
||||
// Signature is the signature of `Signed`.
|
||||
Signature []byte `json:"signature"`
|
||||
}
|
||||
|
||||
// JSONPath returns the path to the JSON file for the request to the config api.
|
||||
func (s signature) JSONPath() string {
|
||||
return path.Join(s.Signed.JSONPath() + ".sig")
|
||||
}
|
||||
|
||||
// ValidateRequest validates the request.
|
||||
func (s signature) ValidateRequest() error {
|
||||
return s.Signed.ValidateRequest()
|
||||
}
|
||||
|
||||
// Validate is a No-Op at the moment.
|
||||
func (s signature) Validate() error {
|
||||
return s.Signed.Validate()
|
||||
}
|
||||
|
@ -5,4 +5,5 @@ go_library(
|
||||
srcs = ["fetcher.go"],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/internal/api/fetcher",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = ["//internal/sigstore"],
|
||||
)
|
||||
|
@ -22,6 +22,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
)
|
||||
|
||||
// NewHTTPClient returns a new http client.
|
||||
@ -71,6 +73,32 @@ func Fetch[T apiObject](ctx context.Context, c HTTPClient, obj T) (T, error) {
|
||||
return newObj, nil
|
||||
}
|
||||
|
||||
// FetchAndVerify fetches the given apiObject, checks if it can fetch an accompanying signature and verifies if the signature matches the found object.
|
||||
// The public key used to verify the signature is embedded in the verifier argument.
|
||||
// FetchAndVerify uses a generic to return a new object of type T.
|
||||
// Otherwise the caller would have to cast the interface type to a concrete object, which could fail.
|
||||
func FetchAndVerify[T apiObject](ctx context.Context, c HTTPClient, obj T, cosignVerifier sigstore.Verifier) (T, error) {
|
||||
fetchedObj, err := Fetch(ctx, c, obj)
|
||||
if err != nil {
|
||||
return fetchedObj, fmt.Errorf("fetching object: %w", err)
|
||||
}
|
||||
marshalledObj, err := json.Marshal(fetchedObj)
|
||||
if err != nil {
|
||||
return fetchedObj, fmt.Errorf("marshalling object: %w", err)
|
||||
}
|
||||
|
||||
signature, err := Fetch(ctx, c, signature{Signed: fetchedObj})
|
||||
if err != nil {
|
||||
return fetchedObj, fmt.Errorf("fetching signature: %w", err)
|
||||
}
|
||||
|
||||
err = cosignVerifier.VerifySignature(marshalledObj, signature.Signature)
|
||||
if err != nil {
|
||||
return fetchedObj, fmt.Errorf("verifying signature: %w", err)
|
||||
}
|
||||
return fetchedObj, nil
|
||||
}
|
||||
|
||||
// NotFoundError is an error that is returned when a resource is not found.
|
||||
type NotFoundError struct {
|
||||
err error
|
||||
@ -94,3 +122,30 @@ type apiObject interface {
|
||||
Validate() error
|
||||
URL() (string, error)
|
||||
}
|
||||
|
||||
// signature wraps another APIObject and adds a signature to it.
|
||||
type signature struct {
|
||||
// Signed is the object that is signed.
|
||||
Signed apiObject `json:"-"`
|
||||
// Signature is the signature of `Signed`.
|
||||
Signature []byte `json:"signature"`
|
||||
}
|
||||
|
||||
// URL returns the URL for the request to the config api.
|
||||
func (s signature) URL() (string, error) {
|
||||
url, err := s.Signed.URL()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return url + ".sig", nil
|
||||
}
|
||||
|
||||
// ValidateRequest validates the request.
|
||||
func (s signature) ValidateRequest() error {
|
||||
return s.Signed.ValidateRequest()
|
||||
}
|
||||
|
||||
// Validate is a No-Op at the moment.
|
||||
func (s signature) Validate() error {
|
||||
return s.Signed.Validate()
|
||||
}
|
||||
|
@ -61,14 +61,9 @@ func runAdd(cmd *cobra.Command, _ []string) (retErr error) {
|
||||
}
|
||||
|
||||
log.Debugf("Creating version struct")
|
||||
ver := versionsapi.Version{
|
||||
Ref: flags.ref,
|
||||
Stream: flags.stream,
|
||||
Version: flags.version,
|
||||
Kind: flags.kind,
|
||||
}
|
||||
if err := ver.Validate(); err != nil {
|
||||
return err
|
||||
ver, err := versionsapi.NewVersion(flags.ref, flags.stream, flags.version, flags.kind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating version: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("Creating versions API client")
|
||||
@ -108,8 +103,8 @@ func ensureVersion(ctx context.Context, client *versionsapi.Client, kind version
|
||||
log *logger.Logger,
|
||||
) error {
|
||||
verListReq := versionsapi.List{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Ref: ver.Ref(),
|
||||
Stream: ver.Stream(),
|
||||
Granularity: gran,
|
||||
Base: ver.WithGranularity(gran),
|
||||
Kind: kind,
|
||||
@ -146,28 +141,28 @@ func ensureVersion(ctx context.Context, client *versionsapi.Client, kind version
|
||||
|
||||
func updateLatest(ctx context.Context, client *versionsapi.Client, kind versionsapi.VersionKind, ver versionsapi.Version, log *logger.Logger) error {
|
||||
latest := versionsapi.Latest{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Ref: ver.Ref(),
|
||||
Stream: ver.Stream(),
|
||||
Kind: kind,
|
||||
}
|
||||
latest, err := client.FetchVersionLatest(ctx, latest)
|
||||
var notFoundErr *apiclient.NotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
log.Debugf("Latest version for ref %q and stream %q not found", ver.Ref, ver.Stream)
|
||||
log.Debugf("Latest version for ref %q and stream %q not found", ver.Ref(), ver.Stream())
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("fetching latest version: %w", err)
|
||||
}
|
||||
|
||||
if latest.Version == ver.Version {
|
||||
if latest.Version == ver.Version() {
|
||||
log.Infof("Version %q is already latest version", ver)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("Setting %q as latest version", ver)
|
||||
latest = versionsapi.Latest{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Version: ver.Version,
|
||||
Ref: ver.Ref(),
|
||||
Stream: ver.Stream(),
|
||||
Version: ver.Version(),
|
||||
Kind: kind,
|
||||
}
|
||||
if err := client.UpdateVersionLatest(ctx, latest); err != nil {
|
||||
|
@ -91,7 +91,7 @@ func runList(cmd *cobra.Command, _ []string) error {
|
||||
log.Debugf("Printing versions as JSON")
|
||||
var vers []string
|
||||
for _, v := range patchVersions {
|
||||
vers = append(vers, v.Version)
|
||||
vers = append(vers, v.Version())
|
||||
}
|
||||
raw, err := json.Marshal(vers)
|
||||
if err != nil {
|
||||
|
@ -181,7 +181,7 @@ func deleteRef(ctx context.Context, clients rmImageClients, ref string, dryrun b
|
||||
|
||||
for _, ver := range vers {
|
||||
if err := deleteImage(ctx, clients, ver, dryrun, log); err != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("deleting images for version %s: %w", ver.Version, err))
|
||||
retErr = errors.Join(retErr, fmt.Errorf("deleting images for version %s: %w", ver.Version(), err))
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,9 +197,9 @@ func deleteImage(ctx context.Context, clients rmImageClients, ver versionsapi.Ve
|
||||
var retErr error
|
||||
|
||||
imageInfo := versionsapi.ImageInfo{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Version: ver.Version,
|
||||
Ref: ver.Ref(),
|
||||
Stream: ver.Stream(),
|
||||
Version: ver.Version(),
|
||||
}
|
||||
imageInfo, err := clients.version.FetchImageInfo(ctx, imageInfo)
|
||||
var notFound *apiclient.NotFoundError
|
||||
@ -291,14 +291,9 @@ func (f *rmFlags) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ver := versionsapi.Version{
|
||||
Ref: f.ref,
|
||||
Stream: f.stream,
|
||||
Version: f.version,
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
if err := ver.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid version: %w", err)
|
||||
ver, err := versionsapi.NewVersion(f.ref, f.stream, f.version, versionsapi.VersionKindImage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating version: %w", err)
|
||||
}
|
||||
f.ver = ver
|
||||
|
||||
|
@ -131,18 +131,18 @@ func (c *Client) DeleteRef(ctx context.Context, ref string) error {
|
||||
func (c *Client) DeleteVersion(ctx context.Context, ver Version) error {
|
||||
var retErr error
|
||||
|
||||
c.Client.Log.Debugf("Deleting version %s from minor version list", ver.Version)
|
||||
c.Client.Log.Debugf("Deleting version %s from minor version list", ver.version)
|
||||
possibleNewLatest, err := c.deleteVersionFromMinorVersionList(ctx, ver)
|
||||
if err != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("removing from minor version list: %w", err))
|
||||
}
|
||||
|
||||
c.Client.Log.Debugf("Checking latest version for %s", ver.Version)
|
||||
c.Client.Log.Debugf("Checking latest version for %s", ver.version)
|
||||
if err := c.deleteVersionFromLatest(ctx, ver, possibleNewLatest); err != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("updating latest version: %w", err))
|
||||
}
|
||||
|
||||
c.Client.Log.Debugf("Deleting artifact path %s for %s", ver.ArtifactPath(APIV1), ver.Version)
|
||||
c.Client.Log.Debugf("Deleting artifact path %s for %s", ver.ArtifactPath(APIV1), ver.version)
|
||||
if err := c.Client.DeletePath(ctx, ver.ArtifactPath(APIV1)); err != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("deleting artifact path: %w", err))
|
||||
}
|
||||
@ -153,32 +153,32 @@ func (c *Client) DeleteVersion(ctx context.Context, ver Version) error {
|
||||
func (c *Client) deleteVersionFromMinorVersionList(ctx context.Context, ver Version,
|
||||
) (*Latest, error) {
|
||||
minorList := List{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Ref: ver.ref,
|
||||
Stream: ver.stream,
|
||||
Granularity: GranularityMinor,
|
||||
Base: ver.WithGranularity(GranularityMinor),
|
||||
Kind: VersionKindImage,
|
||||
}
|
||||
c.Client.Log.Debugf("Fetching minor version list for version %s", ver.Version)
|
||||
c.Client.Log.Debugf("Fetching minor version list for version %s", ver.version)
|
||||
minorList, err := c.FetchVersionList(ctx, minorList)
|
||||
var notFoundErr *apiclient.NotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
c.Client.Log.Warnf("Minor version list for version %s not found", ver.Version)
|
||||
c.Client.Log.Warnf("Minor version list for version %s not found", ver.version)
|
||||
c.Client.Log.Warnf("Skipping update of minor version list")
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("fetching minor version list for version %s: %w", ver.Version, err)
|
||||
return nil, fmt.Errorf("fetching minor version list for version %s: %w", ver.version, err)
|
||||
}
|
||||
|
||||
if !minorList.Contains(ver.Version) {
|
||||
c.Client.Log.Warnf("Version %s is not in minor version list %s", ver.Version, minorList.JSONPath())
|
||||
if !minorList.Contains(ver.version) {
|
||||
c.Client.Log.Warnf("Version %s is not in minor version list %s", ver.version, minorList.JSONPath())
|
||||
c.Client.Log.Warnf("Skipping update of minor version list")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
semver.Sort(minorList.Versions)
|
||||
for i, v := range minorList.Versions {
|
||||
if v == ver.Version {
|
||||
if v == ver.version {
|
||||
minorList.Versions = append(minorList.Versions[:i], minorList.Versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
@ -187,8 +187,8 @@ func (c *Client) deleteVersionFromMinorVersionList(ctx context.Context, ver Vers
|
||||
var latest *Latest
|
||||
if len(minorList.Versions) != 0 {
|
||||
latest = &Latest{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Ref: ver.ref,
|
||||
Stream: ver.stream,
|
||||
Kind: VersionKindImage,
|
||||
Version: minorList.Versions[len(minorList.Versions)-1],
|
||||
}
|
||||
@ -205,15 +205,15 @@ func (c *Client) deleteVersionFromMinorVersionList(ctx context.Context, ver Vers
|
||||
return latest, fmt.Errorf("updating minor version list %s: %w", minorList.JSONPath(), err)
|
||||
}
|
||||
|
||||
c.Client.Log.Debugf("Removed version %s from minor version list %s", ver.Version, minorList.JSONPath())
|
||||
c.Client.Log.Debugf("Removed version %s from minor version list %s", ver.version, minorList.JSONPath())
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func (c *Client) deleteVersionFromLatest(ctx context.Context, ver Version, possibleNewLatest *Latest,
|
||||
) error {
|
||||
latest := Latest{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Ref: ver.ref,
|
||||
Stream: ver.stream,
|
||||
Kind: VersionKindImage,
|
||||
}
|
||||
c.Client.Log.Debugf("Fetching latest version from %s", latest.JSONPath())
|
||||
@ -226,8 +226,8 @@ func (c *Client) deleteVersionFromLatest(ctx context.Context, ver Version, possi
|
||||
return fmt.Errorf("fetching latest version: %w", err)
|
||||
}
|
||||
|
||||
if latest.Version != ver.Version {
|
||||
c.Client.Log.Debugf("Latest version is %s, not the deleted version %s", latest.Version, ver.Version)
|
||||
if latest.Version != ver.version {
|
||||
c.Client.Log.Debugf("Latest version is %s, not the deleted version %s", latest.Version, ver.version)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -163,10 +163,10 @@ func (l List) StructuredVersions() []Version {
|
||||
versions := make([]Version, len(l.Versions))
|
||||
for i, v := range l.Versions {
|
||||
versions[i] = Version{
|
||||
Ref: l.Ref,
|
||||
Stream: l.Stream,
|
||||
Version: v,
|
||||
Kind: l.Kind,
|
||||
ref: l.Ref,
|
||||
stream: l.Stream,
|
||||
version: v,
|
||||
kind: l.Kind,
|
||||
}
|
||||
}
|
||||
return versions
|
||||
|
@ -344,10 +344,10 @@ func TestListStructuredVersions(t *testing.T) {
|
||||
|
||||
verStrs := make([]string, len(versions))
|
||||
for i, v := range versions {
|
||||
assert.Equal(list.Ref, v.Ref)
|
||||
assert.Equal(list.Stream, v.Stream)
|
||||
assert.Equal(list.Kind, v.Kind)
|
||||
verStrs[i] = v.Version
|
||||
assert.Equal(list.Ref, v.Ref())
|
||||
assert.Equal(list.Stream, v.Stream())
|
||||
assert.Equal(list.Kind, v.Kind())
|
||||
verStrs[i] = v.version
|
||||
}
|
||||
|
||||
assert.ElementsMatch(list.Versions, verStrs)
|
||||
|
@ -28,11 +28,30 @@ const ReleaseRef = "-"
|
||||
// Notice that version is a meta object to the versions API and there isn't an
|
||||
// actual corresponding object in the S3 bucket.
|
||||
// Therefore, the version object doesn't have a URL or JSON path.
|
||||
//
|
||||
// Versions fields are private so the type can be used in other packages by
|
||||
// defining private interfaces.
|
||||
type Version struct {
|
||||
Ref string
|
||||
Stream string
|
||||
Version string
|
||||
Kind VersionKind
|
||||
ref string
|
||||
stream string
|
||||
version string
|
||||
kind VersionKind
|
||||
}
|
||||
|
||||
// NewVersion creates a new Version object and validates it.
|
||||
func NewVersion(ref, stream, version string, kind VersionKind) (Version, error) {
|
||||
ver := Version{
|
||||
ref: ref,
|
||||
stream: stream,
|
||||
version: version,
|
||||
kind: kind,
|
||||
}
|
||||
|
||||
if err := ver.Validate(); err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// NewVersionFromShortPath creates a new Version from a version short path.
|
||||
@ -43,10 +62,10 @@ func NewVersionFromShortPath(shortPath string, kind VersionKind) (Version, error
|
||||
}
|
||||
|
||||
ver := Version{
|
||||
Ref: ref,
|
||||
Stream: stream,
|
||||
Version: version,
|
||||
Kind: kind,
|
||||
ref: ref,
|
||||
stream: stream,
|
||||
version: version,
|
||||
kind: kind,
|
||||
}
|
||||
|
||||
if err := ver.Validate(); err != nil {
|
||||
@ -56,27 +75,47 @@ func NewVersionFromShortPath(shortPath string, kind VersionKind) (Version, error
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// Ref returns the ref of the version.
|
||||
func (v Version) Ref() string {
|
||||
return v.ref
|
||||
}
|
||||
|
||||
// Stream returns the stream of the version.
|
||||
func (v Version) Stream() string {
|
||||
return v.stream
|
||||
}
|
||||
|
||||
// Version returns the version string of the version.
|
||||
func (v Version) Version() string {
|
||||
return v.version
|
||||
}
|
||||
|
||||
// Kind returns the kind of the version.
|
||||
func (v Version) Kind() VersionKind {
|
||||
return v.kind
|
||||
}
|
||||
|
||||
// ShortPath returns the short path of the version.
|
||||
func (v Version) ShortPath() string {
|
||||
return shortPath(v.Ref, v.Stream, v.Version)
|
||||
return shortPath(v.ref, v.stream, v.version)
|
||||
}
|
||||
|
||||
// Validate validates the version.
|
||||
func (v Version) Validate() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(v.Ref); err != nil {
|
||||
if err := ValidateRef(v.ref); err != nil {
|
||||
retErr = errors.Join(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(v.Ref, v.Stream); err != nil {
|
||||
if err := ValidateStream(v.ref, v.stream); err != nil {
|
||||
retErr = errors.Join(retErr, err)
|
||||
}
|
||||
if !semver.IsValid(v.Version) {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("version %q is not a valid semantic version", v.Version))
|
||||
if !semver.IsValid(v.version) {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("version %q is not a valid semantic version", v.version))
|
||||
}
|
||||
if semver.Canonical(v.Version) != v.Version {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("version %q is not a canonical semantic version", v.Version))
|
||||
if semver.Canonical(v.version) != v.version {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("version %q is not a canonical semantic version", v.version))
|
||||
}
|
||||
if v.Kind == VersionKindUnknown {
|
||||
if v.kind == VersionKindUnknown {
|
||||
retErr = errors.Join(retErr, errors.New("version kind is unknown"))
|
||||
}
|
||||
return retErr
|
||||
@ -84,29 +123,29 @@ func (v Version) Validate() error {
|
||||
|
||||
// Equal returns true if the versions are equal.
|
||||
func (v Version) Equal(other Version) bool {
|
||||
return v.Ref == other.Ref &&
|
||||
v.Stream == other.Stream &&
|
||||
v.Version == other.Version &&
|
||||
v.Kind == other.Kind
|
||||
return v.ref == other.ref &&
|
||||
v.stream == other.stream &&
|
||||
v.version == other.version &&
|
||||
v.kind == other.kind
|
||||
}
|
||||
|
||||
// Major returns the major version corresponding to the version.
|
||||
// For example, if the version is "v1.2.3", the major version is "v1".
|
||||
func (v Version) Major() string {
|
||||
return semver.Major(v.Version)
|
||||
return semver.Major(v.version)
|
||||
}
|
||||
|
||||
// MajorMinor returns the major and minor version corresponding to the version.
|
||||
// For example, if the version is "v1.2.3", the major and minor version is "v1.2".
|
||||
func (v Version) MajorMinor() string {
|
||||
return semver.MajorMinor(v.Version)
|
||||
return semver.MajorMinor(v.version)
|
||||
}
|
||||
|
||||
// WithGranularity returns the version with the given granularity.
|
||||
//
|
||||
// For example, if the version is "v1.2.3" and the granularity is GranularityMajor,
|
||||
// the returned version is "v1".
|
||||
// This is a helper function for Major() and MajorMinor() and v.Version.
|
||||
// This is a helper function for Major() and MajorMinor() and v.version.
|
||||
// In case of an unknown granularity, an empty string is returned.
|
||||
func (v Version) WithGranularity(gran Granularity) string {
|
||||
switch gran {
|
||||
@ -115,7 +154,7 @@ func (v Version) WithGranularity(gran Granularity) string {
|
||||
case GranularityMinor:
|
||||
return v.MajorMinor()
|
||||
case GranularityPatch:
|
||||
return v.Version
|
||||
return v.version
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@ -144,11 +183,11 @@ func (v Version) ListPath(gran Granularity) string {
|
||||
}
|
||||
return path.Join(
|
||||
constants.CDNAPIPrefix,
|
||||
"ref", v.Ref,
|
||||
"stream", v.Stream,
|
||||
"ref", v.ref,
|
||||
"stream", v.stream,
|
||||
"versions",
|
||||
gran.String(), v.WithGranularity(gran),
|
||||
v.Kind.String()+".json",
|
||||
v.kind.String()+".json",
|
||||
)
|
||||
}
|
||||
|
||||
@ -164,9 +203,9 @@ func (v Version) ArtifactPath(apiVer apiVersion) string {
|
||||
return path.Join(
|
||||
constants.CDNAPIBase,
|
||||
apiVer.String(),
|
||||
"ref", v.Ref,
|
||||
"stream", v.Stream,
|
||||
v.Version,
|
||||
"ref", v.ref,
|
||||
"stream", v.stream,
|
||||
v.version,
|
||||
)
|
||||
}
|
||||
|
||||
@ -336,8 +375,8 @@ func ValidateStream(ref, stream string) error {
|
||||
|
||||
// MeasurementURL builds the measurement and signature URLs for the given version.
|
||||
func MeasurementURL(version Version) (measurementURL, signatureURL *url.URL, err error) {
|
||||
if version.Kind != VersionKindImage {
|
||||
return &url.URL{}, &url.URL{}, fmt.Errorf("kind %q is not supported", version.Kind)
|
||||
if version.kind != VersionKindImage {
|
||||
return &url.URL{}, &url.URL{}, fmt.Errorf("kind %q is not supported", version.kind)
|
||||
}
|
||||
|
||||
measurementPath, err := url.JoinPath(version.ArtifactsURL(APIV2), "image", constants.CDNMeasurementsFile)
|
||||
|
@ -27,40 +27,40 @@ func TestNewVersionFromShortPath(t *testing.T) {
|
||||
path: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
wantVer: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
},
|
||||
"release debug image": {
|
||||
path: "stream/debug/v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
wantVer: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
},
|
||||
"stable release cli": {
|
||||
path: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
wantVer: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
},
|
||||
"release debug cli": {
|
||||
path: "stream/debug/v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
wantVer: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
},
|
||||
"unknown kind": {
|
||||
@ -102,55 +102,55 @@ func TestVersionShortPath(t *testing.T) {
|
||||
}{
|
||||
"stable release image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
want: "v9.9.9",
|
||||
},
|
||||
"release debug image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
want: "stream/debug/v9.9.9",
|
||||
},
|
||||
"branch image": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: "foo",
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
want: "ref/foo/stream/debug/v9.9.9",
|
||||
},
|
||||
"stable release cli": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
want: "v9.9.9",
|
||||
},
|
||||
"release debug cli": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
want: "stream/debug/v9.9.9",
|
||||
},
|
||||
"branch cli": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: "foo",
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
want: "ref/foo/stream/debug/v9.9.9",
|
||||
},
|
||||
@ -173,71 +173,71 @@ func TestVersionValidate(t *testing.T) {
|
||||
}{
|
||||
"valid image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
},
|
||||
"invalid ref image": {
|
||||
ver: Version{
|
||||
Ref: "foo/bar",
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: "foo/bar",
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "foo/bar",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "foo/bar",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid version image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9/foo",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9/foo",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"valid cli": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
},
|
||||
"invalid ref cli": {
|
||||
ver: Version{
|
||||
Ref: "foo/bar",
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: "foo/bar",
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream cli": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "foo/bar",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "foo/bar",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid version cli": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9/foo",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9/foo",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
@ -268,7 +268,7 @@ func TestVersionMajor(t *testing.T) {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ver := Version{Version: version}
|
||||
ver := Version{version: version}
|
||||
assert.Equal(major, ver.Major())
|
||||
})
|
||||
}
|
||||
@ -285,7 +285,7 @@ func TestVersionMajorMinor(t *testing.T) {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ver := Version{Version: version}
|
||||
ver := Version{version: version}
|
||||
assert.Equal(major, ver.MajorMinor())
|
||||
})
|
||||
}
|
||||
@ -333,7 +333,7 @@ func TestVersionWithGranularity(t *testing.T) {
|
||||
t.Run(tc.ver, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ver := Version{Version: tc.ver}
|
||||
ver := Version{version: tc.ver}
|
||||
assert.Equal(tc.want, ver.WithGranularity(tc.gran))
|
||||
})
|
||||
}
|
||||
@ -348,10 +348,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
}{
|
||||
"release image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMajor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/versions/major/v9/image.json",
|
||||
@ -359,10 +359,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
},
|
||||
"release with minor image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMinor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/versions/minor/v9.9/image.json",
|
||||
@ -370,10 +370,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
},
|
||||
"release with patch image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityPatch,
|
||||
wantPath: "",
|
||||
@ -381,10 +381,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
},
|
||||
"release with unknown image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityUnknown,
|
||||
wantPath: "",
|
||||
@ -392,10 +392,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
},
|
||||
"release debug stream image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMajor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/versions/major/v9/image.json",
|
||||
@ -403,10 +403,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
},
|
||||
"release debug stream with minor image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMinor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/versions/minor/v9.9/image.json",
|
||||
@ -414,10 +414,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
},
|
||||
"branch ref image": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: "foo",
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMajor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/foo/stream/debug/versions/major/v9/image.json",
|
||||
@ -425,10 +425,10 @@ func TestVersionListPathURL(t *testing.T) {
|
||||
},
|
||||
"branch ref with minor image": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: "foo",
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMinor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/foo/stream/debug/versions/minor/v9.9/image.json",
|
||||
@ -463,10 +463,10 @@ func TestVersionArtifactURL(t *testing.T) {
|
||||
}{
|
||||
"nightly-feature": {
|
||||
ver: Version{
|
||||
Ref: "feat-some-feature",
|
||||
Stream: "nightly",
|
||||
Version: "v2.6.0-pre.0.20230217095603-193dd48ca19f",
|
||||
Kind: VersionKindImage,
|
||||
ref: "feat-some-feature",
|
||||
stream: "nightly",
|
||||
version: "v2.6.0-pre.0.20230217095603-193dd48ca19f",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
wantMeasurementURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefixV2 + "/ref/feat-some-feature/stream/nightly/v2.6.0-pre.0.20230217095603-193dd48ca19f/image/measurements.json",
|
||||
@ -474,7 +474,7 @@ func TestVersionArtifactURL(t *testing.T) {
|
||||
},
|
||||
"fail for wrong kind": {
|
||||
ver: Version{
|
||||
Kind: VersionKindCLI,
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
@ -503,55 +503,55 @@ func TestVersionArtifactPathURL(t *testing.T) {
|
||||
}{
|
||||
"release image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/v9.9.9",
|
||||
},
|
||||
"release debug stream image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/v9.9.9",
|
||||
},
|
||||
"branch ref image": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
ref: "foo",
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/foo/stream/debug/v9.9.9",
|
||||
},
|
||||
"release cli": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/v9.9.9",
|
||||
},
|
||||
"release debug stream cli": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/v9.9.9",
|
||||
},
|
||||
"branch ref cli": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindCLI,
|
||||
ref: "foo",
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
kind: VersionKindCLI,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/foo/stream/debug/v9.9.9",
|
||||
},
|
||||
|
@ -16,7 +16,6 @@ go_library(
|
||||
"//internal/api/versionsapi",
|
||||
"//internal/attestation/variant",
|
||||
"//internal/cloud/cloudprovider",
|
||||
"//internal/sigstore",
|
||||
"@com_github_google_go_tpm//tpmutil",
|
||||
"@com_github_siderolabs_talos_pkg_machinery//config/encoder",
|
||||
"@in_gopkg_yaml_v3//:yaml_v3",
|
||||
|
@ -12,6 +12,7 @@ go_library(
|
||||
"//internal/attestation/variant",
|
||||
"//internal/cloud/cloudprovider",
|
||||
"//internal/sigstore",
|
||||
"//internal/sigstore/keyselect",
|
||||
"@org_golang_x_tools//go/ast/astutil",
|
||||
],
|
||||
)
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect"
|
||||
"golang.org/x/tools/go/ast/astutil"
|
||||
)
|
||||
|
||||
@ -122,10 +123,20 @@ func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, provider c
|
||||
panic(err)
|
||||
}
|
||||
|
||||
publicKey, err := keyselect.CosignPublicKeyForVersion(imageVersion)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("getting public key: %w", err))
|
||||
}
|
||||
|
||||
cosignVerifier, err := sigstore.NewCosignVerifier(publicKey)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("creating cosign verifier: %w", err))
|
||||
}
|
||||
|
||||
log.Println("Fetching measurements from", measurementsURL, "and signature from", signatureURL)
|
||||
var fetchedMeasurements measurements.M
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, http.DefaultClient, sigstore.CosignVerifier{},
|
||||
ctx, http.DefaultClient, cosignVerifier,
|
||||
measurementsURL,
|
||||
signatureURL,
|
||||
imageVersion,
|
||||
@ -135,7 +146,7 @@ func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, provider c
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := sigstore.VerifyWithRekor(ctx, imageVersion, verifier, hash); err != nil {
|
||||
if err := sigstore.VerifyWithRekor(ctx, publicKey, verifier, hash); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fetchedMeasurements
|
||||
|
@ -29,7 +29,6 @@ import (
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/google/go-tpm/tpmutil"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -137,14 +136,10 @@ func (m *M) FetchAndVerify(
|
||||
measurementsURL, signatureURL *url.URL,
|
||||
version versionsapi.Version, csp cloudprovider.Provider, attestationVariant variant.Variant,
|
||||
) (string, error) {
|
||||
publicKey, err := sigstore.CosignPublicKeyForVersion(version)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
return m.fetchAndVerify(
|
||||
ctx, client, verifier,
|
||||
measurementsURL, signatureURL,
|
||||
publicKey, version, csp, attestationVariant,
|
||||
version, csp, attestationVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@ -153,7 +148,7 @@ func (m *M) FetchAndVerify(
|
||||
// The hash of the fetched measurements is returned.
|
||||
func (m *M) fetchAndVerify(
|
||||
ctx context.Context, client *http.Client, verifier cosignVerifier,
|
||||
measurementsURL, signatureURL *url.URL, publicKey []byte,
|
||||
measurementsURL, signatureURL *url.URL,
|
||||
version versionsapi.Version, csp cloudprovider.Provider, attestationVariant variant.Variant,
|
||||
) (string, error) {
|
||||
measurementsRaw, err := getFromURL(ctx, client, measurementsURL)
|
||||
@ -164,7 +159,7 @@ func (m *M) fetchAndVerify(
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch signature: %w", err)
|
||||
}
|
||||
if err := verifier.VerifySignature(measurementsRaw, signature, publicKey); err != nil {
|
||||
if err := verifier.VerifySignature(measurementsRaw, signature); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@ -308,11 +303,9 @@ func (m *M) fromImageMeasurementsV2(
|
||||
measurements ImageMeasurementsV2, wantVersion versionsapi.Version,
|
||||
csp cloudprovider.Provider, attestationVariant variant.Variant,
|
||||
) error {
|
||||
gotVersion := versionsapi.Version{
|
||||
Ref: measurements.Ref,
|
||||
Stream: measurements.Stream,
|
||||
Version: measurements.Version,
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
gotVersion, err := versionsapi.NewVersion(measurements.Ref, measurements.Stream, measurements.Version, versionsapi.VersionKindImage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid metadata version: %w", err)
|
||||
}
|
||||
if !wantVersion.Equal(gotVersion) {
|
||||
return fmt.Errorf("invalid measurement metadata: version mismatch: expected %s, got %s", wantVersion.ShortPath(), gotVersion.ShortPath())
|
||||
@ -553,5 +546,5 @@ func getFromURL(ctx context.Context, client *http.Client, sourceURL *url.URL) ([
|
||||
}
|
||||
|
||||
type cosignVerifier interface {
|
||||
VerifySignature(content, signature, publicKey []byte) error
|
||||
VerifySignature(content, signature []byte) error
|
||||
}
|
||||
|
@ -332,6 +332,11 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
// -----END ENCRYPTED COSIGN PRIVATE KEY-----
|
||||
cosignPublicKey := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
|
||||
|
||||
v1Test, err := versionsapi.NewVersion("-", "stable", "v1.0.0-test", versionsapi.VersionKindImage)
|
||||
require.NoError(t, err)
|
||||
v1AnotherImage, err := versionsapi.NewVersion("-", "stable", "v1.0.0-another-image", versionsapi.VersionKindImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := map[string]struct {
|
||||
measurements string
|
||||
csp cloudprovider.Provider
|
||||
@ -347,7 +352,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
"json measurements": {
|
||||
measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
|
||||
csp: cloudprovider.Unknown,
|
||||
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
|
||||
imageVersion: v1Test,
|
||||
measurementsStatus: http.StatusOK,
|
||||
signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
|
||||
signatureStatus: http.StatusOK,
|
||||
@ -359,7 +364,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
"404 measurements": {
|
||||
measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
|
||||
csp: cloudprovider.Unknown,
|
||||
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
|
||||
imageVersion: v1Test,
|
||||
measurementsStatus: http.StatusNotFound,
|
||||
signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
|
||||
signatureStatus: http.StatusOK,
|
||||
@ -368,7 +373,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
"404 signature": {
|
||||
measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
|
||||
csp: cloudprovider.Unknown,
|
||||
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
|
||||
imageVersion: v1Test,
|
||||
measurementsStatus: http.StatusOK,
|
||||
signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
|
||||
signatureStatus: http.StatusNotFound,
|
||||
@ -377,7 +382,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
"broken signature": {
|
||||
measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
|
||||
csp: cloudprovider.Unknown,
|
||||
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
|
||||
imageVersion: v1Test,
|
||||
measurementsStatus: http.StatusOK,
|
||||
signature: "AAAAAAA1RR91pWPw1BMWXTSmTBHg/JtfKerbZNQ9PJTWDdW0sgIhANQbETJGb67qzQmMVmcq007VUFbHRMtYWKZeeyRf0gVa",
|
||||
signatureStatus: http.StatusOK,
|
||||
@ -386,7 +391,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
"metadata CSP mismatch": {
|
||||
measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
|
||||
csp: cloudprovider.GCP,
|
||||
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
|
||||
imageVersion: v1Test,
|
||||
measurementsStatus: http.StatusOK,
|
||||
signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
|
||||
signatureStatus: http.StatusOK,
|
||||
@ -395,7 +400,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
"metadata image mismatch": {
|
||||
measurements: `{"version":"v1.0.0-test","ref":"-","stream":"stable","list":[{"csp":"Unknown","attestationVariant":"dummy","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}]}`,
|
||||
csp: cloudprovider.Unknown,
|
||||
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-another-image", Kind: versionsapi.VersionKindImage},
|
||||
imageVersion: v1AnotherImage,
|
||||
measurementsStatus: http.StatusOK,
|
||||
signature: "MEUCIHuW2420EqN4Kj6OEaVMmufH7d01vyR1J+SWg8H4elyBAiEA1Ki5Hfq0iI70qpViYbrTFrd8e840NjtdAxGqJKiJgbA=",
|
||||
signatureStatus: http.StatusOK,
|
||||
@ -404,7 +409,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
"not json": {
|
||||
measurements: "This is some content to be signed!\n",
|
||||
csp: cloudprovider.Unknown,
|
||||
imageVersion: versionsapi.Version{Ref: "-", Stream: "stable", Version: "v1.0.0-test", Kind: versionsapi.VersionKindImage},
|
||||
imageVersion: v1Test,
|
||||
measurementsStatus: http.StatusOK,
|
||||
signature: "MEUCIQCGA/lSu5qCJgNNvgMaTKJ9rj6vQMecUDaQo3ukaiAfUgIgWoxXRoDKLY9naN7YgxokM7r2fwnyYk3M2WKJJO1g6yo=",
|
||||
signatureStatus: http.StatusOK,
|
||||
@ -418,6 +423,7 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
if tc.attestationVariant == nil {
|
||||
tc.attestationVariant = variant.Dummy{}
|
||||
@ -447,10 +453,12 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
||||
|
||||
m := M{}
|
||||
|
||||
verifier, err := sigstore.NewCosignVerifier(cosignPublicKey)
|
||||
require.NoError(err)
|
||||
|
||||
hash, err := m.fetchAndVerify(
|
||||
context.Background(), client, sigstore.CosignVerifier{},
|
||||
context.Background(), client, verifier,
|
||||
measurementsURL, signatureURL,
|
||||
cosignPublicKey,
|
||||
tc.imageVersion,
|
||||
tc.csp,
|
||||
tc.attestationVariant,
|
||||
|
@ -578,7 +578,7 @@ func (c *Config) IsNamedLikeDebugImage() bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return v.Stream == "debug"
|
||||
return v.Stream() == "debug"
|
||||
}
|
||||
|
||||
// GetProvider returns the configured cloud provider.
|
||||
|
@ -513,7 +513,7 @@ func validateImageCompatibilityHelper(binaryVersion consemver.Semver, fieldName,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configuredVersion = imageVersion.Version
|
||||
configuredVersion = imageVersion.Version()
|
||||
}
|
||||
|
||||
return compatibility.BinaryWith(binaryVersion.String(), configuredVersion)
|
||||
|
@ -51,9 +51,9 @@ func (f *Fetcher) FetchReference(ctx context.Context,
|
||||
}
|
||||
|
||||
imgInfoReq := versionsapi.ImageInfo{
|
||||
Ref: ver.Ref,
|
||||
Stream: ver.Stream,
|
||||
Version: ver.Version,
|
||||
Ref: ver.Ref(),
|
||||
Stream: ver.Stream(),
|
||||
Version: ver.Version(),
|
||||
}
|
||||
|
||||
url, err := imgInfoReq.URL()
|
||||
|
@ -67,7 +67,7 @@ func (a *Archivist) Close(ctx context.Context) error {
|
||||
|
||||
// Archive reads the OS image in img and uploads it as key.
|
||||
func (a *Archivist) Archive(ctx context.Context, version versionsapi.Version, csp, attestationVariant string, img io.Reader) (string, error) {
|
||||
key, err := url.JoinPath(version.ArtifactPath(versionsapi.APIV1), version.Kind.String(), "csp", csp, attestationVariant, "image.raw")
|
||||
key, err := url.JoinPath(version.ArtifactPath(versionsapi.APIV1), version.Kind().String(), "csp", csp, attestationVariant, "image.raw")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ func New(region, bucketName string, log *logger.Logger) (*Uploader, error) {
|
||||
|
||||
// Upload uploads an OS image to AWS.
|
||||
func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) ([]versionsapi.ImageInfoEntry, error) {
|
||||
blobName := fmt.Sprintf("image-%s-%s-%d.raw", req.Version.Stream, req.Version.Version, req.Timestamp.Unix())
|
||||
blobName := fmt.Sprintf("image-%s-%s-%d.raw", req.Version.Stream(), req.Version.Version(), req.Timestamp.Unix())
|
||||
imageName := imageName(req.Version, req.AttestationVariant, req.Timestamp)
|
||||
allRegions := []string{u.region}
|
||||
allRegions = append(allRegions, replicationRegions...)
|
||||
@ -479,10 +479,10 @@ func (u *Uploader) ensureImageDeleted(ctx context.Context, imageName, region str
|
||||
}
|
||||
|
||||
func imageName(version versionsapi.Version, attestationVariant string, timestamp time.Time) string {
|
||||
if version.Stream == "stable" {
|
||||
return fmt.Sprintf("constellation-%s-%s", version.Version, attestationVariant)
|
||||
if version.Stream() == "stable" {
|
||||
return fmt.Sprintf("constellation-%s-%s", version.Version(), attestationVariant)
|
||||
}
|
||||
return fmt.Sprintf("constellation-%s-%s-%s-%s", version.Stream, version.Version, attestationVariant, timestamp.Format(timestampFormat))
|
||||
return fmt.Sprintf("constellation-%s-%s-%s-%s", version.Stream(), version.Version(), attestationVariant, timestamp.Format(timestampFormat))
|
||||
}
|
||||
|
||||
func waitForSnapshotImport(ctx context.Context, ec2C ec2API, importTaskID string) (string, error) {
|
||||
|
@ -95,9 +95,9 @@ func New(subscription, location, resourceGroup string, log *logger.Logger) (*Upl
|
||||
// Upload uploads an OS image to Azure.
|
||||
func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) ([]versionsapi.ImageInfoEntry, error) {
|
||||
formattedTime := req.Timestamp.Format(timestampFormat)
|
||||
diskName := fmt.Sprintf("constellation-%s-%s-%s", req.Version.Stream, formattedTime, req.AttestationVariant)
|
||||
diskName := fmt.Sprintf("constellation-%s-%s-%s", req.Version.Stream(), formattedTime, req.AttestationVariant)
|
||||
var sigName string
|
||||
switch req.Version.Stream {
|
||||
switch req.Version.Stream() {
|
||||
case "stable":
|
||||
sigName = sigNameStable
|
||||
case "debug":
|
||||
@ -517,12 +517,12 @@ func uploadChunk(ctx context.Context, uploader azurePageblobAPI, chunk io.ReadSe
|
||||
|
||||
func imageOffer(version versionsapi.Version) string {
|
||||
switch {
|
||||
case version.Stream == "stable":
|
||||
case version.Stream() == "stable":
|
||||
return "constellation"
|
||||
case version.Stream == "debug" && version.Ref == "-":
|
||||
return version.Version
|
||||
case version.Stream() == "debug" && version.Ref() == "-":
|
||||
return version.Version()
|
||||
}
|
||||
return version.Ref + "-" + version.Stream
|
||||
return version.Ref() + "-" + version.Stream()
|
||||
}
|
||||
|
||||
// imageVersion determines the semantic version string used inside a sig image.
|
||||
@ -530,10 +530,10 @@ func imageOffer(version versionsapi.Version) string {
|
||||
// Otherwise, the version is derived from the commit timestamp.
|
||||
func imageVersion(version versionsapi.Version, timestamp time.Time) (string, error) {
|
||||
switch {
|
||||
case version.Stream == "stable":
|
||||
case version.Stream() == "stable":
|
||||
fallthrough
|
||||
case version.Stream == "debug" && version.Ref == "-":
|
||||
return strings.TrimLeft(version.Version, "v"), nil
|
||||
case version.Stream() == "debug" && version.Ref() == "-":
|
||||
return strings.TrimLeft(version.Version(), "v"), nil
|
||||
}
|
||||
|
||||
formattedTime := timestamp.Format(timestampFormat)
|
||||
|
@ -225,16 +225,16 @@ func (u *Uploader) blobURL(blobName string) string {
|
||||
}
|
||||
|
||||
func (u *Uploader) imageName(version versionsapi.Version, attestationVariant string) string {
|
||||
return strings.ReplaceAll(version.Version, ".", "-") + "-" + attestationVariant + "-" + version.Stream
|
||||
return strings.ReplaceAll(version.Version(), ".", "-") + "-" + attestationVariant + "-" + version.Stream()
|
||||
}
|
||||
|
||||
func (u *Uploader) imageFamily(version versionsapi.Version) string {
|
||||
if version.Stream == "stable" {
|
||||
if version.Stream() == "stable" {
|
||||
return "constellation"
|
||||
}
|
||||
truncatedRef := version.Ref
|
||||
if len(version.Ref) > 45 {
|
||||
truncatedRef = version.Ref[:45]
|
||||
truncatedRef := version.Ref()
|
||||
if len(version.Ref()) > 45 {
|
||||
truncatedRef = version.Ref()[:45]
|
||||
}
|
||||
return "constellation-" + truncatedRef
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||
@ -67,13 +68,11 @@ func (a *Uploader) Close(ctx context.Context) error {
|
||||
|
||||
// Upload marshals the image info to JSON and uploads it to S3.
|
||||
func (a *Uploader) Upload(ctx context.Context, imageInfo versionsapi.ImageInfo) (string, error) {
|
||||
ver := versionsapi.Version{
|
||||
Ref: imageInfo.Ref,
|
||||
Stream: imageInfo.Stream,
|
||||
Version: imageInfo.Version,
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
ver, err := versionsapi.NewVersion(imageInfo.Ref, imageInfo.Stream, imageInfo.Version, versionsapi.VersionKindImage)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating version: %w", err)
|
||||
}
|
||||
key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "info.json")
|
||||
key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind().String(), "info.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -78,17 +78,15 @@ func (a *Uploader) Upload(ctx context.Context, rawMeasurement, signature io.Read
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
ver := versionsapi.Version{
|
||||
Ref: measurements.Ref,
|
||||
Stream: measurements.Stream,
|
||||
Version: measurements.Version,
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
ver, err := versionsapi.NewVersion(measurements.Ref, measurements.Stream, measurements.Version, versionsapi.VersionKindImage)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("creating version: %w", err)
|
||||
}
|
||||
key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "measurements.json")
|
||||
key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind().String(), "measurements.json")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
sigKey, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "measurements.json.sig")
|
||||
sigKey, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind().String(), "measurements.json.sig")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
@ -12,8 +12,6 @@ go_library(
|
||||
importpath = "github.com/edgelesssys/constellation/v2/internal/sigstore",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api/versionsapi",
|
||||
"//internal/constants",
|
||||
"@com_github_sigstore_rekor//pkg/client",
|
||||
"@com_github_sigstore_rekor//pkg/generated/client",
|
||||
"@com_github_sigstore_rekor//pkg/generated/client/entries",
|
||||
|
23
internal/sigstore/keyselect/BUILD.bazel
Normal file
23
internal/sigstore/keyselect/BUILD.bazel
Normal file
@ -0,0 +1,23 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "foo",
|
||||
srcs = ["foo.go"],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/internal/sigstore/foo",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api/versionsapi",
|
||||
"//internal/constants",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "keyselect",
|
||||
srcs = ["keyselect.go"],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api/versionsapi",
|
||||
"//internal/constants",
|
||||
],
|
||||
)
|
28
internal/sigstore/keyselect/keyselect.go
Normal file
28
internal/sigstore/keyselect/keyselect.go
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// Package keyselect is used to select the correct public key for signature verification.
|
||||
// The content of keyselect must be kept separate from internal/sigstore because keyselect relies on internal/api/versionsapi.
|
||||
// Since internal/api relies on internal/sigstore, we need to separate the functions to avoid import cycles.
|
||||
package keyselect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
)
|
||||
|
||||
// CosignPublicKeyForVersion returns the public key for the given version.
|
||||
func CosignPublicKeyForVersion(ver versionsapi.Version) ([]byte, error) {
|
||||
if err := ver.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("selecting public key: invalid version %s: %w", ver.ShortPath(), err)
|
||||
}
|
||||
if ver.Ref() == versionsapi.ReleaseRef && ver.Stream() == "stable" {
|
||||
return []byte(constants.CosignPublicKeyReleases), nil
|
||||
}
|
||||
return []byte(constants.CosignPublicKeyDev), nil
|
||||
}
|
@ -17,7 +17,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||
"github.com/sigstore/rekor/pkg/client"
|
||||
genclient "github.com/sigstore/rekor/pkg/generated/client"
|
||||
"github.com/sigstore/rekor/pkg/generated/client/entries"
|
||||
@ -29,12 +28,7 @@ import (
|
||||
)
|
||||
|
||||
// VerifyWithRekor checks if the hash of a signature is present in Rekor.
|
||||
func VerifyWithRekor(ctx context.Context, version versionsapi.Version, verifier rekorVerifier, hash string) error {
|
||||
publicKey, err := CosignPublicKeyForVersion(version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
|
||||
func VerifyWithRekor(ctx context.Context, publicKey []byte, verifier rekorVerifier, hash string) error {
|
||||
uuids, err := verifier.SearchByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("searching Rekor for hash: %w", err)
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSignSignature(t *testing.T) {
|
||||
@ -50,8 +51,10 @@ func TestSignSignature(t *testing.T) {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
verifier := CosignVerifier{}
|
||||
assert.NoError(verifier.VerifySignature(tc.content, signature, publicKey))
|
||||
|
||||
verifier, err := NewCosignVerifier(publicKey)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(verifier.VerifySignature(tc.content, signature))
|
||||
|
||||
}
|
||||
})
|
||||
|
@ -12,37 +12,47 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
sigsig "github.com/sigstore/sigstore/pkg/signature"
|
||||
)
|
||||
|
||||
// Verifier checks if the signature of content can be verified.
|
||||
type Verifier interface {
|
||||
VerifySignature(content, signature, publicKey []byte) error
|
||||
VerifySignature(content, signature []byte) error
|
||||
}
|
||||
|
||||
// CosignVerifier checks if the signature of content can be verified
|
||||
// using a cosign public key.
|
||||
type CosignVerifier struct{}
|
||||
// CosignVerifier wraps a public key that can be used for verifying signatures.
|
||||
type CosignVerifier struct {
|
||||
publicKey crypto.PublicKey
|
||||
}
|
||||
|
||||
// NewCosignVerifier unmarshalls and validates the given pem encoded public key and returns a new CosignVerifier.
|
||||
func NewCosignVerifier(pem []byte) (Verifier, error) {
|
||||
pubkey, err := cryptoutils.UnmarshalPEMToPublicKey(pem)
|
||||
if err != nil {
|
||||
return CosignVerifier{}, fmt.Errorf("unable to parse public key: %w", err)
|
||||
}
|
||||
if err := cryptoutils.ValidatePubKey(pubkey); err != nil {
|
||||
return CosignVerifier{}, fmt.Errorf("unable to validate public key: %w", err)
|
||||
}
|
||||
|
||||
return CosignVerifier{pubkey}, nil
|
||||
}
|
||||
|
||||
// VerifySignature checks if the signature of content can be verified
|
||||
// using publicKey.
|
||||
// signature is expected to be base64 encoded.
|
||||
// publicKey is expected to be PEM encoded.
|
||||
func (CosignVerifier) VerifySignature(content, signature, publicKey []byte) error {
|
||||
func (c CosignVerifier) VerifySignature(content, signature []byte) error {
|
||||
// LoadVerifier would also error if no public key is set.
|
||||
// However, this error message should be easier to debug.
|
||||
if c.publicKey == nil {
|
||||
return fmt.Errorf("no public key set")
|
||||
}
|
||||
|
||||
sigRaw := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(signature))
|
||||
|
||||
pubKeyRaw, err := cryptoutils.UnmarshalPEMToPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse public key: %w", err)
|
||||
}
|
||||
if err := cryptoutils.ValidatePubKey(pubKeyRaw); err != nil {
|
||||
return fmt.Errorf("unable to validate public key: %w", err)
|
||||
}
|
||||
|
||||
verifier, err := sigsig.LoadVerifier(pubKeyRaw, crypto.SHA256)
|
||||
verifier, err := sigsig.LoadVerifier(c.publicKey, crypto.SHA256)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load verifier: %w", err)
|
||||
}
|
||||
@ -53,14 +63,3 @@ func (CosignVerifier) VerifySignature(content, signature, publicKey []byte) erro
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CosignPublicKeyForVersion returns the public key for the given version.
|
||||
func CosignPublicKeyForVersion(ver versionsapi.Version) ([]byte, error) {
|
||||
if err := ver.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("selecting public key: invalid version %s: %w", ver.ShortPath(), err)
|
||||
}
|
||||
if ver.Ref == versionsapi.ReleaseRef && ver.Stream == "stable" {
|
||||
return []byte(constants.CosignPublicKeyReleases), nil
|
||||
}
|
||||
return []byte(constants.CosignPublicKeyDev), nil
|
||||
}
|
||||
|
@ -10,8 +10,44 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCosignVerifier(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
publicKey []byte
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
publicKey: []byte(`-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElWUhon39eAqzEC+/GP03oY4/MQg+
|
||||
gCDlEzkuOCybCHf+q766bve799L7Y5y5oRsHY1MrUCUwYF/tL7Sg7EYMsA==
|
||||
-----END PUBLIC KEY-----`),
|
||||
},
|
||||
"broken public key": {
|
||||
publicKey: []byte(`-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIthisIsNotAValidPublicAtAllUhon39eAqzEC+/GP03oY4/MQg+
|
||||
gCDlEzkuOCybCHf+q766bve799L7Y5y5oRsHY1MrUCUwYF/tL7Sg7EYMsA==
|
||||
-----END PUBLIC KEY-----`),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
verifier, err := NewCosignVerifier(tc.publicKey)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.NotEqual(verifier, CosignVerifier{})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySignature(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
content []byte
|
||||
@ -19,7 +55,7 @@ func TestVerifySignature(t *testing.T) {
|
||||
publicKey []byte
|
||||
wantErr bool
|
||||
}{
|
||||
"good verification": {
|
||||
"success": {
|
||||
content: []byte("This is some content to be signed!\n"),
|
||||
signature: []byte("MEUCIQDzMN3yaiO9sxLGAaSA9YD8rLwzvOaZKWa/bzkcjImUFAIgXLLGzClYUd1dGbuEiY3O/g/eiwQYlyxqLQalxjFmz+8="),
|
||||
publicKey: []byte(`-----BEGIN PUBLIC KEY-----
|
||||
@ -36,32 +72,15 @@ gCDlEzkuOCybCHf+q766bve799L7Y5y5oRsHY1MrUCUwYF/tL7Sg7EYMsA==
|
||||
-----END PUBLIC KEY-----`),
|
||||
wantErr: true,
|
||||
},
|
||||
"broken public key": {
|
||||
content: []byte("This is some content to be signed!\n"),
|
||||
signature: []byte("MEUCIQDzMN3yaiO9sxLGAaSA9YD8rLwzvOaZKWa/bzkcjImUFAIgXLLGzClYUd1dGbuEiY3O/g/eiwQYlyxqLQalxjFmz+8="),
|
||||
publicKey: []byte(`-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIthisIsNotAValidPublicAtAllUhon39eAqzEC+/GP03oY4/MQg+
|
||||
gCDlEzkuOCybCHf+q766bve799L7Y5y5oRsHY1MrUCUwYF/tL7Sg7EYMsA==
|
||||
-----END PUBLIC KEY-----`),
|
||||
wantErr: true,
|
||||
},
|
||||
"valid content and sig, but mismatching public key": {
|
||||
content: []byte("This is some content to be signed!\n"),
|
||||
signature: []byte("MEUCIQDzMN3yaiO9sxLGAaSA9YD8rLwzvOaZKWa/bzkcjImUFAIgXLLGzClYUd1dGbuEiY3O/g/eiwQYlyxqLQalxjFmz+8="),
|
||||
publicKey: []byte(`-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFARL653CK4xicoxqwr4M9A2A/3hz
|
||||
hQaKKRsnjc2LITnxKYmQ4CYqTOAMfZ3agxpW/ndillUox4eDYcidZSXvWw==
|
||||
-----END PUBLIC KEY-----`),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cosign := CosignVerifier{}
|
||||
err := cosign.VerifySignature(tc.content, tc.signature, tc.publicKey)
|
||||
cosign, err := NewCosignVerifier(tc.publicKey)
|
||||
require.NoError(t, err)
|
||||
err = cosign.VerifySignature(tc.content, tc.signature)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
|
Loading…
x
Reference in New Issue
Block a user