Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add logic to detect and use ambient OIDC from exec envs. #644

Merged
merged 1 commit into from
Sep 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions KEYLESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 13 additions & 1 deletion cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/providers/doc.go
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions pkg/providers/github/doc.go
Original file line number Diff line number Diff line change
@@ -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
76 changes: 76 additions & 0 deletions pkg/providers/github/github.go
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions pkg/providers/google/doc.go
Original file line number Diff line number Diff line change
@@ -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
68 changes: 68 additions & 0 deletions pkg/providers/google/google.go
Original file line number Diff line number Diff line change
@@ -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
}
85 changes: 85 additions & 0 deletions pkg/providers/interface.go
Original file line number Diff line number Diff line change
@@ -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
}