2022-10-11 07:57:52 -04:00
|
|
|
/*
|
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
2023-05-26 11:49:46 -04:00
|
|
|
// VerifyWithRekor checks if the hash of a signature is present in Rekor.
|
2023-08-01 10:48:13 -04:00
|
|
|
func VerifyWithRekor(ctx context.Context, publicKey []byte, verifier rekorVerifier, hash string) error {
|
2023-05-26 11:49:46 -04:00
|
|
|
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),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-10-11 07:57:52 -04:00
|
|
|
// 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
|
|
|
|
}
|
2023-05-26 11:49:46 -04:00
|
|
|
|
|
|
|
type rekorVerifier interface {
|
|
|
|
SearchByHash(context.Context, string) ([]string, error)
|
|
|
|
VerifyEntry(context.Context, string, string) error
|
|
|
|
}
|