refactor: consolidate verification funcs for GHA (#348)

* consolidate verification funcs

Signed-off-by: Asra Ali <asraa@google.com>
This commit is contained in:
asraa
2022-11-02 15:32:39 -05:00
committed by GitHub
parent 26f422b6b2
commit ef0f1a7a24
6 changed files with 158 additions and 216 deletions

View File

@@ -46,4 +46,4 @@ jobs:
go build -mod=vendor -o service ./cli/experimental/service/
# Tests
go test -mod=vendor -v ./...
go test -mod=vendor -v -timeout=20m ./...

View File

@@ -401,7 +401,7 @@ func Test_runVerifyGHAArtifactPath(t *testing.T) {
name: "rekor upload bypassed",
artifact: "binary-linux-amd64-no-tlog-upload",
source: "github.com/slsa-framework/example-package",
err: serrors.ErrorNoValidRekorEntries,
err: serrors.ErrorRekorSearch,
noversion: true,
},
{
@@ -415,7 +415,7 @@ func Test_runVerifyGHAArtifactPath(t *testing.T) {
name: "malicious: invalid signature expired certificate",
artifact: "binary-linux-amd64-expired-cert",
source: "github.com/slsa-framework/example-package",
err: serrors.ErrorNoValidRekorEntries,
err: serrors.ErrorRekorSearch,
noversion: true,
},
// Annotated tags.

View File

@@ -14,11 +14,24 @@ import (
intoto "github.com/in-toto/in-toto-golang/in_toto"
dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/slsa-framework/slsa-github-generator/signing/envelope"
serrors "github.com/slsa-framework/slsa-verifier/errors"
"github.com/slsa-framework/slsa-verifier/options"
)
// SignedAttestation contains a signed DSSE envelope
// and its associated signing certificate.
type SignedAttestation struct {
// The signed DSSE envelope
Envelope *dsselib.Envelope
// The signing certificate
SigningCert *x509.Certificate
// The associated verified Rekor entry
RekorEntry *models.LogEntryAnon
}
func EnvelopeFromBytes(payload []byte) (env *dsselib.Envelope, err error) {
env = &dsselib.Envelope{}
err = json.Unmarshal(payload, env)
@@ -150,32 +163,26 @@ func verifySha256Digest(prov *intoto.ProvenanceStatement, expectedHash string) e
// VerifyProvenanceSignature returns the verified DSSE envelope containing the provenance
// and the signing certificate given the provenance and artifact hash.
func VerifyProvenanceSignature(ctx context.Context, rClient *client.Rekor, provenance []byte, artifactHash string) (*dsselib.Envelope, *x509.Certificate, error) {
// Get Rekor entries corresponding to provenance
env, cert, err := GetRekorEntriesWithCert(rClient, provenance)
if err == nil {
return env, cert, nil
func VerifyProvenanceSignature(ctx context.Context, rClient *client.Rekor,
provenance []byte, artifactHash string) (
*SignedAttestation, error) {
// There are two cases, either we have an embedded certificate, or we need
// to use the Redis index for searching by artifact SHA.
if hasCertInEnvelope(provenance) {
// Get Rekor entries corresponding to provenance
return GetValidSignedAttestationWithCert(rClient, provenance)
}
// Fallback on using the redis search index to get matching UUIDs.
fmt.Fprintf(os.Stderr, "Getting rekor entry error %s, trying Redis search index to find entries by subject digest\n", err)
uuids, err := GetRekorEntries(rClient, artifactHash)
if err != nil {
return nil, nil, err
}
env, err = EnvelopeFromBytes(provenance)
if err != nil {
return nil, nil, err
}
fmt.Fprintf(os.Stderr, "No certificate provided, trying Redis search index to find entries by subject digest\n")
// Verify the provenance and return the signing certificate.
cert, err = FindSigningCertificate(ctx, uuids, *env, rClient)
signedAttestation, err := SearchValidSignedAttestation(ctx, artifactHash, provenance, rClient)
if err != nil {
return nil, nil, err
return nil, err
}
return env, cert, nil
return signedAttestation, nil
}
func VerifyProvenance(env *dsselib.Envelope, provenanceOpts *options.ProvenanceOpts) error {
@@ -556,3 +563,10 @@ func getBranch(prov *intoto.ProvenanceStatement) (string, error) {
"unknown ref type", refType)
}
}
// hasCertInEnvelope checks if a valid x509 certificate is present in the
// envelope.
func hasCertInEnvelope(provenance []byte) bool {
certPem, err := envelope.GetCertFromEnvelope(provenance)
return err == nil && len(certPem) > 0
}

View File

@@ -4,10 +4,8 @@ import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"os"
@@ -18,25 +16,20 @@ import (
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/bundle"
"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/client/tlog"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/rekor/pkg/sharding"
"github.com/sigstore/rekor/pkg/types"
intotod "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1"
"github.com/sigstore/rekor/pkg/util"
rverify "github.com/sigstore/rekor/pkg/verify"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/dsse"
"github.com/slsa-framework/slsa-github-generator/signing/envelope"
"github.com/transparency-dev/merkle/proof"
"github.com/transparency-dev/merkle/rfc6962"
serrors "github.com/slsa-framework/slsa-verifier/errors"
)
@@ -45,75 +38,8 @@ const (
defaultRekorAddr = "https://rekor.sigstore.dev"
)
func verifyRootHash(ctx context.Context, rekorClient *client.Rekor,
treeID int64, eproof *models.InclusionProof, pub *ecdsa.PublicKey) error {
treeIDString := fmt.Sprintf("%d", treeID)
infoParams := tlog.NewGetLogInfoParamsWithContext(ctx)
result, err := rekorClient.Tlog.GetLogInfo(infoParams)
if err != nil {
return err
}
logInfo := result.GetPayload()
sth := util.SignedCheckpoint{}
if err := sth.UnmarshalText([]byte(*logInfo.SignedTreeHead)); err != nil {
return err
}
for _, inactiveShard := range logInfo.InactiveShards {
if *inactiveShard.TreeID == treeIDString {
if err := sth.UnmarshalText([]byte(*inactiveShard.SignedTreeHead)); err != nil {
return err
}
}
}
verifier, err := signature.LoadVerifier(pub, crypto.SHA256)
if err != nil {
return err
}
if !sth.Verify(verifier) {
return errors.New("signature on tree head did not verify")
}
rootHash, err := hex.DecodeString(*eproof.RootHash)
if err != nil {
return errors.New("error decoding root hash in inclusion proof")
}
if *eproof.TreeSize == int64(sth.Size) {
if !bytes.Equal(rootHash, sth.Hash) {
return errors.New("root hash returned from server does not match inclusion proof hash")
}
} else if *eproof.TreeSize < int64(sth.Size) {
consistencyParams := tlog.NewGetLogProofParamsWithContext(ctx)
consistencyParams.FirstSize = eproof.TreeSize // Root hash at the time the proof was returned
consistencyParams.LastSize = int64(sth.Size) // Root hash verified with rekor pubkey
consistencyProof, err := rekorClient.Tlog.GetLogProof(consistencyParams)
if err != nil {
return err
}
var hashes [][]byte
for _, h := range consistencyProof.Payload.Hashes {
b, err := hex.DecodeString(h)
if err != nil {
return errors.New("error decoding consistency proof hashes")
}
hashes = append(hashes, b)
}
if err := proof.VerifyConsistency(rfc6962.DefaultHasher,
uint64(*eproof.TreeSize), sth.Size, hashes, rootHash, sth.Hash); err != nil {
return err
}
} else if *eproof.TreeSize > int64(sth.Size) {
return errors.New("inclusion proof returned a tree size larger than the verified tree size")
}
return nil
}
func verifyTlogEntryByUUID(ctx context.Context, rekorClient *client.Rekor, entryUUID string) (*models.LogEntryAnon, error) {
func verifyTlogEntryByUUID(ctx context.Context, rekorClient *client.Rekor, entryUUID string) (
*models.LogEntryAnon, error) {
params := entries.NewGetLogEntryByUUIDParamsWithContext(ctx)
params.EntryUUID = entryUUID
@@ -140,89 +66,33 @@ func verifyTlogEntryByUUID(ctx context.Context, rekorClient *client.Rekor, entry
if returnUUID != uuid {
return nil, errors.New("expected matching UUID")
}
return verifyTlogEntry(ctx, rekorClient, k, entry)
// Validate the entry response.
return verifyTlogEntry(ctx, rekorClient, entry)
}
return nil, serrors.ErrorRekorSearch
}
func verifyTlogEntry(ctx context.Context, rekorClient *client.Rekor,
entryUUID string, e models.LogEntryAnon) (*models.LogEntryAnon, error) {
if e.Verification == nil || e.Verification.InclusionProof == nil {
return nil, errors.New("inclusion proof not provided")
}
uuid, err := sharding.GetUUIDFromIDString(entryUUID)
if err != nil {
return nil, fmt.Errorf("%w: retrieving uuid from entry uuid", err)
}
treeID, err := sharding.TreeID(entryUUID)
if err != nil {
return nil, fmt.Errorf("%w: retrieving tree ID", err)
}
var hashes [][]byte
for _, h := range e.Verification.InclusionProof.Hashes {
hb, err := hex.DecodeString(h)
if err != nil {
return nil, errors.New("error decoding inclusion proof hashes")
}
hashes = append(hashes, hb)
}
rootHash, err := hex.DecodeString(*e.Verification.InclusionProof.RootHash)
if err != nil {
return nil, errors.New("error decoding hex encoded root hash")
}
leafHash, err := hex.DecodeString(uuid)
if err != nil {
return nil, errors.New("error decoding hex encoded leaf hash")
}
func verifyTlogEntry(ctx context.Context, rekorClient *client.Rekor, e models.LogEntryAnon) (
*models.LogEntryAnon, error) {
// Verify the root hash against the current Signed Entry Tree Head
pubs, err := cosign.GetRekorPubs(ctx, nil)
if err != nil {
return nil, fmt.Errorf("%w: %s", err, "unable to fetch Rekor public keys from TUF repository")
}
var entryVerError error
for _, pubKey := range pubs {
// Verify inclusion against the signed tree head
entryVerError = verifyRootHash(ctx, rekorClient, treeID,
e.Verification.InclusionProof, pubKey.PubKey)
if entryVerError == nil {
break
}
}
if entryVerError != nil {
return nil, fmt.Errorf("%w: %s", entryVerError, "error verifying root hash")
verifier, err := signature.LoadECDSAVerifier(pubs[*e.LogID].PubKey, crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("%w: %s", err, "unable to fetch Rekor public keys from TUF repository")
}
// Verify the entry's inclusion
if err := proof.VerifyInclusion(rfc6962.DefaultHasher,
uint64(*e.Verification.InclusionProof.LogIndex),
uint64(*e.Verification.InclusionProof.TreeSize), leafHash, hashes, rootHash); err != nil {
return nil, fmt.Errorf("%w: %s", err, "verifying inclusion proof")
// This function verifies the inclusion proof, the signature on the root hash of the
// inclusion proof, and the SignedEntryTimestamp.
if err := rverify.VerifyLogEntry(ctx, &e, verifier); err != nil {
return nil, fmt.Errorf("%w: %s", err, "unable to fetch Rekor public keys from TUF repository")
}
// Verify rekor's signature over the SET.
payload := bundle.RekorPayload{
Body: e.Body,
IntegratedTime: *e.IntegratedTime,
LogIndex: *e.LogIndex,
LogID: *e.LogID,
}
var setVerError error
for _, pubKey := range pubs {
setVerError = cosign.VerifySET(payload, e.Verification.SignedEntryTimestamp, pubKey.PubKey)
// Return once the SET is verified successfully.
if setVerError == nil {
break
}
}
return &e, setVerError
return &e, nil
}
func extractCert(e *models.LogEntryAnon) (*x509.Certificate, error) {
@@ -284,8 +154,8 @@ func intotoEntry(certPem []byte, provenance []byte) (*intotod.V001Entry, error)
}, nil
}
// GetRekorEntries finds all entry UUIDs by the digest of the artifact binary.
func GetRekorEntries(rClient *client.Rekor, artifactHash string) ([]string, error) {
// getUUIDsByArtifactDigest finds all entry UUIDs by the digest of the artifact binary.
func getUUIDsByArtifactDigest(rClient *client.Rekor, artifactHash string) ([]string, error) {
// Use search index to find rekor entry UUIDs that match Subject Digest.
params := index.NewSearchIndexParams()
params.Query = &models.SearchIndex{Hash: fmt.Sprintf("sha256:%v", artifactHash)}
@@ -301,20 +171,21 @@ func GetRekorEntries(rClient *client.Rekor, artifactHash string) ([]string, erro
return resp.GetPayload(), nil
}
// GetRekorEntriesWithCert finds all entry UUIDs with the full intoto attestation.
// GetValidSignedAttestationWithCert finds and validates the matching entry UUIDs with
// the full intoto attestation.
// The attestation generated by the slsa-github-generator libraries contain a signing certificate.
func GetRekorEntriesWithCert(rClient *client.Rekor, provenance []byte) (*dsselib.Envelope, *x509.Certificate, error) {
func GetValidSignedAttestationWithCert(rClient *client.Rekor, provenance []byte) (*SignedAttestation, error) {
// Use intoto attestation to find rekor entry UUIDs.
params := entries.NewSearchLogQueryParams()
searchLogQuery := models.SearchLogQuery{}
certPem, err := envelope.GetCertFromEnvelope(provenance)
if err != nil {
return nil, nil, fmt.Errorf("error getting certificate from provenance: %w", err)
return nil, fmt.Errorf("error getting certificate from provenance: %w", err)
}
e, err := intotoEntry(certPem, provenance)
if err != nil {
return nil, nil, fmt.Errorf("error creating intoto entry: %w", err)
return nil, fmt.Errorf("error creating intoto entry: %w", err)
}
entry := models.Intoto{
APIVersion: swag.String(e.APIVersion()),
@@ -326,41 +197,61 @@ func GetRekorEntriesWithCert(rClient *client.Rekor, provenance []byte) (*dsselib
params.SetEntry(&searchLogQuery)
resp, err := rClient.Entries.SearchLogQuery(params)
if err != nil {
return nil, nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, err.Error())
return nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, err.Error())
}
if len(resp.GetPayload()) != 1 {
return nil, nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, "no matching rekor entries")
return nil, fmt.Errorf("%w: %s", serrors.ErrorRekorSearch, "no matching rekor entries")
}
logEntry := resp.Payload[0]
var rekorEntry models.LogEntryAnon
for uuid, e := range logEntry {
if _, err := verifyTlogEntry(context.Background(), rClient, uuid, e); err != nil {
return nil, nil, fmt.Errorf("error verifying tlog entry: %w", err)
if _, err := verifyTlogEntry(context.Background(), rClient, e); err != nil {
return nil, fmt.Errorf("error verifying tlog entry: %w", err)
}
rekorEntry = e
url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", uuid)
fmt.Fprintf(os.Stderr, "Verified signature against tlog entry index %d at URL: %s\n", *e.LogIndex, url)
}
env, err := EnvelopeFromBytes(provenance)
if err != nil {
return nil, nil, err
}
certs, err := cryptoutils.UnmarshalCertificatesFromPEM(certPem)
if err != nil {
return nil, nil, err
return nil, err
}
if len(certs) != 1 {
return nil, nil, fmt.Errorf("error unmarshaling certificate from pem")
return nil, fmt.Errorf("error unmarshaling certificate from pem")
}
return env, certs[0], nil
env, err := EnvelopeFromBytes(provenance)
if err != nil {
return nil, err
}
proposedSignedAtt := &SignedAttestation{
SigningCert: certs[0],
Envelope: env,
RekorEntry: &rekorEntry,
}
if err := verifySignedAttestation(proposedSignedAtt); err != nil {
return nil, err
}
return proposedSignedAtt, nil
}
// FindSigningCertificate finds and verifies a matching signing certificate from a list of Rekor entry UUIDs.
func FindSigningCertificate(ctx context.Context, uuids []string, dssePayload dsselib.Envelope, rClient *client.Rekor) (*x509.Certificate, error) {
attBytes, err := cjson.MarshalCanonical(dssePayload)
// SearchValidSignedAttestation searches for a valid signing certificate using the Rekor
// Redis search index by using the artifact digest.
func SearchValidSignedAttestation(ctx context.Context, artifactHash string, provenance []byte,
rClient *client.Rekor) (*SignedAttestation, error) {
// Get Rekor UUIDs by artifact digest.
uuids, err := getUUIDsByArtifactDigest(rClient, artifactHash)
if err != nil {
return nil, err
}
env, err := EnvelopeFromBytes(provenance)
if err != nil {
return nil, err
}
@@ -379,6 +270,7 @@ func FindSigningCertificate(ctx context.Context, uuids []string, dssePayload dss
errs = append(errs, fmt.Sprintf("%s: verifying tlog entry %s", err, uuid))
continue
}
cert, err := extractCert(entry)
if err != nil {
// this is unexpected, hold on to this error.
@@ -386,39 +278,73 @@ func FindSigningCertificate(ctx context.Context, uuids []string, dssePayload dss
continue
}
roots, err := fulcio.GetRoots()
if err != nil {
// this is unexpected, hold on to this error.
errs = append(errs, fmt.Sprintf("%s: retrieving fulcio root", err))
continue
proposedSignedAtt := &SignedAttestation{
Envelope: env,
SigningCert: cert,
RekorEntry: entry,
}
co := &cosign.CheckOpts{
RootCerts: roots,
CertOidcIssuer: certOidcIssuer,
}
verifier, err := cosign.ValidateAndUnpackCert(cert, co)
if err != nil {
continue
}
verifier = dsse.WrapVerifier(verifier)
if err := verifier.VerifySignature(bytes.NewReader(attBytes), bytes.NewReader(attBytes)); err != nil {
continue
}
it := time.Unix(*entry.IntegratedTime, 0)
if err := cosign.CheckExpiry(cert, it); err != nil {
continue
}
uuid, err := cosign.ComputeLeafHash(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Error computing leaf hash for tlog entry at index: %d\n", *entry.LogIndex)
err = verifySignedAttestation(proposedSignedAtt)
if errors.Is(err, serrors.ErrorInternal) {
// Return on an internal error
return nil, err
} else if err != nil {
errs = append(errs, err.Error())
continue
}
// success!
url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", hex.EncodeToString(uuid))
url := fmt.Sprintf("%v/%v/%v", defaultRekorAddr, "api/v1/log/entries", uuid)
fmt.Fprintf(os.Stderr, "Verified signature against tlog entry index %d at URL: %s\n", *entry.LogIndex, url)
return cert, nil
return proposedSignedAtt, nil
}
return nil, fmt.Errorf("%w: got unexpected errors %s", serrors.ErrorNoValidRekorEntries, strings.Join(errs, ", "))
}
// verifyAttestationSignature validates the signature on the attestation
// given a certificate and a validated signature time.
// The certificate is verified up to Fulcio, the signature is validated
// using the certificate, and the signature generation time is checked
// to be within the certificate validity period.
func verifySignedAttestation(signedAtt *SignedAttestation) error {
cert := signedAtt.SigningCert
attBytes, err := cjson.MarshalCanonical(signedAtt.Envelope)
if err != nil {
return err
}
signatureTimestamp := time.Unix(*signedAtt.RekorEntry.IntegratedTime, 0)
// 1. Verify certificate chain.
roots, err := fulcio.GetRoots()
if err != nil {
// this is unexpected, hold on to this error.
return fmt.Errorf("%w: %s", serrors.ErrorInternal, err)
}
intermediates, err := fulcio.GetIntermediates()
if err != nil {
// this is unexpected, hold on to this error.
return fmt.Errorf("%w: %s", serrors.ErrorInternal, err)
}
co := &cosign.CheckOpts{
RootCerts: roots,
IntermediateCerts: intermediates,
CertOidcIssuer: certOidcIssuer,
}
verifier, err := cosign.ValidateAndUnpackCert(signedAtt.SigningCert, co)
if err != nil {
return fmt.Errorf("%w: %s", serrors.ErrorInvalidSignature, err)
}
// 2. Verify signature using validated certificate.
verifier = dsse.WrapVerifier(verifier)
if err := verifier.VerifySignature(bytes.NewReader(attBytes), bytes.NewReader(attBytes)); err != nil {
return fmt.Errorf("%w: %s", serrors.ErrorInvalidSignature, err)
}
// 3. Verify signature was creating during certificate validity period.
if err := cosign.CheckExpiry(cert, signatureTimestamp); err != nil {
return fmt.Errorf("%w: %s", serrors.ErrorInvalidSignature, err)
}
return nil
}

View File

@@ -81,7 +81,7 @@ func Test_GetRekorEntries(t *testing.T) {
var mClient client.Rekor
mClient.Index = &MockIndexClient{result: tt.res}
_, err := GetRekorEntries(&mClient, tt.artifactHash)
_, err := getUUIDsByArtifactDigest(&mClient, tt.artifactHash)
if !errCmp(err, tt.expected) {
t.Errorf(cmp.Diff(err, tt.expected))
}

View File

@@ -92,12 +92,14 @@ func (v *GHAVerifier) VerifyArtifact(ctx context.Context,
}
/* Verify signature on the intoto attestation. */
env, cert, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash)
// TODO(https://github.com/slsa-framework/slsa-github-generator/issues/716):
// We will also need to support bundles when those are complete.
signedAtt, err := VerifyProvenanceSignature(ctx, rClient, provenance, artifactHash)
if err != nil {
return nil, nil, err
}
return verifyEnvAndCert(env, cert,
return verifyEnvAndCert(signedAtt.Envelope, signedAtt.SigningCert,
provenanceOpts, builderOpts,
defaultArtifactTrustedReusableWorkflows)
}