Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tuf: improve TUF client concurrency and caching #1953

Merged
merged 5 commits into from
Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ func initRoots() (*x509.CertPool, *x509.CertPool, error) {
if err != nil {
return nil, nil, fmt.Errorf("initializing tuf: %w", err)
}
defer tufClient.Close()
// Retrieve from the embedded or cached TUF root. If expired, a network
// call is made to update the root.
targets, err := tufClient.GetTargetsByMeta(tuf.Fulcio, []string{fulcioTargetStr, fulcioV1TargetStr})
Expand Down
1 change: 0 additions & 1 deletion cmd/cosign/cli/fulcio/fulcioverifier/ctl/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ func VerifySCT(ctx context.Context, certPEM, chainPEM, rawSCT []byte) error {
if err != nil {
return err
}
defer tufClient.Close()

targets, err := tufClient.GetTargetsByMeta(tuf.CTFE, []string{ctPublicKeyStr})
if err != nil {
Expand Down
17 changes: 10 additions & 7 deletions cmd/cosign/cli/verify/verify_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,16 @@ func payloadBytes(blobRef string) ([]byte, error) {
}

func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntryAnon, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error {
// TODO: This can be moved below offline bundle verification when SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY
// is removed.
rekorClient, err := rekor.NewClient(ko.RekorURL)
if err != nil {
return err
}

// If we have a bundle with a rekor entry, let's first try to verify offline
if ko.BundlePath != "" {
if err := verifyRekorBundle(ctx, ko.BundlePath, cert); err == nil {
if err := verifyRekorBundle(ctx, ko.BundlePath, cert, rekorClient); err == nil {
fmt.Fprintf(os.Stderr, "tlog entry verified offline\n")
return nil
}
Expand All @@ -304,10 +311,6 @@ func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntr
return nil
}

rekorClient, err := rekor.NewClient(ko.RekorURL)
if err != nil {
return err
}
// Only fetch from rekor tlog if we don't already have the entry.
if e == nil {
var pubBytes []byte
Expand Down Expand Up @@ -346,15 +349,15 @@ func verifyRekorEntry(ctx context.Context, ko options.KeyOpts, e *models.LogEntr
return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0))
}

func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate) error {
func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate, rekorClient *client.Rekor) error {
b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
if err != nil {
return err
}
if b.Bundle == nil {
return fmt.Errorf("rekor entry is not available")
}
publicKeys, err := cosign.GetRekorPubs(ctx)
publicKeys, err := cosign.GetRekorPubs(ctx, rekorClient)
if err != nil {
return fmt.Errorf("retrieving rekor public key: %w", err)
}
Expand Down
64 changes: 30 additions & 34 deletions pkg/cosign/tlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,15 @@ func getLogID(pub crypto.PublicKey) (string, error) {

// GetRekorPubs retrieves trusted Rekor public keys from the embedded or cached
// TUF root. If expired, makes a network call to retrieve the updated targets.
// There is an Env variable that can be used to override this behaviour:
// A Rekor client may optionally be provided in case using SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY
// (see below).
// There are two Env variable that can be used to override this behaviour:
// SIGSTORE_REKOR_PUBLIC_KEY - If specified, location of the file that contains
// the Rekor Public Key on local filesystem
func GetRekorPubs(ctx context.Context) (map[string]RekorPubKey, error) {
// SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY - If specified, fetches the Rekor public
// key from the Rekor server using the provided rekorClient.
// TODO: Rename SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY to be test-only or remove.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd say remove entirely - Ultimately this will get used outside of testing, and at least with the other env var, the file is provided out of band rather than fetching the key directly from the source.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thought here is that being able to fetch the public key from Rekor is part of the Rekor API, and whether this flag is here or not is only one part of getting used 'outside of testing'.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s fine to provide an API to fetch the public key, but there needs to be an explicit strategy around root of trust management. Ideally it’d always be TUF for Sigstore, but consumers may choose to TOFU - but they must persist the key, verification logic can’t rely on fetching the key each time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree it should be removed -- but I'd like to do this in another PR because the current webhook and tests rely on it and I need to dig in separately into that.

func GetRekorPubs(ctx context.Context, rekorClient *client.Rekor) (map[string]RekorPubKey, error) {
publicKeys := make(map[string]RekorPubKey)
altRekorPub := os.Getenv(altRekorPublicKey)

Expand All @@ -104,7 +109,6 @@ func GetRekorPubs(ctx context.Context) (map[string]RekorPubKey, error) {
if err != nil {
return nil, err
}
defer tufClient.Close()
targets, err := tufClient.GetTargetsByMeta(tuf.Rekor, []string{rekorTargetStr})
if err != nil {
return nil, err
Expand All @@ -121,6 +125,26 @@ func GetRekorPubs(ctx context.Context) (map[string]RekorPubKey, error) {
publicKeys[keyID] = RekorPubKey{PubKey: rekorPubKey, Status: t.Status}
}
}

// If we have a Rekor client and we've been told to fetch the Public Key from Rekor,
// additionally fetch it here.
addRekorPublic := os.Getenv(addRekorPublicKeyFromRekor)
if addRekorPublic != "" && rekorClient != nil {
pubOK, err := rekorClient.Pubkey.GetPublicKey(nil)
if err != nil {
return nil, fmt.Errorf("unable to fetch rekor public key from rekor: %w", err)
}
pubFromAPI, err := PemToECDSAKey([]byte(pubOK.Payload))
if err != nil {
return nil, fmt.Errorf("error converting rekor PEM public key from rekor to ECDSAKey: %w", err)
}
keyID, err := getLogID(pubFromAPI)
if err != nil {
return nil, fmt.Errorf("error generating log ID: %w", err)
}
publicKeys[keyID] = RekorPubKey{PubKey: pubFromAPI, Status: tuf.Active}
}

if len(publicKeys) == 0 {
return nil, errors.New("none of the Rekor public keys have been found")
}
Expand Down Expand Up @@ -330,9 +354,7 @@ func FindTLogEntriesByPayload(ctx context.Context, rekorClient *client.Rekor, pa
return searchIndex.GetPayload(), nil
}

// VerityTLogEntry verifies a TLog entry. If the Env variable
// SIGSTORE_TRUST_REKOR_API_PUBLIC_KEY is specified, fetches the Rekor public
// key from the Rekor server using the provided rekorClient.
// VerityTLogEntry verifies a TLog entry.
func VerifyTLogEntry(ctx context.Context, rekorClient *client.Rekor, e *models.LogEntryAnon) error {
if e.Verification == nil || e.Verification.InclusionProof == nil {
return errors.New("inclusion proof not provided")
Expand Down Expand Up @@ -365,37 +387,11 @@ func VerifyTLogEntry(ctx context.Context, rekorClient *client.Rekor, e *models.L
LogID: *e.LogID,
}

// If we've been told to fetch the Public Key from Rekor, fetch it here
// first before using the TUF code below.
rekorPubKeys := make(map[string]RekorPubKey)
addRekorPublic := os.Getenv(addRekorPublicKeyFromRekor)
if addRekorPublic != "" {
pubOK, err := rekorClient.Pubkey.GetPublicKey(nil)
if err != nil {
return fmt.Errorf("unable to fetch rekor public key from rekor: %w", err)
}
pubFromAPI, err := PemToECDSAKey([]byte(pubOK.Payload))
if err != nil {
return fmt.Errorf("error converting rekor PEM public key from rekor to ECDSAKey: %w", err)
}
keyID, err := getLogID(pubFromAPI)
if err != nil {
return fmt.Errorf("error generating log ID: %w", err)
}
rekorPubKeys[keyID] = RekorPubKey{PubKey: pubFromAPI, Status: tuf.Active}
}

rekorPubKeysTuf, err := GetRekorPubs(ctx)
rekorPubKeys, err := GetRekorPubs(ctx, rekorClient)
if err != nil {
if len(rekorPubKeys) == 0 {
return fmt.Errorf("unable to fetch Rekor public keys from TUF repository, and not trusting the Rekor API for fetching public keys: %w", err)
}
fmt.Fprintf(os.Stderr, "**Warning** Failed to fetch Rekor public keys from TUF, using the public key from Rekor API because %s was specified", addRekorPublicKeyFromRekor)
return fmt.Errorf("unable to fetch Rekor public keys: %w", err)
}

for k, v := range rekorPubKeysTuf {
rekorPubKeys[k] = v
}
pubKey, ok := rekorPubKeys[payload.LogID]
if !ok {
return errors.New("rekor log public key not found for payload")
Expand Down
2 changes: 1 addition & 1 deletion pkg/cosign/tlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
)

func TestGetRekorPubKeys(t *testing.T) {
keys, err := GetRekorPubs(context.Background())
keys, err := GetRekorPubs(context.Background(), nil)
if err != nil {
t.Errorf("Unexpected error calling GetRekorPubs, expected nil: %v", err)
}
Expand Down
Loading