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