mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-02-04 17:15:26 -05:00
cli: dynamically select signature validation pubkey for release and pre-release artifacts
This commit is contained in:
parent
ada66a64a1
commit
8a851c8f39
@ -31,7 +31,6 @@ go_library(
|
|||||||
"upgradecheck.go",
|
"upgradecheck.go",
|
||||||
"userinteraction.go",
|
"userinteraction.go",
|
||||||
"validargs.go",
|
"validargs.go",
|
||||||
"verifier.go",
|
|
||||||
"verify.go",
|
"verify.go",
|
||||||
"version.go",
|
"version.go",
|
||||||
],
|
],
|
||||||
|
@ -63,11 +63,11 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error {
|
|||||||
}
|
}
|
||||||
cfm := &configFetchMeasurementsCmd{log: log}
|
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(
|
func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
||||||
cmd *cobra.Command, verifier rekorVerifier, cosignPublicKey []byte,
|
cmd *cobra.Command, cosign cosignVerifier, rekor rekorVerifier,
|
||||||
fileHandler file.Handler, client *http.Client,
|
fileHandler file.Handler, client *http.Client,
|
||||||
) error {
|
) error {
|
||||||
flags, err := cfm.parseFetchMeasurementsFlags(cmd)
|
flags, err := cfm.parseFetchMeasurementsFlags(cmd)
|
||||||
@ -106,10 +106,9 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
|||||||
}
|
}
|
||||||
var fetchedMeasurements measurements.M
|
var fetchedMeasurements measurements.M
|
||||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||||
ctx, client,
|
ctx, client, cosign,
|
||||||
flags.measurementsURL,
|
flags.measurementsURL,
|
||||||
flags.signatureURL,
|
flags.signatureURL,
|
||||||
cosignPublicKey,
|
|
||||||
imageVersion,
|
imageVersion,
|
||||||
conf.GetProvider(),
|
conf.GetProvider(),
|
||||||
conf.GetAttestationConfig().GetVariant(),
|
conf.GetAttestationConfig().GetVariant(),
|
||||||
@ -119,7 +118,7 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfm.log.Debugf("Fetched and verified measurements, hash is %s", hash)
|
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.PrintErrf("Ignoring Rekor related error: %v\n", err)
|
||||||
cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!")
|
cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!")
|
||||||
}
|
}
|
||||||
@ -199,3 +198,12 @@ func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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) {
|
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 := `{
|
measurements := `{
|
||||||
"version": "v999.999.999",
|
"version": "v999.999.999",
|
||||||
"ref": "-",
|
"ref": "-",
|
||||||
@ -222,7 +204,7 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
signature := "MEUCIHQETkvMRy8WaWMroX4Aa2J86bTW0kGMp8NG0YLXJKZJAiEA7ZdxoQzSTyBFNhZ1bwB5eT3av0biAdb66dJRFxQlKLA="
|
signature := "placeholder-signature"
|
||||||
|
|
||||||
client := newTestClient(func(req *http.Request) *http.Response {
|
client := newTestClient(func(req *http.Request) *http.Response {
|
||||||
if req.URL.Path == "/constellation/v2/ref/-/stream/stable/v999.999.999/image/measurements.json" {
|
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 {
|
testCases := map[string]struct {
|
||||||
verifier rekorVerifier
|
cosign cosignVerifier
|
||||||
|
rekor rekorVerifier
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
"success": {
|
"success": {
|
||||||
verifier: singleUUIDVerifier(),
|
cosign: &stubCosignVerifier{},
|
||||||
|
rekor: singleUUIDVerifier(),
|
||||||
},
|
},
|
||||||
"failing search should not result in error": {
|
"failing search should not result in error": {
|
||||||
verifier: &stubRekorVerifier{
|
cosign: &stubCosignVerifier{},
|
||||||
|
rekor: &stubRekorVerifier{
|
||||||
SearchByHashUUIDs: []string{},
|
SearchByHashUUIDs: []string{},
|
||||||
SearchByHashError: errors.New("some error"),
|
SearchByHashError: errors.New("some error"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"failing verify should not result in error": {
|
"failing verify should not result in error": {
|
||||||
verifier: &stubRekorVerifier{
|
cosign: &stubCosignVerifier{},
|
||||||
|
rekor: &stubRekorVerifier{
|
||||||
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
|
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
|
||||||
VerifyEntryError: errors.New("some error"),
|
VerifyEntryError: errors.New("some error"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"signature verification failure": {
|
||||||
|
cosign: &stubCosignVerifier{
|
||||||
|
verifyError: errors.New("some error"),
|
||||||
|
},
|
||||||
|
rekor: singleUUIDVerifier(),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, tc := range testCases {
|
for name, tc := range testCases {
|
||||||
@ -285,7 +279,12 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
|||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
cfm := &configFetchMeasurementsCmd{log: logger.NewTest(t)}
|
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,
|
verListFetcher: versionListFetcher,
|
||||||
fileHandler: fileHandler,
|
fileHandler: fileHandler,
|
||||||
client: http.DefaultClient,
|
client: http.DefaultClient,
|
||||||
|
cosign: sigstore.CosignVerifier{},
|
||||||
rekor: rekor,
|
rekor: rekor,
|
||||||
flags: flags,
|
flags: flags,
|
||||||
cliVersion: compatibility.EnsurePrefixV(constants.VersionInfo()),
|
cliVersion: compatibility.EnsurePrefixV(constants.VersionInfo()),
|
||||||
@ -113,12 +114,11 @@ func parseUpgradeCheckFlags(cmd *cobra.Command) (upgradeCheckFlags, error) {
|
|||||||
return upgradeCheckFlags{}, err
|
return upgradeCheckFlags{}, err
|
||||||
}
|
}
|
||||||
return upgradeCheckFlags{
|
return upgradeCheckFlags{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
force: force,
|
force: force,
|
||||||
writeConfig: writeConfig,
|
writeConfig: writeConfig,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
stream: stream,
|
stream: stream,
|
||||||
cosignPubKey: constants.CosignPublicKey,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,6 +254,7 @@ type versionCollector struct {
|
|||||||
verListFetcher versionListFetcher
|
verListFetcher versionListFetcher
|
||||||
fileHandler file.Handler
|
fileHandler file.Handler
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
cosign cosignVerifier
|
||||||
rekor rekorVerifier
|
rekor rekorVerifier
|
||||||
flags upgradeCheckFlags
|
flags upgradeCheckFlags
|
||||||
versionsapi versionFetcher
|
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) {
|
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
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetching measurements for compatible images: %w", err)
|
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.
|
// 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,
|
csp cloudprovider.Provider, attestationVariant variant.Variant, versions []versionsapi.Version, log debugLog,
|
||||||
) (map[string]measurements.M, error) {
|
) (map[string]measurements.M, error) {
|
||||||
upgrades := make(map[string]measurements.M)
|
upgrades := make(map[string]measurements.M)
|
||||||
@ -540,10 +541,9 @@ func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, clien
|
|||||||
var fetchedMeasurements measurements.M
|
var fetchedMeasurements measurements.M
|
||||||
log.Debugf("Fetching for measurement url: %s", measurementsURL)
|
log.Debugf("Fetching for measurement url: %s", measurementsURL)
|
||||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||||
ctx, client,
|
ctx, client, cosign,
|
||||||
measurementsURL,
|
measurementsURL,
|
||||||
signatureURL,
|
signatureURL,
|
||||||
pubK,
|
|
||||||
version,
|
version,
|
||||||
csp,
|
csp,
|
||||||
attestationVariant,
|
attestationVariant,
|
||||||
@ -555,7 +555,7 @@ func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, clien
|
|||||||
continue
|
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 {
|
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)
|
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||||
}
|
}
|
||||||
@ -651,12 +651,11 @@ func (v *versionCollector) filterCompatibleCLIVersions(ctx context.Context, cliP
|
|||||||
}
|
}
|
||||||
|
|
||||||
type upgradeCheckFlags struct {
|
type upgradeCheckFlags struct {
|
||||||
configPath string
|
configPath string
|
||||||
force bool
|
force bool
|
||||||
writeConfig bool
|
writeConfig bool
|
||||||
ref string
|
ref string
|
||||||
stream string
|
stream string
|
||||||
cosignPubKey string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type upgradeChecker interface {
|
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, &stubCosignVerifier{}, singleUUIDVerifier(), csp, attestationVariant, images, logger.NewTest(t))
|
||||||
|
|
||||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, singleUUIDVerifier(), pubK, csp, attestationVariant, images, logger.NewTest(t))
|
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
|
|
||||||
for _, measurement := range upgrades {
|
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 {
|
func (v *stubRekorVerifier) VerifyEntry(context.Context, string, string) error {
|
||||||
return v.VerifyEntryError
|
return v.VerifyEntryError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stubCosignVerifier struct {
|
||||||
|
verifyError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *stubCosignVerifier) VerifySignature(_, _, _ []byte) error {
|
||||||
|
return v.verifyError
|
||||||
|
}
|
||||||
|
@ -30,6 +30,7 @@ go_test(
|
|||||||
deps = [
|
deps = [
|
||||||
"//internal/api/versionsapi",
|
"//internal/api/versionsapi",
|
||||||
"//internal/cloud/cloudprovider",
|
"//internal/cloud/cloudprovider",
|
||||||
|
"//internal/sigstore",
|
||||||
"//internal/variant",
|
"//internal/variant",
|
||||||
"@com_github_siderolabs_talos_pkg_machinery//config/encoder",
|
"@com_github_siderolabs_talos_pkg_machinery//config/encoder",
|
||||||
"@com_github_stretchr_testify//assert",
|
"@com_github_stretchr_testify//assert",
|
||||||
|
@ -11,7 +11,6 @@ go_library(
|
|||||||
"//internal/attestation/measurements",
|
"//internal/attestation/measurements",
|
||||||
"//internal/cloud/cloudprovider",
|
"//internal/cloud/cloudprovider",
|
||||||
"//internal/config",
|
"//internal/config",
|
||||||
"//internal/constants",
|
|
||||||
"//internal/sigstore",
|
"//internal/sigstore",
|
||||||
"//internal/variant",
|
"//internal/variant",
|
||||||
"@org_golang_x_tools//go/ast/astutil",
|
"@org_golang_x_tools//go/ast/astutil",
|
||||||
|
@ -9,7 +9,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/parser"
|
"go/parser"
|
||||||
@ -27,7 +26,6 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/variant"
|
"github.com/edgelesssys/constellation/v2/internal/variant"
|
||||||
"golang.org/x/tools/go/ast/astutil"
|
"golang.org/x/tools/go/ast/astutil"
|
||||||
@ -82,7 +80,7 @@ func main() {
|
|||||||
log.Println("Found", variant)
|
log.Println("Found", variant)
|
||||||
returnStmtCtr++
|
returnStmtCtr++
|
||||||
// retrieve and validate measurements for the given CSP and image
|
// retrieve and validate measurements for the given CSP and image
|
||||||
measuremnts := mustGetMeasurements(ctx, rekor, []byte(constants.CosignPublicKey), http.DefaultClient, provider, variant, defaultConf.Image)
|
measuremnts := mustGetMeasurements(ctx, rekor, provider, variant, defaultConf.Image)
|
||||||
// replace the return statement with a composite literal containing the validated measurements
|
// replace the return statement with a composite literal containing the validated measurements
|
||||||
clause.Values[0] = measurementsCompositeLiteral(measuremnts)
|
clause.Values[0] = measurementsCompositeLiteral(measuremnts)
|
||||||
}
|
}
|
||||||
@ -107,7 +105,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mustGetMeasurements fetches the measurements for the given image and CSP and verifies them.
|
// mustGetMeasurements fetches the measurements for the given image and CSP and verifies them.
|
||||||
func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPublicKey []byte, client *http.Client, provider cloudprovider.Provider, attestationVariant variant.Variant, image string) measurements.M {
|
func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, provider cloudprovider.Provider, attestationVariant variant.Variant, image string) measurements.M {
|
||||||
measurementsURL, err := measurementURL(image, "measurements.json")
|
measurementsURL, err := measurementURL(image, "measurements.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -125,10 +123,9 @@ func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPubl
|
|||||||
log.Println("Fetching measurements from", measurementsURL, "and signature from", signatureURL)
|
log.Println("Fetching measurements from", measurementsURL, "and signature from", signatureURL)
|
||||||
var fetchedMeasurements measurements.M
|
var fetchedMeasurements measurements.M
|
||||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||||
ctx, client,
|
ctx, http.DefaultClient, sigstore.CosignVerifier{},
|
||||||
measurementsURL,
|
measurementsURL,
|
||||||
signatureURL,
|
signatureURL,
|
||||||
cosignPublicKey,
|
|
||||||
imageVersion,
|
imageVersion,
|
||||||
provider,
|
provider,
|
||||||
attestationVariant,
|
attestationVariant,
|
||||||
@ -136,7 +133,7 @@ func mustGetMeasurements(ctx context.Context, verifier rekorVerifier, cosignPubl
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := verifyWithRekor(ctx, verifier, hash); err != nil {
|
if err := sigstore.VerifyWithRekor(ctx, imageVersion, verifier, hash); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return fetchedMeasurements
|
return fetchedMeasurements
|
||||||
@ -154,29 +151,6 @@ func measurementURL(image, file string) (*url.URL, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyWithRekor verifies that the given hash is present in rekor and is valid.
|
|
||||||
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)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// byteArrayCompositeLit returns a *ast.CompositeLit representing a byte array literal.
|
// byteArrayCompositeLit returns a *ast.CompositeLit representing a byte array literal.
|
||||||
// The returned literal is of the form:
|
// The returned literal is of the form:
|
||||||
// []byte{ 0x01, 0x02, 0x03, ... }.
|
// []byte{ 0x01, 0x02, 0x03, ... }.
|
||||||
|
@ -129,11 +129,31 @@ func (m M) MarshalYAML() (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FetchAndVerify fetches measurement and signature files via provided URLs,
|
// FetchAndVerify fetches measurement and signature files via provided URLs,
|
||||||
// using client for download. The publicKey is used to verify the measurements.
|
// using client for download.
|
||||||
// The hash of the fetched measurements is returned.
|
// The hash of the fetched measurements is returned.
|
||||||
func (m *M) FetchAndVerify(
|
func (m *M) FetchAndVerify(
|
||||||
ctx context.Context, client *http.Client, measurementsURL, signatureURL *url.URL,
|
ctx context.Context, client *http.Client, verifier cosignVerifier,
|
||||||
publicKey []byte, version versionsapi.Version, csp cloudprovider.Provider, attestationVariant variant.Variant,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchAndVerify fetches measurement and signature files via provided URLs,
|
||||||
|
// using client for download. The publicKey is used to verify the measurements.
|
||||||
|
// 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,
|
||||||
|
version versionsapi.Version, csp cloudprovider.Provider, attestationVariant variant.Variant,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
measurementsRaw, err := getFromURL(ctx, client, measurementsURL)
|
measurementsRaw, err := getFromURL(ctx, client, measurementsURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -143,7 +163,7 @@ func (m *M) FetchAndVerify(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to fetch signature: %w", err)
|
return "", fmt.Errorf("failed to fetch signature: %w", err)
|
||||||
}
|
}
|
||||||
if err := sigstore.VerifySignature(measurementsRaw, signature, publicKey); err != nil {
|
if err := verifier.VerifySignature(measurementsRaw, signature, publicKey); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -539,3 +559,7 @@ func (c mYamlContent) Swap(i, j int) {
|
|||||||
c[2*i], c[2*j] = c[2*j], c[2*i]
|
c[2*i], c[2*j] = c[2*j], c[2*i]
|
||||||
c[2*i+1], c[2*j+1] = c[2*j+1], c[2*i+1]
|
c[2*i+1], c[2*j+1] = c[2*j+1], c[2*i+1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cosignVerifier interface {
|
||||||
|
VerifySignature(content, signature, publicKey []byte) error
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/variant"
|
"github.com/edgelesssys/constellation/v2/internal/variant"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -470,8 +471,8 @@ func TestMeasurementsFetchAndVerify(t *testing.T) {
|
|||||||
|
|
||||||
m := M{}
|
m := M{}
|
||||||
|
|
||||||
hash, err := m.FetchAndVerify(
|
hash, err := m.fetchAndVerify(
|
||||||
context.Background(), client,
|
context.Background(), client, sigstore.CosignVerifier{},
|
||||||
measurementsURL, signatureURL,
|
measurementsURL, signatureURL,
|
||||||
cosignPublicKey,
|
cosignPublicKey,
|
||||||
tc.imageVersion,
|
tc.imageVersion,
|
||||||
|
@ -197,6 +197,23 @@ const (
|
|||||||
CDNMeasurementsFile = "measurements.json"
|
CDNMeasurementsFile = "measurements.json"
|
||||||
// CDNMeasurementsSignature is name of file containing signature for CDNMeasurementsFile.
|
// CDNMeasurementsSignature is name of file containing signature for CDNMeasurementsFile.
|
||||||
CDNMeasurementsSignature = "measurements.json.sig"
|
CDNMeasurementsSignature = "measurements.json.sig"
|
||||||
|
|
||||||
|
//
|
||||||
|
// PKI.
|
||||||
|
//
|
||||||
|
|
||||||
|
// CosignPublicKeyReleases signs all our releases.
|
||||||
|
CosignPublicKeyReleases = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf8F1hpmwE+YCFXzjGtaQcrL6XZVT
|
||||||
|
JmEe5iSLvG1SyQSAew7WdMKF6o9t8e2TFuCkzlOhhlws2OHWbiFZnFWCFw==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
`
|
||||||
|
// CosignPublicKeyDev signs all our development builds.
|
||||||
|
CosignPublicKeyDev = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELcPl4Ik+qZuH4K049wksoXK/Os3Z
|
||||||
|
b92PDCpM7FZAINQF88s1TZS/HmRXYk62UJ4eqPduvUnJmXhNikhLbMi6fw==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
// VersionInfo returns the version of a binary.
|
// VersionInfo returns the version of a binary.
|
||||||
|
@ -8,12 +8,5 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
package constants
|
package constants
|
||||||
|
|
||||||
// CosignPublicKey signs all our releases.
|
|
||||||
const CosignPublicKey = `-----BEGIN PUBLIC KEY-----
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf8F1hpmwE+YCFXzjGtaQcrL6XZVT
|
|
||||||
JmEe5iSLvG1SyQSAew7WdMKF6o9t8e2TFuCkzlOhhlws2OHWbiFZnFWCFw==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
`
|
|
||||||
|
|
||||||
// VersionBuild is the category of the current build.
|
// VersionBuild is the category of the current build.
|
||||||
const VersionBuild = "Enterprise build; see documentation for license agreement"
|
const VersionBuild = "Enterprise build; see documentation for license agreement"
|
||||||
|
@ -8,12 +8,5 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
package constants
|
package constants
|
||||||
|
|
||||||
// CosignPublicKey signs all our development builds.
|
|
||||||
const CosignPublicKey = `-----BEGIN PUBLIC KEY-----
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELcPl4Ik+qZuH4K049wksoXK/Os3Z
|
|
||||||
b92PDCpM7FZAINQF88s1TZS/HmRXYk62UJ4eqPduvUnJmXhNikhLbMi6fw==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
`
|
|
||||||
|
|
||||||
// VersionBuild is the category of the current build.
|
// VersionBuild is the category of the current build.
|
||||||
const VersionBuild = "Open-source software build; AGPL-3.0-only applies"
|
const VersionBuild = "Open-source software build; AGPL-3.0-only applies"
|
||||||
|
@ -11,6 +11,8 @@ go_library(
|
|||||||
importpath = "github.com/edgelesssys/constellation/v2/internal/sigstore",
|
importpath = "github.com/edgelesssys/constellation/v2/internal/sigstore",
|
||||||
visibility = ["//:__subpackages__"],
|
visibility = ["//:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//internal/api/versionsapi",
|
||||||
|
"//internal/constants",
|
||||||
"@com_github_sigstore_rekor//pkg/client",
|
"@com_github_sigstore_rekor//pkg/client",
|
||||||
"@com_github_sigstore_rekor//pkg/generated/client",
|
"@com_github_sigstore_rekor//pkg/generated/client",
|
||||||
"@com_github_sigstore_rekor//pkg/generated/client/entries",
|
"@com_github_sigstore_rekor//pkg/generated/client/entries",
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||||
"github.com/sigstore/rekor/pkg/client"
|
"github.com/sigstore/rekor/pkg/client"
|
||||||
genclient "github.com/sigstore/rekor/pkg/generated/client"
|
genclient "github.com/sigstore/rekor/pkg/generated/client"
|
||||||
"github.com/sigstore/rekor/pkg/generated/client/entries"
|
"github.com/sigstore/rekor/pkg/generated/client/entries"
|
||||||
@ -27,6 +28,34 @@ import (
|
|||||||
"github.com/sigstore/sigstore/pkg/signature"
|
"github.com/sigstore/sigstore/pkg/signature"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuids, err := verifier.SearchByHash(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("searching Rekor for hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(uuids) == 0 {
|
||||||
|
return errors.New("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(publicKey),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Rekor allows to interact with the transparency log at:
|
// Rekor allows to interact with the transparency log at:
|
||||||
// https://rekor.sigstore.dev
|
// https://rekor.sigstore.dev
|
||||||
// For more information see Rekor's Swagger definition:
|
// For more information see Rekor's Swagger definition:
|
||||||
@ -194,3 +223,8 @@ func isEntrySignedBy(rekord *hashedrekord.V001Entry, publicKey string) bool {
|
|||||||
actualKey := rekord.HashedRekordObj.Signature.PublicKey.Content.String()
|
actualKey := rekord.HashedRekordObj.Signature.PublicKey.Content.String()
|
||||||
return actualKey == publicKey
|
return actualKey == publicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rekorVerifier interface {
|
||||||
|
SearchByHash(context.Context, string) ([]string, error)
|
||||||
|
VerifyEntry(context.Context, string, string) error
|
||||||
|
}
|
||||||
|
@ -12,15 +12,21 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||||
sigsig "github.com/sigstore/sigstore/pkg/signature"
|
sigsig "github.com/sigstore/sigstore/pkg/signature"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CosignVerifier checks if the signature of content can be verified
|
||||||
|
// using a cosign public key.
|
||||||
|
type CosignVerifier struct{}
|
||||||
|
|
||||||
// VerifySignature checks if the signature of content can be verified
|
// VerifySignature checks if the signature of content can be verified
|
||||||
// using publicKey.
|
// using publicKey.
|
||||||
// signature is expected to be base64 encoded.
|
// signature is expected to be base64 encoded.
|
||||||
// publicKey is expected to be PEM encoded.
|
// publicKey is expected to be PEM encoded.
|
||||||
func VerifySignature(content, signature, publicKey []byte) error {
|
func (CosignVerifier) VerifySignature(content, signature, publicKey []byte) error {
|
||||||
sigRaw := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(signature))
|
sigRaw := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(signature))
|
||||||
|
|
||||||
pubKeyRaw, err := cryptoutils.UnmarshalPEMToPublicKey(publicKey)
|
pubKeyRaw, err := cryptoutils.UnmarshalPEMToPublicKey(publicKey)
|
||||||
@ -42,3 +48,14 @@ func VerifySignature(content, signature, publicKey []byte) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
@ -60,7 +60,8 @@ gCDlEzkuOCybCHf+q766bve799L7Y5y5oRsHY1MrUCUwYF/tL7Sg7EYMsA==
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
err := VerifySignature(tc.content, tc.signature, tc.publicKey)
|
cosign := CosignVerifier{}
|
||||||
|
err := cosign.VerifySignature(tc.content, tc.signature, tc.publicKey)
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
return
|
return
|
||||||
|
Loading…
x
Reference in New Issue
Block a user