From 541a82f62d27290928cb921a2452d9a52d5feea4 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Fri, 10 Sep 2021 18:32:30 -0700 Subject: [PATCH] Add logic to detect and use ambient OIDC from exec envs. This is based on some work I have been doing here: https://github.com/mattmoor/oidc-magic At present, it is fairly tedious to use the "keyless" flow inside of environments that have a form of ambient OIDC (e.g. GKE workload identity). For example, in the context of Tekton, one needs to overlay `cosign` on an image like `docker.io/google/cloud-sdk:slim`, and then during execution have the step do something like: ```yaml command: ["/bin/sh"] args: - "-c" - | # Generate an identity token. IDENTITY_TOKEN=$(gcloud auth print-identity-token --audiences=sigstore) # Use the identity token to sign the image. cosign sign \ -identity-token $IDENTITY_TOKEN \ my.registry/the-image@sha256:deadbeef ``` This change adds support for detecting when `cosign` is executing within an environment with this kind of ambient authentication, and automatically producing one when `-identity-token` is not specified (and `COSIGN_EXPERIMENTAL=true`). This means the same signing can now be done with: ```yaml args: ["sign", "my.registry/the-image@sha256:deadbeef"] ``` This is much simpler, but also the image will be both smaller (distroless) and more portable (not just GCP, but any provider we link). Signed-off-by: Matt Moore --- KEYLESS.md | 3 ++ cmd/cosign/cli/sign.go | 14 +++++- pkg/providers/doc.go | 18 +++++++ pkg/providers/github/doc.go | 17 +++++++ pkg/providers/github/github.go | 76 ++++++++++++++++++++++++++++++ pkg/providers/google/doc.go | 17 +++++++ pkg/providers/google/google.go | 68 +++++++++++++++++++++++++++ pkg/providers/interface.go | 85 ++++++++++++++++++++++++++++++++++ 8 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 pkg/providers/doc.go create mode 100644 pkg/providers/github/doc.go create mode 100644 pkg/providers/github/github.go create mode 100644 pkg/providers/google/doc.go create mode 100644 pkg/providers/google/google.go create mode 100644 pkg/providers/interface.go diff --git a/KEYLESS.md b/KEYLESS.md index d37df05b8b5..0d4ad2d0750 100644 --- a/KEYLESS.md +++ b/KEYLESS.md @@ -65,6 +65,9 @@ In automated environments, cosign also supports directly using OIDC Identity Tok These can be supplied on the command line with the `--identity-token` flag. The `audiences` field must contain `sigstore`. +`cosign` also has support for detecting some of these automated environments +and producing an identity token. Currently this supports Google and Github. + #### On GCP From a GCE VM, you can use the VM's service account identity to sign an image: diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index da6bf1ec881..4a9d83170f3 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -44,9 +44,14 @@ import ( "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/pivkey" cremote "github.com/sigstore/cosign/pkg/cosign/remote" + "github.com/sigstore/cosign/pkg/providers" fulcioClient "github.com/sigstore/fulcio/pkg/client" "github.com/sigstore/rekor/pkg/generated/models" + // These are the ambient OIDC providers to link in. + _ "github.com/sigstore/cosign/pkg/providers/github" + _ "github.com/sigstore/cosign/pkg/providers/google" + rekorClient "github.com/sigstore/rekor/pkg/client" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" @@ -511,7 +516,14 @@ func signerFromKeyOpts(ctx context.Context, certPath string, ko KeyOpts) (*certS return nil, errors.Wrap(err, "parsing Fulcio URL") } fClient := fulcioClient.New(fulcioServer) - k, err := fulcio.NewSigner(ctx, ko.IDToken, ko.OIDCIssuer, ko.OIDCClientID, fClient) + tok := ko.IDToken + if providers.Enabled(ctx) { + tok, err = providers.Provide(ctx, "sigstore") + if err != nil { + return nil, errors.Wrap(err, "fetching ambient OIDC credentials") + } + } + k, err := fulcio.NewSigner(ctx, tok, ko.OIDCIssuer, ko.OIDCClientID, fClient) if err != nil { return nil, errors.Wrap(err, "getting key from Fulcio") } diff --git a/pkg/providers/doc.go b/pkg/providers/doc.go new file mode 100644 index 00000000000..dd431b79b82 --- /dev/null +++ b/pkg/providers/doc.go @@ -0,0 +1,18 @@ +// +// Copyright 2021 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 providers defines the APIs for providers to detect their relevance +// and register themselves to furnish OIDC tokens within a given environment. +package providers diff --git a/pkg/providers/github/doc.go b/pkg/providers/github/doc.go new file mode 100644 index 00000000000..2c31f5b521e --- /dev/null +++ b/pkg/providers/github/doc.go @@ -0,0 +1,17 @@ +// +// Copyright 2021 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 github defines a github implementation of the providers.Interface. +package github diff --git a/pkg/providers/github/github.go b/pkg/providers/github/github.go new file mode 100644 index 00000000000..db207af4356 --- /dev/null +++ b/pkg/providers/github/github.go @@ -0,0 +1,76 @@ +// +// Copyright 2021 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 github + +import ( + "context" + "encoding/json" + "net/http" + "os" + + "github.com/sigstore/cosign/pkg/providers" +) + +func init() { + providers.Register("github-actions", &githubActions{}) +} + +type githubActions struct{} + +var _ providers.Interface = (*githubActions)(nil) + +const ( + RequestTokenEnvKey = "ACTIONS_ID_TOKEN_REQUEST_TOKEN" + RequestURLEnvKey = "ACTIONS_ID_TOKEN_REQUEST_URL" +) + +// Enabled implements providers.Interface +func (ga *githubActions) Enabled(ctx context.Context) bool { + if os.Getenv(RequestTokenEnvKey) == "" { + return false + } + if os.Getenv(RequestURLEnvKey) == "" { + return false + } + return true +} + +// Provide implements providers.Interface +func (ga *githubActions) Provide(ctx context.Context, audience string) (string, error) { + url := os.Getenv(RequestURLEnvKey) + "&audience=" + audience + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + + req.Header.Add("Authorization", "bearer "+os.Getenv(RequestTokenEnvKey)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var payload struct { + Value string `json:"value"` + } + + decoder := json.NewDecoder(resp.Body) + if err := decoder.Decode(&payload); err != nil { + return "", err + } + return payload.Value, nil +} diff --git a/pkg/providers/google/doc.go b/pkg/providers/google/doc.go new file mode 100644 index 00000000000..d60d913f156 --- /dev/null +++ b/pkg/providers/google/doc.go @@ -0,0 +1,17 @@ +// +// Copyright 2021 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 google defines a google implementation of the providers.Interface. +package google diff --git a/pkg/providers/google/google.go b/pkg/providers/google/google.go new file mode 100644 index 00000000000..1e3edb0c056 --- /dev/null +++ b/pkg/providers/google/google.go @@ -0,0 +1,68 @@ +// +// Copyright 2021 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 google + +import ( + "context" + "io/ioutil" + "strings" + + "google.golang.org/api/idtoken" + + "github.com/sigstore/cosign/pkg/providers" +) + +func init() { + providers.Register("google-workload-identity", &googleWorkloadIdentity{}) +} + +type googleWorkloadIdentity struct{} + +var _ providers.Interface = (*googleWorkloadIdentity)(nil) + +// gceProductNameFile is the product file path that contains the cloud service name. +// This is a variable instead of a const to enable testing. +var gceProductNameFile = "/sys/class/dmi/id/product_name" + +// Enabled implements providers.Interface +// This is based on k8s.io/kubernetes/pkg/credentialprovider/gcp +func (gwi *googleWorkloadIdentity) Enabled(ctx context.Context) bool { + data, err := ioutil.ReadFile(gceProductNameFile) + if err != nil { + return false + } + name := strings.TrimSpace(string(data)) + if name == "Google" || name == "Google Compute Engine" { + // Just because we're on Google, does not mean workload identity is available. + // TODO(mattmoor): do something better than this. + _, err := gwi.Provide(ctx, "garbage") + return err == nil + } + return false +} + +// Provide implements providers.Interface +func (gwi *googleWorkloadIdentity) Provide(ctx context.Context, audience string) (string, error) { + ts, err := idtoken.NewTokenSource(ctx, audience) + if err != nil { + return "", err + } + tok, err := ts.Token() + if err != nil { + return "", err + } + return tok.AccessToken, nil +} diff --git a/pkg/providers/interface.go b/pkg/providers/interface.go new file mode 100644 index 00000000000..8ed4f1eaefe --- /dev/null +++ b/pkg/providers/interface.go @@ -0,0 +1,85 @@ +// +// Copyright 2021 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 providers + +import ( + "context" + "errors" + "fmt" + "sync" +) + +var ( + m sync.Mutex + providers = make(map[string]Interface) +) + +// Interface is what providers need to implement to participate in furnishing OIDC tokens. +type Interface interface { + // Enabled returns true if the provider is enabled. + Enabled(ctx context.Context) bool + + // Provide returns an OIDC token scoped to the provided audience. + Provide(ctx context.Context, audience string) (string, error) +} + +// Register is used by providers to participate in furnishing OIDC tokens. +func Register(name string, p Interface) { + m.Lock() + defer m.Unlock() + + if prev, ok := providers[name]; ok { + panic(fmt.Sprintf("duplicate provider for name %q, %T and %T", name, prev, p)) + } + providers[name] = p +} + +// Enabled checks whether any of the registered providers are enabled in this execution context. +func Enabled(ctx context.Context) bool { + m.Lock() + defer m.Unlock() + + for _, provider := range providers { + if provider.Enabled(ctx) { + return true + } + } + return false +} + +// Provide fetches an OIDC token from one of the active providers. +func Provide(ctx context.Context, audience string) (string, error) { + m.Lock() + defer m.Unlock() + + var id string + var err error + for _, provider := range providers { + if !provider.Enabled(ctx) { + continue + } + id, err = provider.Provide(ctx, audience) + if err == nil { + return id, err + } + } + // return the last id/err combo, unless there wasn't an error in + // which case provider.Enabled() wasn't checked. + if err == nil { + err = errors.New("no providers are enabled, check providers.Enabled() before providers.Provide()") + } + return id, err +}