mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-03 04:26:20 -04:00
cli: dynamically select signature validation pubkey for release and pre-release artifacts
This commit is contained in:
parent
ada66a64a1
commit
8a851c8f39
19 changed files with 170 additions and 145 deletions
|
@ -31,7 +31,6 @@ go_library(
|
|||
"upgradecheck.go",
|
||||
"userinteraction.go",
|
||||
"validargs.go",
|
||||
"verifier.go",
|
||||
"verify.go",
|
||||
"version.go",
|
||||
],
|
||||
|
|
|
@ -63,11 +63,11 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error {
|
|||
}
|
||||
cfm := &configFetchMeasurementsCmd{log: log}
|
||||
|
||||
return cfm.configFetchMeasurements(cmd, rekor, []byte(constants.CosignPublicKey), fileHandler, http.DefaultClient)
|
||||
return cfm.configFetchMeasurements(cmd, sigstore.CosignVerifier{}, rekor, fileHandler, http.DefaultClient)
|
||||
}
|
||||
|
||||
func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
||||
cmd *cobra.Command, verifier rekorVerifier, cosignPublicKey []byte,
|
||||
cmd *cobra.Command, cosign cosignVerifier, rekor rekorVerifier,
|
||||
fileHandler file.Handler, client *http.Client,
|
||||
) error {
|
||||
flags, err := cfm.parseFetchMeasurementsFlags(cmd)
|
||||
|
@ -106,10 +106,9 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
|||
}
|
||||
var fetchedMeasurements measurements.M
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, client,
|
||||
ctx, client, cosign,
|
||||
flags.measurementsURL,
|
||||
flags.signatureURL,
|
||||
cosignPublicKey,
|
||||
imageVersion,
|
||||
conf.GetProvider(),
|
||||
conf.GetAttestationConfig().GetVariant(),
|
||||
|
@ -119,7 +118,7 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
|||
}
|
||||
|
||||
cfm.log.Debugf("Fetched and verified measurements, hash is %s", hash)
|
||||
if err := verifyWithRekor(cmd.Context(), verifier, hash); err != nil {
|
||||
if err := sigstore.VerifyWithRekor(cmd.Context(), imageVersion, rekor, hash); err != nil {
|
||||
cmd.PrintErrf("Ignoring Rekor related error: %v\n", err)
|
||||
cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!")
|
||||
}
|
||||
|
@ -199,3 +198,12 @@ func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type rekorVerifier interface {
|
||||
SearchByHash(context.Context, string) ([]string, error)
|
||||
VerifyEntry(context.Context, string, string) error
|
||||
}
|
||||
|
||||
type cosignVerifier interface {
|
||||
VerifySignature(content, signature, publicKey []byte) error
|
||||
}
|
||||
|
|
|
@ -162,24 +162,6 @@ func newTestClient(fn roundTripFunc) *http.Client {
|
|||
}
|
||||
|
||||
func TestConfigFetchMeasurements(t *testing.T) {
|
||||
// Cosign private key used to sign the measurements.
|
||||
// Generated with: cosign generate-key-pair
|
||||
// Password left empty.
|
||||
//
|
||||
// -----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
|
||||
// eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
|
||||
// OCwicCI6MX0sInNhbHQiOiJlRHVYMWRQMGtIWVRnK0xkbjcxM0tjbFVJaU92eFVX
|
||||
// VXgvNi9BbitFVk5BPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
|
||||
// Iiwibm9uY2UiOiJwaWhLL2txNmFXa2hqSVVHR3RVUzhTVkdHTDNIWWp4TCJ9LCJj
|
||||
// aXBoZXJ0ZXh0Ijoidm81SHVWRVFWcUZ2WFlQTTVPaTVaWHM5a255bndZU2dvcyth
|
||||
// VklIeHcrOGFPamNZNEtvVjVmL3lHRHR0K3BHV2toanJPR1FLOWdBbmtsazFpQ0c5
|
||||
// a2czUXpPQTZsU2JRaHgvZlowRVRZQ0hLeElncEdPRVRyTDlDenZDemhPZXVSOXJ6
|
||||
// TDcvRjBBVy9vUDVqZXR3dmJMNmQxOEhjck9kWE8yVmYxY2w0YzNLZjVRcnFSZzlN
|
||||
// dlRxQWFsNXJCNHNpY1JaMVhpUUJjb0YwNHc9PSJ9
|
||||
// -----END ENCRYPTED COSIGN PRIVATE KEY-----
|
||||
|
||||
cosignPublicKey := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
|
||||
|
||||
measurements := `{
|
||||
"version": "v999.999.999",
|
||||
"ref": "-",
|
||||
|
@ -222,7 +204,7 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
|||
]
|
||||
}
|
||||
`
|
||||
signature := "MEUCIHQETkvMRy8WaWMroX4Aa2J86bTW0kGMp8NG0YLXJKZJAiEA7ZdxoQzSTyBFNhZ1bwB5eT3av0biAdb66dJRFxQlKLA="
|
||||
signature := "placeholder-signature"
|
||||
|
||||
client := newTestClient(func(req *http.Request) *http.Response {
|
||||
if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json" {
|
||||
|
@ -249,23 +231,35 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
|||
})
|
||||
|
||||
testCases := map[string]struct {
|
||||
verifier rekorVerifier
|
||||
cosign cosignVerifier
|
||||
rekor rekorVerifier
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
verifier: singleUUIDVerifier(),
|
||||
cosign: &stubCosignVerifier{},
|
||||
rekor: singleUUIDVerifier(),
|
||||
},
|
||||
"failing search should not result in error": {
|
||||
verifier: &stubRekorVerifier{
|
||||
cosign: &stubCosignVerifier{},
|
||||
rekor: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{},
|
||||
SearchByHashError: errors.New("some error"),
|
||||
},
|
||||
},
|
||||
"failing verify should not result in error": {
|
||||
verifier: &stubRekorVerifier{
|
||||
cosign: &stubCosignVerifier{},
|
||||
rekor: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
|
||||
VerifyEntryError: errors.New("some error"),
|
||||
},
|
||||
},
|
||||
"signature verification failure": {
|
||||
cosign: &stubCosignVerifier{
|
||||
verifyError: errors.New("some error"),
|
||||
},
|
||||
rekor: singleUUIDVerifier(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
|
@ -285,7 +279,12 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
|||
require.NoError(err)
|
||||
cfm := &configFetchMeasurementsCmd{log: logger.NewTest(t)}
|
||||
|
||||
assert.NoError(cfm.configFetchMeasurements(cmd, tc.verifier, cosignPublicKey, fileHandler, client))
|
||||
err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, client)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error {
|
|||
verListFetcher: versionListFetcher,
|
||||
fileHandler: fileHandler,
|
||||
client: http.DefaultClient,
|
||||
cosign: sigstore.CosignVerifier{},
|
||||
rekor: rekor,
|
||||
flags: flags,
|
||||
cliVersion: compatibility.EnsurePrefixV(constants.VersionInfo()),
|
||||
|
@ -113,12 +114,11 @@ func parseUpgradeCheckFlags(cmd *cobra.Command) (upgradeCheckFlags, error) {
|
|||
return upgradeCheckFlags{}, err
|
||||
}
|
||||
return upgradeCheckFlags{
|
||||
configPath: configPath,
|
||||
force: force,
|
||||
writeConfig: writeConfig,
|
||||
ref: ref,
|
||||
stream: stream,
|
||||
cosignPubKey: constants.CosignPublicKey,
|
||||
configPath: configPath,
|
||||
force: force,
|
||||
writeConfig: writeConfig,
|
||||
ref: ref,
|
||||
stream: stream,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -254,6 +254,7 @@ type versionCollector struct {
|
|||
verListFetcher versionListFetcher
|
||||
fileHandler file.Handler
|
||||
client *http.Client
|
||||
cosign cosignVerifier
|
||||
rekor rekorVerifier
|
||||
flags upgradeCheckFlags
|
||||
versionsapi versionFetcher
|
||||
|
@ -263,7 +264,7 @@ type versionCollector struct {
|
|||
|
||||
func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, images []versionsapi.Version) (map[string]measurements.M, error) {
|
||||
// get expected measurements for each image
|
||||
upgrades, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, v.rekor, []byte(v.flags.cosignPubKey), csp, attestationVariant, images, v.log)
|
||||
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)
|
||||
}
|
||||
|
@ -525,7 +526,7 @@ 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, rekor rekorVerifier, pubK []byte,
|
||||
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)
|
||||
|
@ -540,10 +541,9 @@ func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, clien
|
|||
var fetchedMeasurements measurements.M
|
||||
log.Debugf("Fetching for measurement url: %s", measurementsURL)
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, client,
|
||||
ctx, client, cosign,
|
||||
measurementsURL,
|
||||
signatureURL,
|
||||
pubK,
|
||||
version,
|
||||
csp,
|
||||
attestationVariant,
|
||||
|
@ -555,7 +555,7 @@ func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, clien
|
|||
continue
|
||||
}
|
||||
|
||||
if err = verifyWithRekor(ctx, rekor, hash); err != nil {
|
||||
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)
|
||||
}
|
||||
|
@ -651,12 +651,11 @@ func (v *versionCollector) filterCompatibleCLIVersions(ctx context.Context, cliP
|
|||
}
|
||||
|
||||
type upgradeCheckFlags struct {
|
||||
configPath string
|
||||
force bool
|
||||
writeConfig bool
|
||||
ref string
|
||||
stream string
|
||||
cosignPubKey string
|
||||
configPath string
|
||||
force bool
|
||||
writeConfig bool
|
||||
ref string
|
||||
stream string
|
||||
}
|
||||
|
||||
type upgradeChecker interface {
|
||||
|
|
|
@ -203,9 +203,7 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
|
||||
|
||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, singleUUIDVerifier(), pubK, csp, attestationVariant, images, logger.NewTest(t))
|
||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, &stubCosignVerifier{}, singleUUIDVerifier(), csp, attestationVariant, images, logger.NewTest(t))
|
||||
assert.NoError(err)
|
||||
|
||||
for _, measurement := range upgrades {
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
)
|
||||
|
||||
type rekorVerifier interface {
|
||||
SearchByHash(context.Context, string) ([]string, error)
|
||||
VerifyEntry(context.Context, string, string) error
|
||||
}
|
||||
|
||||
func verifyWithRekor(ctx context.Context, verifier rekorVerifier, hash string) error {
|
||||
uuids, err := verifier.SearchByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("searching Rekor for hash: %w", err)
|
||||
}
|
||||
|
||||
if len(uuids) == 0 {
|
||||
return fmt.Errorf("no matching entries in Rekor")
|
||||
}
|
||||
|
||||
// We expect the first entry in Rekor to be our original entry.
|
||||
// SHA256 should ensure there is no entry with the same hash.
|
||||
// Any subsequent hashes are treated as potential attacks and are ignored.
|
||||
// Attacks on Rekor will be monitored from other backend services.
|
||||
artifactUUID := uuids[0]
|
||||
|
||||
return verifier.VerifyEntry(
|
||||
ctx, artifactUUID,
|
||||
base64.StdEncoding.EncodeToString([]byte(constants.CosignPublicKey)),
|
||||
)
|
||||
}
|
|
@ -34,3 +34,11 @@ func (v *stubRekorVerifier) SearchByHash(context.Context, string) ([]string, err
|
|||
func (v *stubRekorVerifier) VerifyEntry(context.Context, string, string) error {
|
||||
return v.VerifyEntryError
|
||||
}
|
||||
|
||||
type stubCosignVerifier struct {
|
||||
verifyError error
|
||||
}
|
||||
|
||||
func (v *stubCosignVerifier) VerifySignature(_, _, _ []byte) error {
|
||||
return v.verifyError
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue