mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-09-24 14:58:35 -04:00
Improve measurements verification with Rekor (#206)
Fetched measurements are now verified using Rekor in addition to a signature check. Signed-off-by: Fabian Kammel <fk@edgeless.systems>
This commit is contained in:
parent
1c29638421
commit
57b8efd1ec
18 changed files with 1320 additions and 322 deletions
196
internal/sigstore/rekor.go
Normal file
196
internal/sigstore/rekor.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package sigstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/sigstore/rekor/pkg/client"
|
||||
genclient "github.com/sigstore/rekor/pkg/generated/client"
|
||||
"github.com/sigstore/rekor/pkg/generated/client/entries"
|
||||
"github.com/sigstore/rekor/pkg/generated/client/index"
|
||||
"github.com/sigstore/rekor/pkg/generated/models"
|
||||
hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
|
||||
"github.com/sigstore/rekor/pkg/verify"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
)
|
||||
|
||||
// Rekor allows to interact with the transparency log at:
|
||||
// https://rekor.sigstore.dev
|
||||
// For more information see Rekor's Swagger definition:
|
||||
// https://www.sigstore.dev/swagger/#/
|
||||
type Rekor struct {
|
||||
client *genclient.Rekor
|
||||
}
|
||||
|
||||
// NewRekor creates a new instance of Rekor to interact with the transparency
|
||||
// log at: https://rekor.sigstore.dev
|
||||
func NewRekor() (*Rekor, error) {
|
||||
client, err := client.GetRekorClient("https://rekor.sigstore.dev")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Rekor{
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchByHash searches for the hash of an artifact in Rekor transparency log.
|
||||
// A list of UUIDs will be returned, since multiple entries could be present for
|
||||
// a single artifact in Rekor.
|
||||
func (r *Rekor) SearchByHash(ctx context.Context, hash string) ([]string, error) {
|
||||
params := index.NewSearchIndexParamsWithContext(ctx)
|
||||
params.SetQuery(
|
||||
&models.SearchIndex{
|
||||
Hash: hash,
|
||||
},
|
||||
)
|
||||
|
||||
index, err := r.client.Index.SearchIndex(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to search index: %w", err)
|
||||
}
|
||||
if !index.IsSuccess() {
|
||||
return nil, fmt.Errorf("search failed: %s", index.Error())
|
||||
}
|
||||
|
||||
return index.GetPayload(), nil
|
||||
}
|
||||
|
||||
// VerifyEntry performs log entry verification (see verifyLogEntry) and
|
||||
// verifies that the provided publicKey was used to sign the entry.
|
||||
// An error is returned if any verification fails.
|
||||
func (r *Rekor) VerifyEntry(ctx context.Context, uuid, publicKey string) error {
|
||||
entry, err := r.getEntry(ctx, uuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.verifyLogEntry(ctx, entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rekord, err := hashedRekordFromEntry(entry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("extracting rekord from Rekor entry: %w", err)
|
||||
}
|
||||
|
||||
if !isEntrySignedBy(rekord, publicKey) {
|
||||
return errors.New("rekord signed by unknown key")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEntry downloads entry for the provided UUID.
|
||||
func (r *Rekor) getEntry(ctx context.Context, uuid string) (models.LogEntryAnon, error) {
|
||||
params := entries.NewGetLogEntryByUUIDParamsWithContext(ctx)
|
||||
params.SetEntryUUID(uuid)
|
||||
|
||||
entry, err := r.client.Entries.GetLogEntryByUUID(params)
|
||||
if err != nil {
|
||||
return models.LogEntryAnon{}, fmt.Errorf("error getting entry: %w", err)
|
||||
}
|
||||
if !entry.IsSuccess() {
|
||||
return models.LogEntryAnon{}, fmt.Errorf("entries failure: %s", entry.Error())
|
||||
}
|
||||
|
||||
if entires := len(entry.GetPayload()); entires != 1 {
|
||||
return models.LogEntryAnon{}, fmt.Errorf("excepted 1 entry, but rekor returned %d", entires)
|
||||
}
|
||||
|
||||
for key := range entry.Payload {
|
||||
return entry.Payload[key], nil
|
||||
}
|
||||
|
||||
return models.LogEntryAnon{}, fmt.Errorf("no entry returned")
|
||||
}
|
||||
|
||||
// verifyLogEntry performs inclusion proof verification, SignedEntryTimestamp
|
||||
// verification, and checkpoint verification of the provided entry in Rekor.
|
||||
// A return value of nil indicates successful verification.
|
||||
func (r *Rekor) verifyLogEntry(ctx context.Context, entry models.LogEntryAnon) error {
|
||||
keyResp, err := r.client.Pubkey.GetPublicKey(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey := keyResp.Payload
|
||||
|
||||
block, _ := pem.Decode([]byte(publicKey))
|
||||
if block == nil {
|
||||
return errors.New("failed to decode key")
|
||||
}
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verifier, err := signature.LoadVerifier(pub, crypto.SHA256)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = verify.VerifyLogEntry(ctx, &entry, verifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashedRekordFromEntry extracts the base64 encoded polymorphic Body field
|
||||
// and unmarshals the contained JSON into the correct type.
|
||||
func hashedRekordFromEntry(entry models.LogEntryAnon) (*hashedrekord.V001Entry, error) {
|
||||
var rekord models.Hashedrekord
|
||||
|
||||
body, ok := entry.Body.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("body is not a string")
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(bytes.NewReader(decoded)).Decode(&rekord)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hashedRekord := &hashedrekord.V001Entry{}
|
||||
if err := hashedRekord.Unmarshal(&rekord); err != nil {
|
||||
return nil, errors.New("failed to unmarshal entry")
|
||||
}
|
||||
|
||||
return hashedRekord, nil
|
||||
}
|
||||
|
||||
// isEntrySignedBy checks whether rekord was signed with provided publicKey.
|
||||
func isEntrySignedBy(rekord *hashedrekord.V001Entry, publicKey string) bool {
|
||||
if rekord == nil {
|
||||
return false
|
||||
}
|
||||
if rekord.HashedRekordObj.Signature == nil {
|
||||
return false
|
||||
}
|
||||
if rekord.HashedRekordObj.Signature.PublicKey == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
actualKey := rekord.HashedRekordObj.Signature.PublicKey.Content.String()
|
||||
return actualKey == publicKey
|
||||
}
|
99
internal/sigstore/rekor_integration_test.go
Normal file
99
internal/sigstore/rekor_integration_test.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
//go:build integration
|
||||
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package sigstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m,
|
||||
// TODO: Remove once https://github.com/sigstore/rekor/issues/1094 resolved
|
||||
goleak.IgnoreTopFunction("internal/poll.runtime_pollWait"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestRekorSearchByHash(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
hash string
|
||||
wantEmpty bool
|
||||
}{
|
||||
"Constellation CLI v2.0.0 hash": {
|
||||
hash: "40e137b9b9b8204d672642fd1e181c6d5ccb50cfc5cc7fcbb06a8c2c78f44aff",
|
||||
},
|
||||
"other hash": {
|
||||
hash: "d9c5a43ba6284e1059b7e871bcf9b52f376d62b9198f300b1402d1c4d9b7431f",
|
||||
wantEmpty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
rekor, err := NewRekor()
|
||||
require.NoError(err)
|
||||
|
||||
uuids, err := rekor.SearchByHash(context.Background(), tc.hash)
|
||||
assert.NoError(err)
|
||||
|
||||
if tc.wantEmpty {
|
||||
assert.Empty(err)
|
||||
return
|
||||
}
|
||||
assert.NotEmpty(uuids)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEntry(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
uuid string
|
||||
pubKey string
|
||||
wantError bool
|
||||
}{
|
||||
"Constellation CLI v2.0.0": {
|
||||
uuid: "362f8ecba72f4326afaba7f6635b3e058888692841848e5514357315be9528474b23f5dcccb82b13",
|
||||
pubKey: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFZjhGMWhwbXdFK1lDRlh6akd0YVFjckw2WFpWVApKbUVlNWlTTHZHMVN5UVNBZXc3V2RNS0Y2bzl0OGUyVEZ1Q2t6bE9oaGx3czJPSFdiaUZabkZXQ0Z3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==",
|
||||
},
|
||||
"unknown uuid": {
|
||||
uuid: "46073a33852fc797ccc341a30323bd69119ff03936bf8d17061606e3e2e4be1fe70dccaa1b66bc34",
|
||||
pubKey: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFZjhGMWhwbXdFK1lDRlh6akd0YVFjckw2WFpWVApKbUVlNWlTTHZHMVN5UVNBZXc3V2RNS0Y2bzl0OGUyVEZ1Q2t6bE9oaGx3czJPSFdiaUZabkZXQ0Z3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==",
|
||||
wantError: true,
|
||||
},
|
||||
"broken key": {
|
||||
uuid: "362f8ecba72f4326afaba7f6635b3e058888692841848e5514357315be9528474b23f5dcccb82b13",
|
||||
pubKey: "d2VsbCB0aGlzIGlzIGRlZmluaXRlbHkgbm90IGEga2V5",
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
rekor, err := NewRekor()
|
||||
require.NoError(err)
|
||||
|
||||
err = rekor.VerifyEntry(context.Background(), tc.uuid, tc.pubKey)
|
||||
if tc.wantError {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
110
internal/sigstore/rekor_test.go
Normal file
110
internal/sigstore/rekor_test.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package sigstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sigstore/rekor/pkg/generated/models"
|
||||
hashedrekord "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsEntrySignedBy(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
entry *hashedrekord.V001Entry
|
||||
key string
|
||||
wantSuccess bool
|
||||
}{
|
||||
"valid key": {
|
||||
entry: &hashedrekord.V001Entry{
|
||||
HashedRekordObj: models.HashedrekordV001Schema{
|
||||
Signature: &models.HashedrekordV001SchemaSignature{
|
||||
PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{
|
||||
Content: []byte("my key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
key: "bXkga2V5", // "my key" in base64
|
||||
wantSuccess: true,
|
||||
},
|
||||
"nil rekord": {
|
||||
entry: nil,
|
||||
wantSuccess: false,
|
||||
},
|
||||
"nil signature": {
|
||||
entry: &hashedrekord.V001Entry{
|
||||
HashedRekordObj: models.HashedrekordV001Schema{
|
||||
Signature: nil,
|
||||
},
|
||||
},
|
||||
wantSuccess: false,
|
||||
},
|
||||
"nil pub key": {
|
||||
entry: &hashedrekord.V001Entry{
|
||||
HashedRekordObj: models.HashedrekordV001Schema{
|
||||
Signature: &models.HashedrekordV001SchemaSignature{
|
||||
PublicKey: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantSuccess: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.Equal(tc.wantSuccess, isEntrySignedBy(tc.entry, tc.key))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRekor(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
rekor, err := NewRekor()
|
||||
assert.NoError(err)
|
||||
assert.NotNil(rekor)
|
||||
}
|
||||
|
||||
func TestHashedRekordFromEntry(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
jsonEntry string
|
||||
wantError bool
|
||||
}{
|
||||
"invalid base64": {
|
||||
jsonEntry: "{\"body\":\"abc!\"}",
|
||||
wantError: true,
|
||||
},
|
||||
"valid base64, but invalid json": {
|
||||
jsonEntry: "{\"body\":\"aGVsbG8K\"}", // base64(hello)
|
||||
wantError: true,
|
||||
},
|
||||
"valid v001Entry": {
|
||||
jsonEntry: "{\"body\":\"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0MGUxMzdiOWI5YjgyMDRkNjcyNjQyZmQxZTE4MWM2ZDVjY2I1MGNmYzVjYzdmY2JiMDZhOGMyYzc4ZjQ0YWZmIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUNTRVIzbUdqK2o1UHIya09YVGxDSUhRQzNnVDMwSTdxa0xyOUF3dDZlVVVRSWdjTFVLUklsWTUwVU44Skd3VmVOZ2tCWnlZRDhITXh3Qy9MRlJXb01uMTgwPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGWmpoR01XaHdiWGRGSzFsRFJsaDZha2QwWVZGamNrdzJXRnBXVkFwS2JVVmxOV2xUVEhaSE1WTjVVVk5CWlhjM1YyUk5TMFkyYnpsME9HVXlWRVoxUTJ0NmJFOW9hR3gzY3pKUFNGZGlhVVphYmtaWFEwWjNQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=\"}", // base64("hello")
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
var entry models.LogEntryAnon
|
||||
err := entry.UnmarshalBinary([]byte(tc.jsonEntry))
|
||||
require.NoError(err)
|
||||
|
||||
_, err = hashedRekordFromEntry(entry)
|
||||
if tc.wantError {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -10,13 +10,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestVerifySignature(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
content []byte
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue