From 0e74150dbf6c7ec383f6979898d04a83b87361db Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Mon, 11 Dec 2023 17:31:18 -0500 Subject: [PATCH] Update TUF client to support options and add LiveTrustedRoot Signed-off-by: Cody Soyland --- cmd/conformance/main.go | 17 +- cmd/sigstore-go/main.go | 24 ++- examples/oci-image-verification/go.mod | 11 +- examples/oci-image-verification/go.sum | 16 +- examples/oci-image-verification/main.go | 22 ++- go.mod | 7 +- go.sum | 8 +- pkg/root/trusted_root.go | 101 +++++++++++ pkg/tuf/client.go | 229 ++++++++++++++---------- pkg/tuf/options.go | 81 +++++++++ pkg/tuf/repository/root.json | 140 +++++++++++++++ 11 files changed, 532 insertions(+), 124 deletions(-) create mode 100644 pkg/tuf/options.go create mode 100644 pkg/tuf/repository/root.json diff --git a/cmd/conformance/main.go b/cmd/conformance/main.go index 1db1d47e..84c2bf96 100644 --- a/cmd/conformance/main.go +++ b/cmd/conformance/main.go @@ -57,10 +57,19 @@ func getTrustedRoot() root.TrustedMaterial { if !ok { log.Fatal("unable to get path") } - - tufDir := path.Join(path.Dir(filename), "tufdata") - - trustedRootJSON, err = tuf.GetTrustedrootJSON("tuf-repo-cdn.sigstore.dev", tufDir) + opts, err := tuf.DefaultOptions() + if err != nil { + log.Fatal(err) + } + opts.CachePath = path.Join(path.Dir(filename), "tufdata") + client, err := tuf.New(opts) + if err != nil { + log.Fatal(err) + } + trustedRootJSON, err = client.GetTarget("trusted_root.json") + if err != nil { + log.Fatal(err) + } } if err != nil { diff --git a/cmd/sigstore-go/main.go b/cmd/sigstore-go/main.go index 3e6ef65c..6380626f 100644 --- a/cmd/sigstore-go/main.go +++ b/cmd/sigstore-go/main.go @@ -47,7 +47,6 @@ var onlineTlog *bool var trustedPublicKey *string var trustedrootJSONpath *string var tufRootURL *string -var tufDirectory *string func init() { artifact = flag.String("artifact", "", "Path to artifact to verify") @@ -63,7 +62,6 @@ func init() { trustedPublicKey = flag.String("publicKey", "", "Path to trusted public key") trustedrootJSONpath = flag.String("trustedrootJSONpath", "examples/trusted-root-public-good.json", "Path to trustedroot JSON file") tufRootURL = flag.String("tufRootURL", "", "URL of TUF root containing trusted root JSON file") - tufDirectory = flag.String("tufDirectory", "tufdata", "Directory to store TUF metadata") flag.Parse() if flag.NArg() == 0 { usage() @@ -122,20 +120,32 @@ func run() error { } var trustedMaterial = make(root.TrustedMaterialCollection, 0) - var trustedrootJSON []byte + var trustedRootJSON []byte if *tufRootURL != "" { - trustedrootJSON, err = tuf.GetTrustedrootJSON(*tufRootURL, *tufDirectory) + opts, err := tuf.DefaultOptions() + if err != nil { + return err + } + opts.RepositoryBaseURL = *tufRootURL + client, err := tuf.New(opts) + if err != nil { + return err + } + trustedRootJSON, err = client.GetTarget("trusted_root.json") + if err != nil { + return err + } } else if *trustedrootJSONpath != "" { - trustedrootJSON, err = os.ReadFile(*trustedrootJSONpath) + trustedRootJSON, err = os.ReadFile(*trustedrootJSONpath) } if err != nil { return err } - if len(trustedrootJSON) > 0 { + if len(trustedRootJSON) > 0 { var trustedRoot *root.TrustedRoot - trustedRoot, err = root.NewTrustedRootFromJSON(trustedrootJSON) + trustedRoot, err = root.NewTrustedRootFromJSON(trustedRootJSON) if err != nil { return err } diff --git a/examples/oci-image-verification/go.mod b/examples/oci-image-verification/go.mod index 08175b9e..81a8a5d3 100644 --- a/examples/oci-image-verification/go.mod +++ b/examples/oci-image-verification/go.mod @@ -1,6 +1,8 @@ module github.com/sigstore/sigstore-go/examples/oci-image-verification -go 1.21 +go 1.21.5 + +replace github.com/sigstore/sigstore-go => ../../ require ( github.com/google/go-containerregistry v0.17.0 @@ -22,7 +24,7 @@ require ( github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.4 // indirect @@ -31,7 +33,7 @@ require ( github.com/go-openapi/loads v0.21.2 // indirect github.com/go-openapi/runtime v0.26.0 // indirect github.com/go-openapi/spec v0.20.11 // indirect - github.com/go-openapi/strfmt v0.21.8 // indirect + github.com/go-openapi/strfmt v0.21.9 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/validate v0.22.3 // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -55,6 +57,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rdimitrov/go-tuf-metadata v0.0.0-20231211110834-6de72dba550c // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect @@ -74,7 +77,7 @@ require ( github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect github.com/vbatts/tar-split v0.11.3 // indirect - go.mongodb.org/mongo-driver v1.12.0 // indirect + go.mongodb.org/mongo-driver v1.13.0 // indirect go.opentelemetry.io/otel v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect diff --git a/examples/oci-image-verification/go.sum b/examples/oci-image-verification/go.sum index 61d492a0..ab6cd501 100644 --- a/examples/oci-image-verification/go.sum +++ b/examples/oci-image-verification/go.sum @@ -168,8 +168,8 @@ github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkc github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= @@ -193,8 +193,8 @@ github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6 github.com/go-openapi/spec v0.20.11 h1:J/TzFDLTt4Rcl/l1PmyErvkqlJDncGvPTMnCI39I4gY= github.com/go-openapi/spec v0.20.11/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/strfmt v0.21.8 h1:VYBUoKYRLAlgKDrIxR/I0lKrztDQ0tuTDrbhLVP8Erg= -github.com/go-openapi/strfmt v0.21.8/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/go-openapi/strfmt v0.21.9 h1:LnEGOO9qyEC1v22Bzr323M98G13paIUGPU7yeJtG9Xs= +github.com/go-openapi/strfmt v0.21.9/go.mod h1:0k3v301mglEaZRJdDDGSlN6Npq4VMVU69DE0LUyf7uA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= @@ -406,6 +406,8 @@ github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lne github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rdimitrov/go-tuf-metadata v0.0.0-20231211110834-6de72dba550c h1:Y4Mx6GbsUbzvV41SuQfE671gKAXdILTSGdUe4+8y7DE= +github.com/rdimitrov/go-tuf-metadata v0.0.0-20231211110834-6de72dba550c/go.mod h1:3l8VADBl9myZ4VNSQtmM46iEA+jolS2ZFviLocdyWPw= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= @@ -432,8 +434,6 @@ github.com/sigstore/rekor v1.3.4 h1:RGIia1iOZU7fOiiP2UY/WFYhhp50S5aUm7YrM8aiA6E= github.com/sigstore/rekor v1.3.4/go.mod h1:1GubPVO2yO+K0m0wt/3SHFqnilr/hWbsjSOe7Vzxrlg= github.com/sigstore/sigstore v1.7.6 h1:zB0woXx+3Bp7dk7AjklHF1VhXBdCs84VXkZbp0IHLv8= github.com/sigstore/sigstore v1.7.6/go.mod h1:FJE+NpEZIs4QKqZl4B2RtaVLVDcDtocAwTiNlexeBkY= -github.com/sigstore/sigstore-go v0.0.0-20231206154419-7f57c1495ca4 h1:2KpuUMK4lFw8GIUPmjbETXjVroV8NBgJima7RRsk55E= -github.com/sigstore/sigstore-go v0.0.0-20231206154419-7f57c1495ca4/go.mod h1:N9kAbQfXk6oK1od+ZAh0zDd2UjBsq2iqpEK7UvZrU1I= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.7.5 h1:ilufPp36exfpivctI3ElU4ZTckP3eVu6RxYebBb6u+M= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.7.5/go.mod h1:121n8nBnuXbcI6K0hIBo/0EMYiyXqGVzbIYd0rV0ZWw= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.7.5 h1:gLdNJJo+xMf7+IeFRlyA/Pjavndo9rivmf5ioYeuPmM= @@ -503,8 +503,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5RI/4= github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= -go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= +go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWUfY= +go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/examples/oci-image-verification/main.go b/examples/oci-image-verification/main.go index 89df1f56..104b0dba 100644 --- a/examples/oci-image-verification/main.go +++ b/examples/oci-image-verification/main.go @@ -142,20 +142,32 @@ func run() error { } var trustedMaterial = make(root.TrustedMaterialCollection, 0) - var trustedrootJSON []byte + var trustedRootJSON []byte if *tufRootURL != "" { - trustedrootJSON, err = tuf.GetTrustedrootJSON(*tufRootURL, *tufDirectory) + opts, err := tuf.DefaultOptions() + if err != nil { + return err + } + opts.RepositoryBaseURL = *tufRootURL + client, err := tuf.New(opts) + if err != nil { + return err + } + trustedRootJSON, err = client.GetTarget("trusted_root.json") + if err != nil { + return err + } } else if *trustedrootJSONpath != "" { - trustedrootJSON, err = os.ReadFile(*trustedrootJSONpath) + trustedRootJSON, err = os.ReadFile(*trustedrootJSONpath) } if err != nil { return err } - if len(trustedrootJSON) > 0 { + if len(trustedRootJSON) > 0 { var trustedRoot *root.TrustedRoot - trustedRoot, err = root.NewTrustedRootFromJSON(trustedrootJSON) + trustedRoot, err = root.NewTrustedRootFromJSON(trustedRootJSON) if err != nil { return err } diff --git a/go.mod b/go.mod index d1ff24ba..54093f36 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sigstore/sigstore-go -go 1.21 +go 1.21.5 require ( github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 @@ -10,13 +10,13 @@ require ( github.com/go-openapi/swag v0.22.4 github.com/google/certificate-transparency-go v1.1.7 github.com/in-toto/in-toto-golang v0.9.0 + github.com/rdimitrov/go-tuf-metadata v0.0.0-20231211110834-6de72dba550c github.com/secure-systems-lab/go-securesystemslib v0.7.0 github.com/sigstore/protobuf-specs v0.2.1 github.com/sigstore/rekor v1.3.4 github.com/sigstore/sigstore v1.7.6 github.com/sigstore/timestamp-authority v1.2.0 github.com/stretchr/testify v1.8.4 - github.com/theupdateframework/go-tuf v0.7.0 golang.org/x/mod v0.14.0 google.golang.org/protobuf v1.31.0 ) @@ -28,7 +28,7 @@ require ( github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.4 // indirect @@ -66,6 +66,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.17.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/theupdateframework/go-tuf v0.7.0 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect go.mongodb.org/mongo-driver v1.13.0 // indirect diff --git a/go.sum b/go.sum index 0b3f0ef8..17d1c62b 100644 --- a/go.sum +++ b/go.sum @@ -156,8 +156,8 @@ github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkc github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= @@ -392,6 +392,8 @@ github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lne github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rdimitrov/go-tuf-metadata v0.0.0-20231211110834-6de72dba550c h1:Y4Mx6GbsUbzvV41SuQfE671gKAXdILTSGdUe4+8y7DE= +github.com/rdimitrov/go-tuf-metadata v0.0.0-20231211110834-6de72dba550c/go.mod h1:3l8VADBl9myZ4VNSQtmM46iEA+jolS2ZFviLocdyWPw= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= @@ -428,6 +430,8 @@ github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.7.5 h1:yWNBuL52Je3u github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.7.5/go.mod h1:EI9vDWVGG8fQU9aFMY7Bd204xJiqmXcDMSkFifCf16Q= github.com/sigstore/timestamp-authority v1.2.0 h1:Ffk10QsHxu6aLwySQ7WuaoWkD63QkmcKtozlEFot/VI= github.com/sigstore/timestamp-authority v1.2.0/go.mod h1:ojKaftH78Ovfow9DzuNl5WgTCEYSa4m5622UkKDHRXc= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= diff --git a/pkg/root/trusted_root.go b/pkg/root/trusted_root.go index dac46549..65558ba9 100644 --- a/pkg/root/trusted_root.go +++ b/pkg/root/trusted_root.go @@ -20,11 +20,14 @@ import ( "crypto/x509" "encoding/hex" "fmt" + "log" "os" + "sync" "time" protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" prototrustroot "github.com/sigstore/protobuf-specs/gen/pb-go/trustroot/v1" + "github.com/sigstore/sigstore-go/pkg/tuf" "google.golang.org/protobuf/encoding/protojson" ) @@ -256,3 +259,101 @@ func NewTrustedRootProtobuf(rootJSON []byte) (*prototrustroot.TrustedRoot, error } return pbTrustedRoot, nil } + +// FetchTrustedRoot fetches the Sigstore trusted root from TUF and returns it. +func FetchTrustedRoot() (*TrustedRoot, error) { + opts, err := tuf.DefaultOptions() + if err != nil { + return nil, err + } + client, err := tuf.New(opts) + if err != nil { + return nil, err + } + return GetTrustedRoot(client) +} + +// GetTrustedRoot returns the trusted root +func GetTrustedRoot(c *tuf.Client) (*TrustedRoot, error) { + jsonBytes, err := c.GetTarget("trusted_root.json") + if err != nil { + return nil, err + } + return NewTrustedRootFromJSON(jsonBytes) +} + +// LiveTrustedRoot is a wrapper around TrustedRoot that periodically +// refreshes the trusted root from TUF. This is needed for long-running +// processes to ensure that the trusted root does not expire. +type LiveTrustedRoot struct { + *TrustedRoot + mu sync.RWMutex +} + +// NewLiveTrustedRoot returns a LiveTrustedRoot that will periodically +// refresh the trusted root from TUF. +func NewLiveTrustedRoot(opts *tuf.Options) (*LiveTrustedRoot, error) { + client, err := tuf.New(opts) + if err != nil { + return nil, err + } + tr, err := GetTrustedRoot(client) + if err != nil { + return nil, err + } + ltr := &LiveTrustedRoot{ + TrustedRoot: tr, + mu: sync.RWMutex{}, + } + ticker := time.NewTicker(time.Hour * 24) + go func() { + for { + select { + case <-ticker.C: + client, err = tuf.New(opts) + if err != nil { + log.Printf("error creating TUF client: %v", err) + } + newTr, err := GetTrustedRoot(client) + if err != nil { + log.Printf("error fetching trusted root: %v", err) + continue + } + ltr.mu.Lock() + ltr.TrustedRoot = newTr + ltr.mu.Unlock() + } + } + }() + return ltr, nil +} + +func (l *LiveTrustedRoot) TSACertificateAuthorities() []CertificateAuthority { + l.mu.RLock() + defer l.mu.RUnlock() + return l.TrustedRoot.TSACertificateAuthorities() +} + +func (l *LiveTrustedRoot) FulcioCertificateAuthorities() []CertificateAuthority { + l.mu.RLock() + defer l.mu.RUnlock() + return l.TrustedRoot.FulcioCertificateAuthorities() +} + +func (l *LiveTrustedRoot) TlogAuthorities() map[string]*TlogAuthority { + l.mu.RLock() + defer l.mu.RUnlock() + return l.TrustedRoot.TlogAuthorities() +} + +func (l *LiveTrustedRoot) CTlogAuthorities() map[string]*TlogAuthority { + l.mu.RLock() + defer l.mu.RUnlock() + return l.TrustedRoot.CTlogAuthorities() +} + +func (l *LiveTrustedRoot) PublicKeyVerifier(keyID string) (TimeConstrainedVerifier, error) { + l.mu.RLock() + defer l.mu.RUnlock() + return l.TrustedRoot.PublicKeyVerifier(keyID) +} diff --git a/pkg/tuf/client.go b/pkg/tuf/client.go index cfdd7d4d..328fd573 100644 --- a/pkg/tuf/client.go +++ b/pkg/tuf/client.go @@ -15,137 +15,184 @@ package tuf import ( - "bytes" - "embed" - "encoding/json" "fmt" - "path" - - tufclient "github.com/theupdateframework/go-tuf/client" - filejsonstore "github.com/theupdateframework/go-tuf/client/filejsonstore" - tufdata "github.com/theupdateframework/go-tuf/data" - tufutil "github.com/theupdateframework/go-tuf/util" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/rdimitrov/go-tuf-metadata/metadata/config" + "github.com/rdimitrov/go-tuf-metadata/metadata/updater" ) -//go:embed repository -var embeddedRepos embed.FS - -const TrustedRootTUFPath = "trusted_root.json" -const RootTUFPath = "root.json" - -// Implementation of go-tuf/client.Destination interface -type Writer struct { - Bytes []byte +// Client is a Sigstore TUF client +type Client struct { + cfg *config.UpdaterConfig + up *updater.Updater + opts *Options } -func (w *Writer) Write(b []byte) (int, error) { - w.Bytes = append(w.Bytes, b...) - return len(b), nil -} - -func (w *Writer) Delete() error { - w = nil - return nil -} - -func GetTrustedrootJSON(tufRootURL, workPath string) (trustedrootJSON []byte, err error) { - // Ensure we have a RootTUFPath file for this TUF URL - tufPath := path.Join(workPath, tufRootURL) - - fileJSONStore, err := filejsonstore.NewFileJSONStore(tufPath) - if err != nil { - return nil, err +// New returns a new client with custom options +func New(opts *Options) (*Client, error) { + var c = Client{ + opts: opts, } + var dir = filepath.Join(opts.CachePath, URLToPath(opts.RepositoryBaseURL)) + var err error - tufMetaMap, err := fileJSONStore.GetMeta() - if err != nil { - return nil, err + if c.cfg, err = config.New(opts.RepositoryBaseURL, opts.Root); err != nil { + return nil, fmt.Errorf("failed to create TUF repo: %w", err) } - _, ok := tufMetaMap[RootTUFPath] - if !ok { - // There isn't a RootTUFPath for this TUF URL, so see if the library has one embedded - _, err = checkEmbedded(tufRootURL, fileJSONStore) - - if err != nil { - return nil, err - } + c.cfg.LocalMetadataDir = dir + c.cfg.LocalTargetsDir = filepath.Join(dir, "targets") + c.cfg.RemoteTargetsURL, err = url.JoinPath(opts.RepositoryBaseURL, "targets") + if err != nil { + return nil, fmt.Errorf("malformed config mirror: %w", err) } + c.cfg.DisableLocalCache = c.opts.DisableLocalCache + c.cfg.PrefixTargetsWithHash = true - // Now that we have fileJSONStore, create a tufclient and check remote for updates - tufRemoteOptions := &tufclient.HTTPRemoteOptions{ - MetadataPath: "", - TargetsPath: "targets", - Retries: tufclient.DefaultHTTPRetries, + if c.cfg.DisableLocalCache { + c.opts.CachePath = "" + c.opts.CacheValidity = 0 + c.opts.ForceCache = false } - tufRemoteStore, err := tufclient.HTTPRemoteStore(fmt.Sprintf("https://%s", tufRootURL), tufRemoteOptions, nil) + // Upon client creation, we may not perform a full TUF update, + // based on the cache control configuration. Start with a local + // client (only reads content on disk) and then decide if we + // must perform a full TUF update. + var tmpCfg = *c.cfg + tmpCfg.UnsafeLocalMode = true + c.up, err = updater.New(&tmpCfg) if err != nil { return nil, err } + if err = c.loadMetadata(); err != nil { + return nil, err + } - tufClient := tufclient.NewClient(fileJSONStore, tufRemoteStore) - targetFiles, err := tufClient.Update() + return &c, nil +} + +// DefaultClient returns a Sigstore TUF client for the public good instance +func DefaultClient() (*Client, error) { + opts, err := DefaultOptions() if err != nil { return nil, err } + return New(opts) +} - // Now that we've updated, see if remote trustedroot metadata matches local disk - trustedrootMeta, ok := targetFiles[TrustedRootTUFPath] - if !ok { - return nil, fmt.Errorf("Unable to find %s via TUF", TrustedRootTUFPath) +// loadMetadata controls if the client actually should perform a TUF refresh. +// The TUF specification mandates so, but for certain Sigstore clients, it +// may be beneficial to rely on the cache, or in air-gapped deployments it +// it may not even be possible. +func (c *Client) loadMetadata() error { + // Load the metadata into memory and verify it + if err := c.up.Refresh(); err != nil { + // this is most likely due to the lack of metadata files + // on disk. Perform a full update and return. + return c.Refresh() } - trustedroot, ok := tufMetaMap[TrustedRootTUFPath] - if ok { - if ok, _ := validTarget(trustedrootMeta, trustedroot); ok { - return trustedroot, nil + var tm = c.up.GetTrustedMetadataSet() + if c.opts.ForceCache { + // Use cache until it expires + if tm.Timestamp.Signed.IsExpired(time.Now()) { + return c.Refresh() } - } - // What's on disk didn't match, so download from TUF remote (and cache it to disk) - writer := &Writer{ - Bytes: make([]byte, 0), - } + // Cache not expired, return + return nil + } else if c.opts.CacheValidity > 0 { + // Use cached metadata for up to CacheValidity days. + // This is a bit of an hack, as we don't know when the + // last the it was updated, fallback to check the + // modification time of timestamp.json + if tm.Timestamp.Signed.IsExpired(time.Now()) { + // Always update if the timestamp is expired + return c.Refresh() + } - err = tufClient.Download(TrustedRootTUFPath, writer) - if err != nil { - return nil, err - } + var p = filepath.Join( + c.opts.CachePath, + URLToPath(c.opts.RepositoryBaseURL), + "timestamp.json", + ) + fi, err := os.Stat(p) + if err != nil { + // Failed to get info on the file, fall back + // and update if needed + return c.Refresh() + } - err = fileJSONStore.SetMeta(TrustedRootTUFPath, writer.Bytes) - if err != nil { - return nil, err + if fi.ModTime().After(time.Now().Add( + time.Duration(-24*c.opts.CacheValidity) * time.Hour)) { + // No need to update + return nil + } + // A TUF client refresh will now happen (c.Refresh), + // update the mod time for the timestamp. + // + // Ignore the error here, there is no need to fail + // operation only because the file's metadata could + // not be updated + //nolint:errcheck + os.Chtimes(p, time.Now(), time.Now()) } - return writer.Bytes, nil + return c.Refresh() } -func checkEmbedded(tufRootURL string, fileJSONStore *filejsonstore.FileJSONStore) (json.RawMessage, error) { - embeddedRootPath := path.Join("repository", tufRootURL, RootTUFPath) +// Refresh forces a refresh of the underlying TUF client. +// As the tuf client does not support multiple refreshes during its +// life-time, this will replace the TUF client with a new one. +func (c *Client) Refresh() error { + var err error - root, err := embeddedRepos.ReadFile(embeddedRootPath) + c.up, err = updater.New(c.cfg) if err != nil { - return nil, err + return err } + return c.up.Refresh() +} - err = fileJSONStore.SetMeta(RootTUFPath, root) +// GetTarget returns a target file from the TUF repository +func (c *Client) GetTarget(target string) ([]byte, error) { + ti, err := c.up.GetTargetInfo(target) if err != nil { - return nil, err + return nil, fmt.Errorf("target %s not found: %w", target, err) } - return root, nil -} + path, tb, err := c.up.FindCachedTarget(ti, "") + if err != nil { + return nil, fmt.Errorf("error getting target cache: %w", err) + } + if path != "" { + // Cached version found + return tb, nil + } -func validTarget(expected tufdata.TargetFileMeta, localTarget []byte) (bool, error) { - got, err := tufutil.GenerateTargetFileMeta( - bytes.NewReader(localTarget), - "sha256", "sha512") + // Download of target is needed + _, tb, err = c.up.DownloadTarget(ti, "", "") if err != nil { - return false, err + return nil, fmt.Errorf("failed to download target file %s - %w", target, err) } - if err = tufutil.TargetFileMetaEqual(got, expected); err != nil { - return false, err + + return tb, nil +} + +// URLToPath converts a URL to a filename-compatible string +func URLToPath(url string) string { + // Strip scheme, replace slashes with dashes + // e.g. https://github.github.com/prod-tuf-root -> github.github.com-prod-tuf-root + fn := url + if len(fn) > 8 && fn[:8] == "https://" { + fn = fn[8:] } - return true, nil + fn = strings.ReplaceAll(fn, "/", "-") + return fn } diff --git a/pkg/tuf/options.go b/pkg/tuf/options.go new file mode 100644 index 00000000..8bd844cd --- /dev/null +++ b/pkg/tuf/options.go @@ -0,0 +1,81 @@ +// Copyright 2023 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tuf + +import ( + "embed" + "os" + "path/filepath" +) + +//go:embed repository +var embeddedRepo embed.FS + +const DefaultMirror = "https://tuf-repo-cdn.sigstore.dev" + +// Options represent the various options for a Sigstore TUF Client +// +// Note that currently, the cache control is not working. Upon initialization +// the client will *ALWAYS* perform a TUF update. +type Options struct { + // CacheValidity period in days (default 1) + CacheValidity int + // ForceCache controls if the cache should be used without update + // as long as the metadata is valid + ForceCache bool + // Root is the TUF trust anchor + Root []byte + // CachePath is the location on disk for TUF cache + // (default $HOME/.sigstore/tuf) + CachePath string + // RepositoryBaseURL is the TUF repository location URL + // (default https://tuf-repo-cdn.sigstore.dev) + RepositoryBaseURL string + // DisableLocalCache mode allows a client to work on a read-only + // files system if this is set, cache path is ignored. + DisableLocalCache bool +} + +// DefaultOptions returns an options struct for the public good instance +func DefaultOptions() (*Options, error) { + var opts Options + var err error + + opts.Root, err = DefaultRoot() + if err != nil { + return nil, err + } + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + opts.CacheValidity = 1 + opts.CachePath = filepath.Join(home, ".sigstore", "root") + opts.RepositoryBaseURL = DefaultMirror + + return &opts, nil +} + +// DefaultRoot returns the root.json for the public good instance +func DefaultRoot() ([]byte, error) { + var p = filepath.Join("repository", "root.json") + + b, err := embeddedRepo.ReadFile(p) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/pkg/tuf/repository/root.json b/pkg/tuf/repository/root.json new file mode 100644 index 00000000..ff409163 --- /dev/null +++ b/pkg/tuf/repository/root.json @@ -0,0 +1,140 @@ +{ + "signed": { + "_type": "root", + "spec_version": "1.0", + "version": 8, + "expires": "2024-03-26T04:38:55Z", + "keys": { + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXsz3SZXFb8jMV42j6pJlyjbjR8K\nN3Bwocexq6LMIb5qsWKOQvLN16NUefLc4HswOoumRsVVaajSpQS6fobkRw==\n-----END PUBLIC KEY-----\n" + } + }, + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ghrh92Lw1Yr3idGV5WqCtMDB8Cx\n+D8hdC4w2ZLNIplVRoVGLskYa3gheMyOjiJ8kPi15aQ2//7P+oj7UvJPGw==\n-----END PUBLIC KEY-----\n" + } + }, + "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELrWvNt94v4R085ELeeCMxHp7PldF\n0/T1GxukUh2ODuggLGJE0pc1e8CSBf6CS91Fwo9FUOuRsjBUld+VqSyCdQ==\n-----END PUBLIC KEY-----\n" + } + }, + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEinikSsAQmYkNeH5eYq/CnIzLaacO\nxlSaawQDOwqKy/tCqxq5xxPSJc21K4WIhs9GyOkKfzueY3GILzcMJZ4cWw==\n-----END PUBLIC KEY-----\n" + } + }, + "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWRiGr5+j+3J5SsH+Ztr5nE2H2wO7\nBV+nO3s93gLca18qTOzHY1oWyAGDykMSsGTUBSt9D+An0KfKsD2mfSM42Q==\n-----END PUBLIC KEY-----\n" + } + }, + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBzVOmHCPojMVLSI364WiiV8NPrD\n6IgRxVliskz/v+y3JER5mcVGcONliDcWMC5J2lfHmjPNPhb4H7xm8LzfSA==\n-----END PUBLIC KEY-----\n" + } + }, + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEy8XKsmhBYDI8Jc0GwzBxeKax0cm5\nSTKEU65HPFunUn41sT8pi0FjM4IkHz/YUmwmLUO0Wt7lxhj6BkLIK4qYAw==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": { + "root": { + "keyids": [ + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f", + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de" + ], + "threshold": 3 + }, + "snapshot": { + "keyids": [ + "45b283825eb184cabd582eb17b74fc8ed404f68cf452acabdad2ed6f90ce216b" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "ff51e17fcf253119b7033f6f57512631da4a0969442afcf9fc8b141c7f2be99c", + "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f", + "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de" + ], + "threshold": 3 + }, + "timestamp": { + "keyids": [ + "e1863ba02070322ebc626dcecf9d881a3a38c35c3b41a83765b6ad6c37eaec2a" + ], + "threshold": 1 + } + }, + "consistent_snapshot": true + }, + "signatures": [ + { + "keyid": "f5312f542c21273d9485a49394386c4575804770667f2ddb59b3bf0669fddd2f", + "sig": "3044022024b8036b374f7071723f3f2cb1979c42e5da1910f0b178835ad546e3c360836302207140ccd408afcf8720dd9bea7f00325768c3aa47c22d531c849c974fd50e45dd" + }, + { + "keyid": "7f7513b25429a64473e10ce3ad2f3da372bbdd14b65d07bbaf547e7c8bbbe62b", + "sig": "3046022100dcb1a96ecbfc05768a3c73726a92d681da78eaec068a9a0cfe13a12db672e44b022100a0dae7bc2e6b953e215f57cc614eb71660b9461d6dc86264b0b74a4f2e1307e1" + }, + { + "keyid": "2e61cd0cbf4a8f45809bda9f7f78c0d33ad11842ff94ae340873e2664dc843de", + "sig": "3046022100c4708d94077cb3d6dd60ebd2dd66545e7afb0464ce2593a5f23f6e3604b9f21e022100992e969cd5069eab17439b2ba60743fe422877bc1a1c46e935a6d5cb47b3cfc6" + }, + { + "keyid": "25a0eb450fd3ee2bd79218c963dce3f1cc6118badf251bf149f0bd07d5cabe99", + "sig": "3045022051faa6b6fc373730b97c1a4cd92d03efd98b83d4c9c93bf4f404d1f88ea2eb18022100f71ac1cd73dcba950f4210b12f9a05b8140b0490247c5339191e842b868155b4" + } + ] +} \ No newline at end of file