api: add functions to transparently handle signatures upon API interaction (#2142)

This commit is contained in:
Otto Bittner 2023-08-01 16:48:13 +02:00 committed by GitHub
parent 002c3a9a32
commit dac690656e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 707 additions and 472 deletions

View File

@ -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",

View File

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

View File

@ -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,

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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,
}

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -5,4 +5,5 @@ go_library(
srcs = ["fetcher.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/api/fetcher",
visibility = ["//:__subpackages__"],
deps = ["//internal/sigstore"],
)

View File

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

View File

@ -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, &notFoundErr) {
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 {

View File

@ -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 {

View File

@ -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

View File

@ -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, &notFoundErr) {
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
}

View File

@ -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

View File

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

View File

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

View File

@ -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",
},

View File

@ -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",

View File

@ -12,6 +12,7 @@ go_library(
"//internal/attestation/variant",
"//internal/cloud/cloudprovider",
"//internal/sigstore",
"//internal/sigstore/keyselect",
"@org_golang_x_tools//go/ast/astutil",
],
)

View File

@ -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

View File

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

View File

@ -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,

View File

@ -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.

View File

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

View File

@ -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()

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View 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",
],
)

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

View File

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

View File

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

View File

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

View File

@ -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