diff --git a/.github/workflows/whitespace.yaml b/.github/workflows/whitespace.yaml index ae200adfa3e..8c7ce8b5b2c 100644 --- a/.github/workflows/whitespace.yaml +++ b/.github/workflows/whitespace.yaml @@ -74,7 +74,7 @@ jobs: LINT_FILES=$(git ls-files | git check-attr --stdin linguist-generated | grep -Ev ': (set|true)$' | cut -d: -f1 | git check-attr --stdin linguist-vendored | grep -Ev ': (set|true)$' | cut -d: -f1 | - grep -Ev '^(vendor/|third_party/|.git)' | + grep -Ev '^(vendor/|third_party/|.git|pkg/cosign/tuf/repository/targets/)' | grep -v '\.ai$') for x in $LINT_FILES; do # Based on https://stackoverflow.com/questions/34943632/linux-check-if-there-is-an-empty-line-at-the-end-of-a-file diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcio.pem b/cmd/cosign/cli/fulcio/fulcioroots/fulcio.pem deleted file mode 100644 index 6a06ff300bc..00000000000 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcio.pem +++ /dev/null @@ -1,13 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq -MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx -MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu -ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy -A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas -taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm -MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE -FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u -Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx -Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup -Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ== ------END CERTIFICATE----- \ No newline at end of file diff --git a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go index f3b1ae2e31a..4ff6201fab5 100644 --- a/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go +++ b/cmd/cosign/cli/fulcio/fulcioroots/fulcioroots.go @@ -25,8 +25,7 @@ import ( "strings" "sync" - _ "embed" // To enable the `go:embed` directive. - + "github.com/pkg/errors" "github.com/sigstore/cosign/pkg/cosign/tuf" ) @@ -36,9 +35,6 @@ var ( ) // This is the root in the fulcio project. -//go:embed fulcio.pem -var rootPem string - var fulcioTargetStr = `fulcio.crt.pem` const ( @@ -64,22 +60,17 @@ func initRoots() *x509.CertPool { panic("error creating root cert pool") } } else { - // First try retrieving from TUF root. Otherwise use rootPem. + // Retrieve from the embedded or cached TUF root. If expired, a network + // call is made to update the root. ctx := context.Background() // TODO: pass in context? buf := tuf.ByteDestination{Buffer: &bytes.Buffer{}} - err := tuf.GetTarget(ctx, fulcioTargetStr, &buf) - if err != nil { - // The user may not have initialized the local root metadata. Log the error and use the embedded root. - fmt.Fprintln(os.Stderr, "No TUF root installed, using embedded CA certificate.") - if !cp.AppendCertsFromPEM([]byte(rootPem)) { - panic("error creating root cert pool") - } - } else { - // TODO: Remove the string replace when SigStore root is updated. - replaced := strings.ReplaceAll(buf.String(), "\n ", "\n") - if !cp.AppendCertsFromPEM([]byte(replaced)) { - panic("error creating root cert pool") - } + if err := tuf.GetTarget(ctx, fulcioTargetStr, &buf); err != nil { + panic(errors.Wrap(err, "creating root cert pool")) + } + // TODO: Remove the string replace when SigStore root is updated. + replaced := strings.ReplaceAll(buf.String(), "\n ", "\n") + if !cp.AppendCertsFromPEM([]byte(replaced)) { + panic("error creating root cert pool") } } return cp diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index 9391afa0ff6..33b07d98759 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -16,8 +16,8 @@ package fulcioverifier import ( + "bytes" "context" - _ "embed" // To enable the `go:embed` directive. "encoding/json" "fmt" "os" @@ -30,19 +30,31 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/tuf" fulcioClient "github.com/sigstore/fulcio/pkg/generated/client" ) -// This is the CT log public key -//go:embed ctfe.pub -var ctPublicKey string +// This is the CT log public key target name +var ctPublicKeyStr = `ctfe.pub` + +func getCTPub() string { + ctx := context.Background() // TODO: pass in context? + buf := tuf.ByteDestination{Buffer: &bytes.Buffer{}} + // Retrieves the CT public key from the embedded or cached TUF root. If expired, makes a + // network call to retrieve the updated target. + if err := tuf.GetTarget(ctx, ctPublicKeyStr, &buf); err != nil { + fmt.Fprintln(os.Stderr, err) + panic("error retrieving CT public key") + } + return buf.String() +} // verifySCT verifies the SCT against the Fulcio CT log public key // The SCT is a `Signed Certificate Timestamp`, which promises that // the certificate issued by Fulcio was also added to the public CT log within // some defined time period func verifySCT(certPEM, rawSCT []byte) error { - pubKey, err := cosign.PemToECDSAKey([]byte(ctPublicKey)) + pubKey, err := cosign.PemToECDSAKey([]byte(getCTPub())) if err != nil { return err } diff --git a/cmd/cosign/cli/initialize.go b/cmd/cosign/cli/initialize.go index cf7b241e04b..1ed6ff9b61b 100644 --- a/cmd/cosign/cli/initialize.go +++ b/cmd/cosign/cli/initialize.go @@ -31,26 +31,27 @@ func addInitialize(topLevel *cobra.Command) { Long: `Initializes SigStore root to retrieve trusted certificate and key targets for verification. The following options are used by default: - - The initial 1.root.json is embedded inside cosign. - - SigStore current TUF repository is pulled from the GCS mirror at sigstore-tuf-root. + - The current trusted Sigstore TUF root is embedded inside cosign at the time of release. + - SigStore remote TUF repository is pulled from the GCS mirror at sigstore-tuf-root. - A default threshold of 3 root signatures is used. To provide an out-of-band trusted initial root.json, use the -root flag with a file or URL reference. +This will enable you to point cosign to a separate TUF root. -The resulting updated TUF repository will be written to $HOME/.sigstore/root/. +Any updated TUF repository will be written to $HOME/.sigstore/root/. Trusted keys and certificate used in cosign verification (e.g. verifying Fulcio issued certificates with Fulcio root CA) are pulled form the trusted metadata.`, - Example: ` cosign initialize -mirror -out + Example: `cosign initialize -mirror -out - # initialize root with distributed root keys, default mirror, and default out path. - cosign initialize +# initialize root with distributed root keys, default mirror, and default out path. +cosign initialize - # initialize with an out-of-band root key file. - cosign initialize +# initialize with an out-of-band root key file, using the default mirror. +cosign initialize -root - # initialize with an out-of-band root key file and custom repository mirror. - cosign initialize -mirror -root `, +# initialize with an out-of-band root key file and custom repository mirror. +cosign initialize -mirror -root `, RunE: func(cmd *cobra.Command, args []string) error { return initialize.DoInitialize(cmd.Context(), o.Root, o.Mirror, o.Threshold) }, diff --git a/cmd/cosign/cli/initialize/init.go b/cmd/cosign/cli/initialize/init.go index bb3c9658e43..793d7f68ea3 100644 --- a/cmd/cosign/cli/initialize/init.go +++ b/cmd/cosign/cli/initialize/init.go @@ -20,31 +20,31 @@ import ( _ "embed" // To enable the `go:embed` directive. "github.com/sigstore/cosign/pkg/blob" - ctuf "github.com/sigstore/cosign/pkg/cosign/tuf" + "github.com/sigstore/cosign/pkg/cosign/tuf" ) -//go:embed 1.root.json -var initialRoot string - func DoInitialize(ctx context.Context, root, mirror string, threshold int) error { // Get the initial trusted root contents. var rootFileBytes []byte - if root == "" { - rootFileBytes = []byte(initialRoot) - } else { - var err error + var err error + if root != "" { rootFileBytes, err = blob.LoadFileOrURL(root) if err != nil { return err } + } else { + rootFileBytes, err = tuf.GetEmbeddedRoot() + if err != nil { + return err + } } // Initialize the remote repository. - remote, err := ctuf.GcsRemoteStore(ctx, mirror, nil, nil) + remote, err := tuf.GcsRemoteStore(ctx, mirror, nil, nil) if err != nil { return err } // Initialize and update the local SigStore root. - return ctuf.Init(ctx, rootFileBytes, remote, threshold) + return tuf.Init(ctx, rootFileBytes, remote, threshold) } diff --git a/doc/cosign_initialize.md b/doc/cosign_initialize.md index 92048d08b99..99cd698a9d5 100644 --- a/doc/cosign_initialize.md +++ b/doc/cosign_initialize.md @@ -7,13 +7,14 @@ Initializes SigStore root to retrieve trusted certificate and key targets for ve Initializes SigStore root to retrieve trusted certificate and key targets for verification. The following options are used by default: - - The initial 1.root.json is embedded inside cosign. - - SigStore current TUF repository is pulled from the GCS mirror at sigstore-tuf-root. + - The current trusted Sigstore TUF root is embedded inside cosign at the time of release. + - SigStore remote TUF repository is pulled from the GCS mirror at sigstore-tuf-root. - A default threshold of 3 root signatures is used. To provide an out-of-band trusted initial root.json, use the -root flag with a file or URL reference. +This will enable you to point cosign to a separate TUF root. -The resulting updated TUF repository will be written to $HOME/.sigstore/root/. +Any updated TUF repository will be written to $HOME/.sigstore/root/. Trusted keys and certificate used in cosign verification (e.g. verifying Fulcio issued certificates with Fulcio root CA) are pulled form the trusted metadata. @@ -25,16 +26,16 @@ cosign initialize [flags] ### Examples ``` - cosign initialize -mirror -out +cosign initialize -mirror -out - # initialize root with distributed root keys, default mirror, and default out path. - cosign initialize +# initialize root with distributed root keys, default mirror, and default out path. +cosign initialize - # initialize with an out-of-band root key file. - cosign initialize +# initialize with an out-of-band root key file, using the default mirror. +cosign initialize -root - # initialize with an out-of-band root key file and custom repository mirror. - cosign initialize -mirror -root +# initialize with an out-of-band root key file and custom repository mirror. +cosign initialize -mirror -root ``` ### Options diff --git a/pkg/cosign/tlog.go b/pkg/cosign/tlog.go index 02627306dfd..7b61359826d 100644 --- a/pkg/cosign/tlog.go +++ b/pkg/cosign/tlog.go @@ -20,11 +20,8 @@ import ( "encoding/base64" "encoding/hex" "fmt" - "os" "strings" - _ "embed" // To enable the `go:embed` directive. - "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/google/trillian/merkle/logverifier" @@ -41,20 +38,16 @@ import ( rekord_v001 "github.com/sigstore/rekor/pkg/types/rekord/v0.0.1" ) -// This is rekor's public key, via `curl -L rekor.sigstore.dev/api/ggcrv1/log/publicKey` -// rekor.pub should be updated whenever the Rekor public key is rotated & the bundle annotation should be up-versioned -//go:embed rekor.pub -var rekorPub string +// This is the rekor public key target name var rekorTargetStr = `rekor.pub` func GetRekorPub() string { ctx := context.Background() // TODO: pass in context? buf := tuf.ByteDestination{Buffer: &bytes.Buffer{}} - err := tuf.GetTarget(ctx, rekorTargetStr, &buf) - if err != nil { - // The user may not have initialized the local root metadata. Log the error and use the embedded root. - fmt.Fprintln(os.Stderr, "No TUF root installed, using embedded rekor key") - return rekorPub + // Retrieves the rekor public key from the embedded or cached TUF root. If expired, makes a + // network call to retrieve the updated target. + if err := tuf.GetTarget(ctx, rekorTargetStr, &buf); err != nil { + panic("error retrieving rekor public key") } return buf.String() } diff --git a/pkg/cosign/tuf/client.go b/pkg/cosign/tuf/client.go index d69f310e057..adf1e56b7d0 100644 --- a/pkg/cosign/tuf/client.go +++ b/pkg/cosign/tuf/client.go @@ -18,11 +18,15 @@ package tuf import ( "bytes" "context" + "embed" "encoding/json" "io" + "io/fs" "os" "path" + "path/filepath" "sync" + "time" "github.com/pkg/errors" "github.com/theupdateframework/go-tuf" @@ -32,17 +36,32 @@ import ( "github.com/theupdateframework/go-tuf/util" ) +// TODO(asraa): Configure an environment variable so users can set their own remote +// outside of an explicit `cosign init` (e.g. when no cache is enabled). const ( TufRootEnv = "TUF_ROOT" + SigstoreNoCache = "SIGSTORE_NO_CACHE" defaultLocalStore = ".sigstore/root/" + DefaultRemoteRoot = "sigstore-tuf-root" ) -// Global TUF client. Stores local targets in $HOME/.sigstore/root. -// Could be in memory local store, but that would mean re-download each time cosign is run. +//go:embed repository/*.json +//go:embed repository/targets/*.pem repository/targets/*.pub +var root embed.FS + +// Global TUF client. +// Uses TUF metadata and targets embedded in repository/* or cached in ${TUF_ROOT} (by default +// $HOME/.sigstore/root). +// If this metadata is invalid, e.g. expired, makes a call to the remote repository and caches +// unless SIGSTORE_NO_CACHE is set. var rootClient *client.Client var rootClientMu = &sync.Mutex{} -func CosignRoot() string { +func GetEmbeddedRoot() ([]byte, error) { + return root.ReadFile(filepath.Join("repository", "root.json")) +} + +func CosignCachedRoot() string { rootDir := os.Getenv(TufRootEnv) if rootDir == "" { home, err := os.UserHomeDir() @@ -54,8 +73,8 @@ func CosignRoot() string { return rootDir } -func CosignTargets() string { - return path.Join(CosignRoot(), "targets") +func CosignCachedTargets() string { + return path.Join(CosignCachedRoot(), "targets") } // Target destinations compatible with go-tuf. @@ -77,52 +96,195 @@ func (b *ByteDestination) Delete() error { return nil } -func getRootKeys(rootFileBytes []byte) ([]*data.Key, error) { - store := tuf.MemoryStore(map[string]json.RawMessage{"root.json": rootFileBytes}, nil) - repo, err := tuf.NewRepo(store) - if err != nil { - return nil, err +// Retrieves a local target, either from the cached root or the embedded metadata. +func getLocalTarget(name string) (fs.File, error) { + if _, err := os.Stat(CosignCachedTargets()); !os.IsNotExist(err) { + // Return local cached target + return os.Open(path.Join(CosignCachedTargets(), name)) } - return repo.RootKeys() + return root.Open(path.Join("repository/targets", name)) +} + +type signedMeta struct { + Type string `json:"_type"` + Expires time.Time `json:"expires"` + Version int `json:"version"` +} + +func isExpiredMetadata(metadata []byte) bool { + s := &data.Signed{} + if err := json.Unmarshal(metadata, s); err != nil { + return true + } + sm := &signedMeta{} + if err := json.Unmarshal(s.Signed, sm); err != nil { + return true + } + return time.Until(sm.Expires) <= 0 } // Gets the global TUF client if the directory exists. -// This will not make a remote call. -func RootClient(ctx context.Context, local string, remote client.RemoteStore) (*client.Client, error) { +// This will not make a remote call unless fetch is true. +func RootClient(ctx context.Context, remote client.RemoteStore, altRoot []byte) (*client.Client, error) { rootClientMu.Lock() defer rootClientMu.Unlock() if rootClient == nil { - // Instantiate the global TUF client from the local store. This does not do a download. - local, err := tuf_leveldbstore.FileLocalStore(local) + // Instantiate the global TUF client from the local embedded root or the cached root unless altRoot is provided. + // In that case, always instantiate from altRoot. + path := filepath.Join(CosignCachedRoot(), "tuf.db") + _, err := os.Open(path) + if os.IsNotExist(err) && altRoot == nil { + // Cache does not exist, check if the embedded metadata is currently valid. + // TODO(asraa): Need a better way to check if local metadata is verified at this stage. + timestamp, err := root.ReadFile(filepath.Join("repository", "timestamp.json")) + if err != nil { + return nil, errors.Wrap(err, "reading local timestamp") + } + if !isExpiredMetadata(timestamp) { + local := client.MemoryLocalStore() + if err := local.SetMeta("timestamp.json", timestamp); err != nil { + return nil, errors.Wrap(err, "setting local meta") + } + for _, metadata := range []string{"root.json", "targets.json", "snapshot.json"} { + msg, err := root.ReadFile(filepath.Join("repository", metadata)) + if err != nil { + return nil, errors.Wrap(err, "reading local root") + } + if err := local.SetMeta(metadata, msg); err != nil { + return nil, errors.Wrap(err, "setting local meta") + } + } + return client.NewClient(local, remote), nil + } + } + + // Local cached metadata exists, altRoot is provided, or embedded metadata is expired. + // In these cases, we need to pull from remote and may cache locally. + // TODO(asraa): Respect SIGSTORE_NO_CACHE. + // Initialize the remote repository. + if remote == nil { + var err error + remote, err = GcsRemoteStore(ctx, DefaultRemoteRoot, nil, nil) + if err != nil { + return nil, err + } + } + local, err := tuf_leveldbstore.FileLocalStore(path) if err != nil { - return nil, errors.Wrap(err, "initializing local store") + return nil, errors.Wrap(err, "creating cached local store") } rootClient = client.NewClient(local, remote) + // We may need to download latest metadata and targets if the cache is un-initialized or expired. + trustedMeta, err := local.GetMeta() + if err != nil { + return nil, errors.Wrap(err, "getting trusted meta") + } + trustedTimestamp, ok := trustedMeta["timestamp.json"] + if !ok || isExpiredMetadata(trustedTimestamp) { + var trustedRoot []byte + trustedRoot, ok := trustedMeta["root.json"] + if !ok { + // Use embedded root or altRoot as trusted if cached root does not exist + if altRoot != nil { + trustedRoot = altRoot + } else { + trustedRoot, err = root.ReadFile(filepath.Join("repository", "root.json")) + if err != nil { + return nil, errors.Wrap(err, "reading embedded trusted root") + } + } + } + rootKeys, rootThreshold, err := getRootKeys(trustedRoot) + if err != nil { + return nil, errors.Wrap(err, "bad trusted root") + } + if err := rootClient.Init(rootKeys, rootThreshold); err != nil { + return nil, errors.Wrap(err, "initializing root client") + } + if err := updateMetadataAndDownloadTargets(rootClient); err != nil { + return nil, errors.Wrap(err, "updating from remote TUF repository") + } + } } + return rootClient, nil } +func getTargetHelper(name string, out client.Destination, c *client.Client) error { + // Get valid target metadata. Does a local verification. + validMeta, err := c.Target(name) + if err != nil { + return errors.Wrap(err, "error verifying local metadata; local cache may be corrupt") + } + + // We have valid local metadata and targets. Get embedded or cached local target. + localTarget, err := getLocalTarget(name) + if err != nil { + return errors.Wrap(err, "reading local targets") + } + + tee := io.TeeReader(localTarget, out) + localMeta, err := util.GenerateTargetFileMeta(tee) + if err != nil { + return errors.Wrap(err, "generating local target metadata") + } + + // If local target meta does not match the valid local meta, consider this an error. + // We may want to make a network call to update the local metadata and re-download. + if err := util.TargetFileMetaEqual(validMeta, localMeta); err != nil { + return errors.Wrap(err, "bad local target") + } + + return localTarget.Close() +} + +func GetTarget(ctx context.Context, name string, out client.Destination) error { + // Reads the embedded or cached root. Fallsback on the default remote. + // TODO(asraa): Replace default remote with a configurable environment variable. + c, err := RootClient(ctx, nil, nil) + if err != nil { + return errors.Wrap(err, "retrieving trusted root; local cache may be corrupt") + } + + // Retrieves the target and writes to out. This may make a network call and cache if + // the embedded or cached root is invalid (e.g. expired). + return getTargetHelper(name, out, c) +} + +func getRootKeys(rootFileBytes []byte) ([]*data.Key, int, error) { + store := tuf.MemoryStore(map[string]json.RawMessage{"root.json": rootFileBytes}, nil) + repo, err := tuf.NewRepo(store) + if err != nil { + return nil, 0, err + } + rootKeys, err := repo.RootKeys() + if err != nil { + return nil, 0, err + } + rootThreshold, err := repo.GetThreshold("root") + return rootKeys, rootThreshold, err +} + func updateMetadataAndDownloadTargets(c *client.Client) error { - // Download initial targets and store in $HOME/.sigstore/root/targets/. + // Download updated targets and cache new metadata and targets in ${TUF_ROOT}. targetFiles, err := c.Update() if err != nil && !client.IsLatestSnapshot(err) { return errors.Wrap(err, "updating tuf metadata") } - // Download targets, if they don't already exist and match the updated metadata. - if err := os.MkdirAll(CosignTargets(), 0700); err != nil { + if err := os.MkdirAll(CosignCachedTargets(), 0700); err != nil { return errors.Wrap(err, "creating targets dir") } for name := range targetFiles { - if err := downloadTarget(name, c, nil); err != nil { + if err := downloadRemoteTarget(name, c, nil); err != nil { return err } } return nil } -func downloadTarget(name string, c *client.Client, out client.Destination) error { - f, err := os.Create(path.Join(CosignTargets(), name)) +func downloadRemoteTarget(name string, c *client.Client, out client.Destination) error { + f, err := os.Create(path.Join(CosignCachedTargets(), name)) if err != nil { return errors.Wrap(err, "creating target file") } @@ -138,22 +300,24 @@ func downloadTarget(name string, c *client.Client, out client.Destination) error return err } -// Instantiates the global TUF client. Downloads all initial targets and stores in $HOME/.sigstore/root/targets/. +// Instantiates the global TUF client. Uses the embedded (by default trusted) root in cosign +// unless a custom root is provided. This will always perform a remote call to update. func Init(ctx context.Context, rootBytes []byte, remote client.RemoteStore, threshold int) error { - rootClient, err := RootClient(ctx, CosignRoot(), remote) + rootClient, err := RootClient(ctx, remote, rootBytes) if err != nil { return errors.Wrap(err, "initializing root client") } - rootKeys, err := getRootKeys(rootBytes) + rootKeys, rootThreshold, err := getRootKeys(rootBytes) if err != nil { return errors.Wrap(err, "retrieving root keys") } - if err := rootClient.Init(rootKeys, threshold); err != nil { + // Initiates a network call to the remote. + if err := rootClient.Init(rootKeys, rootThreshold); err != nil { return errors.Wrap(err, "initializing tuf client") } - // Download initial targets and store in $HOME/.sigstore/root/targets/. - if err := os.MkdirAll(CosignRoot(), 0755); err != nil { - return errors.Wrap(err, "creating targets dir") + // Download initial targets and store in ${TUF_ROOT}/.sigstore/root/targets/. + if err := os.MkdirAll(CosignCachedRoot(), 0755); err != nil { + return errors.Wrap(err, "creating root dir") } if err := updateMetadataAndDownloadTargets(rootClient); err != nil { return errors.Wrap(err, "updating local metadata and targets") @@ -161,52 +325,3 @@ func Init(ctx context.Context, rootBytes []byte, remote client.RemoteStore, thre return nil } - -func getTargetHelper(name string, out client.Destination, c *client.Client) error { - // Get valid target metadata. Does a local verification. - validMeta, err := c.Target(name) - if err != nil { - return errors.Wrap(err, "missing target metadata") - } - - // Get local target. - localTarget, err := os.Open(path.Join(CosignTargets(), name)) - if err != nil { - // If the file does not exist, download the target and copy to out. - return downloadTarget(name, c, out) - } - - // Otherwise, the file exists in the local store. - localMeta, err := util.GenerateTargetFileMeta(localTarget) - if err != nil { - return errors.Wrap(err, "generating local target metadata") - } - - // If local target meta does not match valid meta, update and re-download. - if err := util.TargetFileMetaEqual(validMeta, localMeta); err != nil { - if err := updateMetadataAndDownloadTargets(c); err != nil { - return errors.Wrap(err, "updating target metadata") - } - // Try again with updated metadata. - return getTargetHelper(name, out, c) - } - - // Target metadata equal, copy the file into out. - if _, err := localTarget.Seek(0, io.SeekStart); err != nil { - return err - } - if _, err := io.Copy(out, localTarget); err != nil { - return errors.Wrap(err, "copying target") - } - return localTarget.Close() -} - -func GetTarget(ctx context.Context, name string, out client.Destination) error { - // Reads the root in CosignRoot() directory. Does not use a remote. - c, err := RootClient(ctx, CosignRoot(), nil) - if err != nil { - return errors.Wrap(err, "retrieving root") - } - - return getTargetHelper(name, out, c) -} diff --git a/pkg/cosign/tuf/client_test.go b/pkg/cosign/tuf/client_test.go index 003d0cc6791..e86eca5fb1d 100644 --- a/pkg/cosign/tuf/client_test.go +++ b/pkg/cosign/tuf/client_test.go @@ -17,7 +17,6 @@ package tuf import ( "bytes" - "context" "encoding/json" "io" "os" @@ -138,8 +137,19 @@ func TestValidMetadata(t *testing.T) { defer os.Unsetenv(TufRootEnv) // Set up client - if err := Init(context.Background(), root, remote, 1); err != nil { - t.Fatalf("unexpected error initializing root client %v", err) + rootClient := client.NewClient(local, remote) + if err != nil { + t.Fatalf("creating root client") + } + rootKeys, rootThreshold, err := getRootKeys(root) + if err != nil { + t.Fatalf("bad trusted root") + } + if err := rootClient.Init(rootKeys, rootThreshold); err != nil { + t.Fatalf("initializing root client") + } + if err := updateMetadataAndDownloadTargets(rootClient); err != nil { + t.Fatalf("updating from remote TUF repository") } target := "foo.txt" diff --git a/cmd/cosign/cli/initialize/1.root.json b/pkg/cosign/tuf/repository/root.json similarity index 99% rename from cmd/cosign/cli/initialize/1.root.json rename to pkg/cosign/tuf/repository/root.json index dcc71f963a8..d2ddcab9d6b 100644 --- a/cmd/cosign/cli/initialize/1.root.json +++ b/pkg/cosign/tuf/repository/root.json @@ -127,4 +127,4 @@ "spec_version": "1.0", "version": 1 } -} \ No newline at end of file +} diff --git a/pkg/cosign/tuf/repository/snapshot.json b/pkg/cosign/tuf/repository/snapshot.json new file mode 100644 index 00000000000..187bc4e315a --- /dev/null +++ b/pkg/cosign/tuf/repository/snapshot.json @@ -0,0 +1,46 @@ +{ + "signatures": [ + { + "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", + "sig": "3045022100a7aa1299c325e9d42442ab3d2d25332692d598d5128bc44849fe7108738104ba02201fd3551848bbe94d5c74b9c305920c2d885746134241f54af705380ca4326789" + }, + { + "keyid": "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", + "sig": "304502202d3eff55465a12fb9f35ce189872f1d5487428e2b3dab32a57cdc02bf652ebc5022100a735d4399ccd6f78dfa3b708b9fd4ce8148d3e9cb9e6bdb3b4a9f997d3e6f88b" + }, + { + "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", + "sig": "3046022100a0eefb45472bef9803dfdd16e657603cf2c96561c587057f7ad92dc7851dcfcc02210095a78985adb2a5a1c239ea43019eaed8e4cffe58a224a32f31ffb9c45bd45928" + }, + { + "keyid": "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", + "sig": "304502201cb475203a2726ba21db3cceeff39739f4e1c4b05a8af5e4d1155b60b88a107702210085d841e5420cf403ef65cd94ca92cb75c90b558ebf82de9b93370d7ea0612b56" + }, + { + "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", + "sig": "3044022044b6fa3e918110508c21999974f0a7716a9dd4a0a68234e7330c889e2e336cab0220271a14fa0ea822ed539746f13546139414ae2e8a961db6732f18c3aba08da892" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2021-12-18T13:28:12.99008-06:00", + "meta": { + "root.json": { + "hashes": { + "sha512": "87d41965ac08a2e03a33aa2db1dbbf5a7d6616e49f0ca3b20afe1c46ae78e52e841e21cca347f7b77ec929b6a3687d86d320ee04ba5f75a1880974310958b9c2" + }, + "length": 5096, + "version": 1 + }, + "targets.json": { + "hashes": { + "sha512": "14018a7442402d31ea7090cf9d7f1e0f56b1811884d843f33e1754b4d50521a83cf3425b93b49ad8a7eaedf5c993cec053e558888d1a97ec94d539abd97022a3" + }, + "length": 2235, + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} diff --git a/pkg/cosign/tuf/repository/targets.json b/pkg/cosign/tuf/repository/targets.json new file mode 100644 index 00000000000..4bf99244828 --- /dev/null +++ b/pkg/cosign/tuf/repository/targets.json @@ -0,0 +1,56 @@ +{ + "signatures": [ + { + "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", + "sig": "3045022058cbcea7670f78202eaa99a77f099ca99b8cb9bd09d1d8367ea7736021da7124022100ee5c5dc57f3055861f1c6e8571b3749f462cfadce961aed7e121be0f29d19694" + }, + { + "keyid": "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", + "sig": "30450220688e5776537a91cd2cdb1cf786efd7ee2751f775dd80e3ee50181b08ce644cfd022100da925d870514ff85bc497333df44fd5db91bddce1c2a2265c65010fa41d51538" + }, + { + "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", + "sig": "3046022100b42c58c65bc52f3628307a658bea1ed4b1dddd1a03cf04fbef50a391be1a2814022100d1f86ac629f53273b8462204ad8f9a34e0967a052873c5de45a05848c48607d3" + }, + { + "keyid": "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", + "sig": "3045022043d10ae5ee3bdd18b81cb0c1307091d379b23a47287fb1c7992b43d17503d580022100b5c6b94865cf81e54959a5768ff627798aa017c3fb37287ecdb146f9198eb228" + }, + { + "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", + "sig": "3045022100b5a9ad67c30b11ee60d28511eaeed69af9f06f4061703dd1e9f1f41565ff06fa02201e632c9f198fcd9df23e544e29c97a7e483bfa600d3b2759233f7a5a2ba5e7a8" + } + ], + "signed": { + "_type": "targets", + "expires": "2021-12-18T13:28:13-06:00", + "spec_version": "1.0", + "targets": { + "artifact.pub": { + "hashes": { + "sha512": "4dd0c86b85b98695f403ce0858ac37cf3d39598dbeb754ce7e5ee8fe845e35cd64523cb9d452800eaf153fc769616cc2e0d12527b1ce9339ba1ae0372276a2d4" + }, + "length": 177 + }, + "ctfe.pub": { + "hashes": { + "sha512": "4b20747d1afe2544238ad38cc0cc3010921b177d60ac743767e0ef675b915489bd01a36606c0ff83c06448622d7160f0d866c83d20f0c0f44653dcc3f9aa0bd4" + }, + "length": 177 + }, + "fulcio.crt.pem": { + "hashes": { + "sha512": "100f563c94b14c09c61adbaa460e3caa49083662dfcc4ad0a07296e3e719d8b449a0c0ddad37775f32af69c3535629b83aa8c95286e32251ad99eed38fff69c3" + }, + "length": 768 + }, + "rekor.pub": { + "hashes": { + "sha512": "0ae7705e02db33e814329746a4a0e5603c5bdcd91c96d072158d71011a2695788866565a2fec0fe363eb72cbcaeda39e54c5fe8d416daf9f3101fdba4217ef35" + }, + "length": 178 + } + }, + "version": 1 + } +} diff --git a/pkg/cosign/tuf/repository/targets/artifact.pub b/pkg/cosign/tuf/repository/targets/artifact.pub new file mode 100644 index 00000000000..342813054ab --- /dev/null +++ b/pkg/cosign/tuf/repository/targets/artifact.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZxAfzrQG1EbWyCI8LiSB7YgSFXoI +FNGTyQGKHFc6/H8TQumT9VLS78pUwtv3w7EfKoyFZoP32KrO7nzUy2q6Cw== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/ctfe.pub b/pkg/cosign/tuf/repository/targets/ctfe.pub similarity index 85% rename from cmd/cosign/cli/fulcio/fulcioverifier/ctfe.pub rename to pkg/cosign/tuf/repository/targets/ctfe.pub index 75df6bbb9bc..1bb1488c99f 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/ctfe.pub +++ b/pkg/cosign/tuf/repository/targets/ctfe.pub @@ -1,4 +1,4 @@ -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3Pyu dDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w== ------END PUBLIC KEY----- +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/pkg/cosign/tuf/repository/targets/fulcio.crt.pem b/pkg/cosign/tuf/repository/targets/fulcio.crt.pem new file mode 100644 index 00000000000..174244bb593 --- /dev/null +++ b/pkg/cosign/tuf/repository/targets/fulcio.crt.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- + MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAq + MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIx + MDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUu + ZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSy + A7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0Jcas + taRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6Nm + MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE + FMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2u + Su1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJx + Ve/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uup + Hr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ== + -----END CERTIFICATE----- \ No newline at end of file diff --git a/pkg/cosign/rekor.pub b/pkg/cosign/tuf/repository/targets/rekor.pub similarity index 100% rename from pkg/cosign/rekor.pub rename to pkg/cosign/tuf/repository/targets/rekor.pub diff --git a/pkg/cosign/tuf/repository/timestamp.json b/pkg/cosign/tuf/repository/timestamp.json new file mode 100644 index 00000000000..5bea8900a29 --- /dev/null +++ b/pkg/cosign/tuf/repository/timestamp.json @@ -0,0 +1,39 @@ +{ + "signatures": [ + { + "keyid": "2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97", + "sig": "3044022079252576532ed5ed4a19e4135997d89172101ed745a4489be6b20d04d483bbcc0220515119aab690033dc1e1650f08995dc839dcd161cab3898db0749063ca32dc86" + }, + { + "keyid": "bdde902f5ec668179ff5ca0dabf7657109287d690bf97e230c21d65f99155c62", + "sig": "3045022100c5216dd17d381c951b5174f8ee2157b315d1f26247e7f9e49c42cf975dfcf49b022048fb1751a86fddedc21129e94a3e7e0efeeb93f1238fad6636bbf0c0d39543e8" + }, + { + "keyid": "eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b", + "sig": "3045022042c6b4003deee27db7db6f5aebb29ac89625fd7389dfff434fa93c65cf8aed5f022100fe6cbd036b5fce1169d7392ecfaf76e01f05fdc6c81cf9bae8c9227fc09c65d9" + }, + { + "keyid": "f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb", + "sig": "3045022100bc431b7315c2aa657418835005692021de7496bbc7c1a2fedf2aafe8d861ca5402200cbca80a4555d8236265e1b746743532894b46257c450ff8706d0e50e659978c" + }, + { + "keyid": "f505595165a177a41750a8e864ed1719b1edfccd5a426fd2c0ffda33ce7ff209", + "sig": "3046022100c09aea2f05e94a656bd70379340c7b5f09b24bbd20adf4855be3783d7ce39482022100da93a8a1577599979d38bfc44016bde5838d9548797fc9e960780276855bbcf9" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2021-12-18T13:28:12.99008-06:00", + "meta": { + "snapshot.json": { + "hashes": { + "sha512": "9103503c18f7da2098dce04892948ad240c1b9965048c4ab4da0c32248f3491652d91d76fe9022be2cf15a99e68b3a3ddd1034e5293c8aac86d0446c4354716d" + }, + "length": 1849, + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +}