diff --git a/.gitignore b/.gitignore index 7ee7edd..ee81e53 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /tufdata /conformance /pkg/tuf/testing.local.json +/examples/oci-image-verification/oci-image-verification +/examples/sigstore-go-signing/sigstore-go-signing diff --git a/examples/oci-image-verification/go.mod b/examples/oci-image-verification/go.mod index a767631..97d9c2d 100644 --- a/examples/oci-image-verification/go.mod +++ b/examples/oci-image-verification/go.mod @@ -6,7 +6,7 @@ replace github.com/sigstore/sigstore-go => ../../ require ( github.com/google/go-containerregistry v0.20.2 - github.com/sigstore/protobuf-specs v0.3.2 + github.com/sigstore/protobuf-specs v0.3.3-0.20241218070516-b4ea46eb6f7b github.com/sigstore/sigstore v1.8.11 github.com/sigstore/sigstore-go v0.6.2 ) @@ -91,7 +91,7 @@ require ( golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect - google.golang.org/protobuf v1.35.2 // indirect + google.golang.org/protobuf v1.36.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/examples/oci-image-verification/go.sum b/examples/oci-image-verification/go.sum index 0e7ab80..2f4ec3e 100644 --- a/examples/oci-image-verification/go.sum +++ b/examples/oci-image-verification/go.sum @@ -281,8 +281,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= -github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo= -github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA= +github.com/sigstore/protobuf-specs v0.3.3-0.20241218070516-b4ea46eb6f7b h1:via4M7tXznyLDUoj7oi71z2Id3x0xM1xffb0+0Ous6w= +github.com/sigstore/protobuf-specs v0.3.3-0.20241218070516-b4ea46eb6f7b/go.mod h1:FaGuR+Emg7JTQ82axlgsQj+3h6Xzu/N79F64TYmMz5s= github.com/sigstore/rekor v1.3.7 h1:Z5UW5TmqbTZnyOFkMRfi32q/CWcxK6VuzIkx+33mbq8= github.com/sigstore/rekor v1.3.7/go.mod h1:TihqJscZ6L6398x68EHY82t0AOnGYfrQ0siXe3WgbR4= github.com/sigstore/sigstore v1.8.11 h1:tEqeQqbT+awtM87ec9KEeSUxT/AFvJNawneYJyAkFrQ= @@ -396,8 +396,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/examples/sigstore-go-signing/main.go b/examples/sigstore-go-signing/main.go index 21a271b..faa5ac2 100644 --- a/examples/sigstore-go-signing/main.go +++ b/examples/sigstore-go-signing/main.go @@ -96,12 +96,18 @@ func main() { log.Fatal(err) } - trustedRootJSON, err := tufClient.GetTarget("trusted_root.json") + trustedRoot, err := root.GetTrustedRoot(tufClient) if err != nil { log.Fatal(err) } - trustedRoot, err := root.NewTrustedRootFromJSON(trustedRootJSON) + // TODO: Uncomment once the TUF staging root distributes this file + // signingConfig, err := root.GetSigningConfig(tufClient) + signingConfig, err := root.NewSigningConfig(root.SigningConfigMediaType01, + "https://fulcio.sigstage.dev", + "https://oauth2.sigstage.dev/auth", + []string{"https://rekor.sigstage.dev"}, + []string{"https://timestamp.githubapp.com/api/v1/timestamp"}) if err != nil { log.Fatal(err) } @@ -110,7 +116,7 @@ func main() { if *idToken != "" { fulcioOpts := &sign.FulcioOptions{ - BaseURL: "https://fulcio.sigstage.dev", + BaseURL: signingConfig.FulcioCertificateAuthorityURL(), Timeout: time.Duration(30 * time.Second), Retries: 1, } @@ -121,24 +127,28 @@ func main() { } if *tsa { - tsaOpts := &sign.TimestampAuthorityOptions{ - URL: "https://timestamp.githubapp.com/api/v1/timestamp", - Timeout: time.Duration(30 * time.Second), - Retries: 1, + for _, tsaURL := range signingConfig.TimestampAuthorityURLs() { + tsaOpts := &sign.TimestampAuthorityOptions{ + URL: tsaURL, + Timeout: time.Duration(30 * time.Second), + Retries: 1, + } + opts.TimestampAuthorities = append(opts.TimestampAuthorities, sign.NewTimestampAuthority(tsaOpts)) } - opts.TimestampAuthorities = append(opts.TimestampAuthorities, sign.NewTimestampAuthority(tsaOpts)) // staging TUF repo doesn't have accessible timestamp authorities opts.TrustedRoot = nil } if *rekor { - rekorOpts := &sign.RekorOptions{ - BaseURL: "https://rekor.sigstage.dev", - Timeout: time.Duration(90 * time.Second), - Retries: 1, + for _, rekorURL := range signingConfig.RekorLogURLs() { + rekorOpts := &sign.RekorOptions{ + BaseURL: rekorURL, + Timeout: time.Duration(90 * time.Second), + Retries: 1, + } + opts.TransparencyLogs = append(opts.TransparencyLogs, sign.NewRekor(rekorOpts)) } - opts.TransparencyLogs = append(opts.TransparencyLogs, sign.NewRekor(rekorOpts)) } bundle, err := sign.Bundle(content, keypair, opts) diff --git a/go.mod b/go.mod index 179da48..c83a087 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/in-toto/attestation v1.1.0 github.com/in-toto/in-toto-golang v0.9.0 github.com/secure-systems-lab/go-securesystemslib v0.9.0 - github.com/sigstore/protobuf-specs v0.3.2 + github.com/sigstore/protobuf-specs v0.3.3-0.20241218070516-b4ea46eb6f7b github.com/sigstore/rekor v1.3.7 github.com/sigstore/sigstore v1.8.11 github.com/sigstore/timestamp-authority v1.2.3 @@ -22,7 +22,7 @@ require ( github.com/theupdateframework/go-tuf/v2 v2.0.2 golang.org/x/crypto v0.31.0 golang.org/x/mod v0.22.0 - google.golang.org/protobuf v1.35.2 + google.golang.org/protobuf v1.36.0 ) require ( diff --git a/go.sum b/go.sum index d2b7eec..8ec6757 100644 --- a/go.sum +++ b/go.sum @@ -285,8 +285,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= -github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo= -github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA= +github.com/sigstore/protobuf-specs v0.3.3-0.20241218070516-b4ea46eb6f7b h1:via4M7tXznyLDUoj7oi71z2Id3x0xM1xffb0+0Ous6w= +github.com/sigstore/protobuf-specs v0.3.3-0.20241218070516-b4ea46eb6f7b/go.mod h1:FaGuR+Emg7JTQ82axlgsQj+3h6Xzu/N79F64TYmMz5s= github.com/sigstore/rekor v1.3.7 h1:Z5UW5TmqbTZnyOFkMRfi32q/CWcxK6VuzIkx+33mbq8= github.com/sigstore/rekor v1.3.7/go.mod h1:TihqJscZ6L6398x68EHY82t0AOnGYfrQ0siXe3WgbR4= github.com/sigstore/sigstore v1.8.11 h1:tEqeQqbT+awtM87ec9KEeSUxT/AFvJNawneYJyAkFrQ= @@ -441,8 +441,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/root/signing_config.go b/pkg/root/signing_config.go new file mode 100644 index 0000000..4966b28 --- /dev/null +++ b/pkg/root/signing_config.go @@ -0,0 +1,130 @@ +// Copyright 2024 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 root + +import ( + "fmt" + "os" + + prototrustroot "github.com/sigstore/protobuf-specs/gen/pb-go/trustroot/v1" + "github.com/sigstore/sigstore-go/pkg/tuf" + "google.golang.org/protobuf/encoding/protojson" +) + +const SigningConfigMediaType01 = "application/vnd.dev.sigstore.signingconfig.v0.1+json" + +type SigningConfig struct { + signingConfig *prototrustroot.SigningConfig +} + +func (sc *SigningConfig) FulcioCertificateAuthorityURL() string { + return sc.signingConfig.GetCaUrl() +} + +func (sc *SigningConfig) OIDCProviderURL() string { + return sc.signingConfig.GetOidcUrl() +} + +func (sc *SigningConfig) RekorLogURLs() []string { + return sc.signingConfig.GetTlogUrls() +} + +func (sc *SigningConfig) TimestampAuthorityURLs() []string { + return sc.signingConfig.GetTsaUrls() +} + +// NewSigningConfig initializes a SigningConfig object from a mediaType string, Fulcio certificate +// authority URL, OIDC provider URL, list of Rekor transpraency log URLs, and a list of +// timestamp authorities. +func NewSigningConfig(mediaType string, + fulcioCertificateAuthority string, + oidcProvider string, + rekorLogs []string, + timestampAuthorities []string) (*SigningConfig, error) { + if mediaType != SigningConfigMediaType01 { + return nil, fmt.Errorf("unsupported SigningConfig media type, must be: %s", SigningConfigMediaType01) + } + sc := &SigningConfig{ + signingConfig: &prototrustroot.SigningConfig{ + MediaType: mediaType, + CaUrl: fulcioCertificateAuthority, + OidcUrl: oidcProvider, + TlogUrls: rekorLogs, + TsaUrls: timestampAuthorities, + }, + } + return sc, nil +} + +// NewSigningConfigFromProtobuf returns a Sigstore signing configuration. +func NewSigningConfigFromProtobuf(sc *prototrustroot.SigningConfig) (*SigningConfig, error) { + if sc.GetMediaType() != SigningConfigMediaType01 { + return nil, fmt.Errorf("unsupported SigningConfig media type: %s", sc.GetMediaType()) + } + return &SigningConfig{signingConfig: sc}, nil +} + +// NewSigningConfigFromPath returns a Sigstore signing configuration from a file. +func NewSigningConfigFromPath(path string) (*SigningConfig, error) { + scJSON, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return NewSigningConfigFromJSON(scJSON) +} + +// NewSigningConfigFromJSON returns a Sigstore signing configuration from JSON. +func NewSigningConfigFromJSON(rootJSON []byte) (*SigningConfig, error) { + pbSC, err := NewSigningConfigProtobuf(rootJSON) + if err != nil { + return nil, err + } + + return NewSigningConfigFromProtobuf(pbSC) +} + +// NewSigningConfigProtobuf returns a Sigstore signing configuration as a protobuf. +func NewSigningConfigProtobuf(scJSON []byte) (*prototrustroot.SigningConfig, error) { + pbSC := &prototrustroot.SigningConfig{} + err := protojson.Unmarshal(scJSON, pbSC) + if err != nil { + return nil, err + } + return pbSC, nil +} + +// FetchSigningConfig fetches the public-good Sigstore signing configuration from TUF. +func FetchSigningConfig() (*SigningConfig, error) { + return FetchSigningConfigWithOptions(tuf.DefaultOptions()) +} + +// FetchSigningConfig fetches the public-good Sigstore signing configuration with the given options from TUF. +func FetchSigningConfigWithOptions(opts *tuf.Options) (*SigningConfig, error) { + client, err := tuf.New(opts) + if err != nil { + return nil, err + } + return GetSigningConfig(client) +} + +// FetchSigningConfig fetches the public-good Sigstore signing configuration target from TUF. +func GetSigningConfig(c *tuf.Client) (*SigningConfig, error) { + jsonBytes, err := c.GetTarget("signing_config.json") + if err != nil { + return nil, err + } + return NewSigningConfigFromJSON(jsonBytes) +} diff --git a/pkg/root/signing_config_test.go b/pkg/root/signing_config_test.go new file mode 100644 index 0000000..736d237 --- /dev/null +++ b/pkg/root/signing_config_test.go @@ -0,0 +1,215 @@ +// Copyright 2024 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 root + +import ( + "reflect" + "testing" + + prototrustroot "github.com/sigstore/protobuf-specs/gen/pb-go/trustroot/v1" +) + +func TestSigningConfig_FulcioCertificateAuthorityURL(t *testing.T) { + tests := []struct { + name string + signingConfig *prototrustroot.SigningConfig + want string + }{ + { + name: "valid", + signingConfig: &prototrustroot.SigningConfig{ + CaUrl: "https://fulcio.sigstore.dev", + }, + want: "https://fulcio.sigstore.dev", + }, + { + name: "empty", + signingConfig: &prototrustroot.SigningConfig{ + CaUrl: "", + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &SigningConfig{ + signingConfig: tt.signingConfig, + } + if got := sc.FulcioCertificateAuthorityURL(); got != tt.want { + t.Errorf("SigningConfig.FulcioCertificateAuthorityURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigningConfig_OIDCProviderURL(t *testing.T) { + tests := []struct { + name string + signingConfig *prototrustroot.SigningConfig + want string + }{ + { + name: "valid", + signingConfig: &prototrustroot.SigningConfig{ + OidcUrl: "https://oauth2.sigstore.dev/auth", + }, + want: "https://oauth2.sigstore.dev/auth", + }, + { + name: "empty", + signingConfig: &prototrustroot.SigningConfig{ + OidcUrl: "", + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &SigningConfig{ + signingConfig: tt.signingConfig, + } + if got := sc.OIDCProviderURL(); got != tt.want { + t.Errorf("SigningConfig.OIDCProviderURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigningConfig_RekorLogURLs(t *testing.T) { + tests := []struct { + name string + signingConfig *prototrustroot.SigningConfig + want []string + }{ + { + name: "valid", + signingConfig: &prototrustroot.SigningConfig{ + TlogUrls: []string{"https://rekor.sigstore.dev", "https://2025.rekor.sigstore.dev"}, + }, + want: []string{"https://rekor.sigstore.dev", "https://2025.rekor.sigstore.dev"}, + }, + { + name: "empty", + signingConfig: &prototrustroot.SigningConfig{ + TlogUrls: []string{}, + }, + want: []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &SigningConfig{ + signingConfig: tt.signingConfig, + } + if got := sc.RekorLogURLs(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SigningConfig.RekorLogURLs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigningConfig_TimestampAuthorityURLs(t *testing.T) { + tests := []struct { + name string + signingConfig *prototrustroot.SigningConfig + want []string + }{ + { + name: "valid", + signingConfig: &prototrustroot.SigningConfig{ + TsaUrls: []string{"https://tsa1.sigstore.dev", "https://tsa2.sigstore.dev"}, + }, + want: []string{"https://tsa1.sigstore.dev", "https://tsa2.sigstore.dev"}, + }, + { + name: "empty", + signingConfig: &prototrustroot.SigningConfig{ + TsaUrls: []string{}, + }, + want: []string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &SigningConfig{ + signingConfig: tt.signingConfig, + } + if got := sc.TimestampAuthorityURLs(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SigningConfig.TimestampAuthorityURLs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewSigningConfig(t *testing.T) { + type args struct { + mediaType string + fulcioCertificateAuthority string + oidcProvider string + rekorLogs []string + timestampAuthorities []string + } + tests := []struct { + name string + args args + want *SigningConfig + wantErr bool + }{ + { + name: "valid", + args: args{ + mediaType: SigningConfigMediaType01, + fulcioCertificateAuthority: "https://fulcio.sigstore.dev", + oidcProvider: "https://oauth2.sigstore.dev/auth", + rekorLogs: []string{"https://rekor.sigstore.dev"}, + timestampAuthorities: []string{"https://tsa.sigstore.dev"}, + }, + want: &SigningConfig{ + signingConfig: &prototrustroot.SigningConfig{ + MediaType: SigningConfigMediaType01, + CaUrl: "https://fulcio.sigstore.dev", + OidcUrl: "https://oauth2.sigstore.dev/auth", + TlogUrls: []string{"https://rekor.sigstore.dev"}, + TsaUrls: []string{"https://tsa.sigstore.dev"}, + }, + }, + wantErr: false, + }, + { + name: "invalid media type", + args: args{ + mediaType: "application/json", + fulcioCertificateAuthority: "https://fulcio.sigstore.dev", + oidcProvider: "https://oauth2.sigstore.dev/auth", + rekorLogs: []string{"https://rekor.sigstore.dev"}, + timestampAuthorities: []string{"https://tsa.sigstore.dev"}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewSigningConfig(tt.args.mediaType, tt.args.fulcioCertificateAuthority, tt.args.oidcProvider, tt.args.rekorLogs, tt.args.timestampAuthorities) + if (err != nil) != tt.wantErr { + t.Errorf("NewSigningConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewSigningConfig() = %v, want %v", got, tt.want) + } + }) + } +}