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:
Fabian Kammel 2022-10-11 13:57:52 +02:00 committed by GitHub
parent 1c29638421
commit 57b8efd1ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1320 additions and 322 deletions

196
internal/sigstore/rekor.go Normal file
View 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
}

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

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

View file

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