diff --git a/pkg/oauthflow/client_credentials.go b/pkg/oauthflow/client_credentials.go new file mode 100644 index 000000000..8a1a4e0a0 --- /dev/null +++ b/pkg/oauthflow/client_credentials.go @@ -0,0 +1,156 @@ +// +// 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 oauthflow + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +// CodeURL fetches the client credentials token authorization endpoint URL from the provider's well-known configuration endpoint +func (d *DefaultFlowClientCredentials) CodeURL() (string, error) { + if d.codeURL != "" { + return d.codeURL, nil + } + + wellKnown := strings.TrimSuffix(d.Issuer, "/") + "/.well-known/openid-configuration" + /* #nosec */ + httpClient := &http.Client{ + Timeout: 3 * time.Second, + } + resp, err := httpClient.Get(wellKnown) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("unable to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("%s: %s", resp.Status, body) + } + + providerConfig := struct { + Issuer string `json:"issuer"` + TokenEndpoint string `json:"token_endpoint"` + }{} + if err = json.Unmarshal(body, &providerConfig); err != nil { + return "", fmt.Errorf("oidc: failed to decode provider discovery object: %w", err) + } + + if d.Issuer != providerConfig.Issuer { + return "", fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", d.Issuer, providerConfig.Issuer) + } + + if providerConfig.TokenEndpoint == "" { + return "", fmt.Errorf("oidc: client credentials token authorization endpoint not returned by provider") + } + + d.codeURL = providerConfig.TokenEndpoint + return d.codeURL, nil +} + +// DefaultFlowClientCredentials fetches an OIDC Identity token using the Client Credentials Grant flow as specified in RFC8628 +type DefaultFlowClientCredentials struct { + Issuer string + codeURL string +} + +// NewClientCredentialsFlow creates a new DefaultFlowClientCredentials that retrieves an OIDC Identity Token using a Client Credentials Grant +func NewClientCredentialsFlow(issuer string) *DefaultFlowClientCredentials { + return &DefaultFlowClientCredentials{ + Issuer: issuer, + } +} + +func (d *DefaultFlowClientCredentials) clientCredentialsFlow(_ *oidc.Provider, clientID, clientSecret, redirectURL string) (string, error) { + data := url.Values{ + "client_id": []string{clientID}, + "client_secret": []string{clientSecret}, + "scope": []string{"openid email"}, + "grant_type": []string{"client_credentials"}, + } + if redirectURL != "" { + // If a redirect uri is provided then use it + data["redirect_uri"] = []string{redirectURL} + } + + codeURL, err := d.CodeURL() + if err != nil { + return "", err + } + /* #nosec */ + resp, err := http.PostForm(codeURL, data) + if err != nil { + return "", err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("%s: %s", resp.Status, b) + } + + tr := tokenResp{} + if err := json.Unmarshal(b, &tr); err != nil { + return "", err + } + + if tr.IDToken != "" { + fmt.Println("Token received!") + return tr.IDToken, nil + } + + return "", fmt.Errorf("unexpected error in client flow: %s", tr.Error) +} + +// GetIDToken gets an OIDC ID Token from the specified provider using the Client Credentials Grant flow +func (d *DefaultFlowClientCredentials) GetIDToken(p *oidc.Provider, cfg oauth2.Config) (*OIDCIDToken, error) { + idToken, err := d.clientCredentialsFlow(p, cfg.ClientID, cfg.ClientSecret, cfg.RedirectURL) + if err != nil { + return nil, err + } + verifier := p.Verifier(&oidc.Config{ClientID: cfg.ClientID}) + parsedIDToken, err := verifier.Verify(context.Background(), idToken) + if err != nil { + return nil, err + } + + subj, err := SubjectFromToken(parsedIDToken) + if err != nil { + return nil, err + } + + return &OIDCIDToken{ + RawString: idToken, + Subject: subj, + }, nil +} diff --git a/pkg/oauthflow/client_credentials_test.go b/pkg/oauthflow/client_credentials_test.go new file mode 100644 index 000000000..b65f6a3fe --- /dev/null +++ b/pkg/oauthflow/client_credentials_test.go @@ -0,0 +1,91 @@ +// +// 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 oauthflow + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/coreos/go-oidc/v3/oidc" +) + +type testccDriver struct { + respCh chan interface{} + t *testing.T +} + +func (td *testccDriver) handler(w http.ResponseWriter, r *http.Request) { + td.t.Log("got request:", r.URL.Path) + if r.URL.Path == "/.well-known/openid-configuration" { + _, _ = w.Write([]byte(strings.ReplaceAll(wellKnownOIDCConfig, "ISSUER", fmt.Sprintf("http://%s", r.Host)))) + return + } + nextReply := <-td.respCh + b, err := json.Marshal(nextReply) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + switch r.URL.Path { + case "/token": + _, _ = w.Write(b) + default: + w.WriteHeader(http.StatusBadRequest) + return + } +} + +func TestClientCredentialsFlowTokenGetter_ccFlow(t *testing.T) { + td := testccDriver{ + respCh: make(chan interface{}, 3), + t: t, + } + + ts := httptest.NewServer(http.HandlerFunc(td.handler)) + defer ts.Close() + + dtg := DefaultFlowClientCredentials{ + Issuer: ts.URL, + } + p, pErr := oidc.NewProvider(context.Background(), ts.URL) + if pErr != nil { + t.Fatal(pErr) + } + + tokenCh, errCh := make(chan string), make(chan error) + go func() { + token, err := dtg.clientCredentialsFlow(p, "sigstore", "", "") + tokenCh <- token + errCh <- err + }() + + td.respCh <- tokenResponse("mytoken", "") + + token := <-tokenCh + err := <-errCh + if err != nil { + t.Fatal(err) + } + if token != "mytoken" { + t.Fatal("expected mytoken") + } +} diff --git a/pkg/signature/kms/azure/client_test.go b/pkg/signature/kms/azure/client_test.go index 6c02e87d3..0f961e047 100644 --- a/pkg/signature/kms/azure/client_test.go +++ b/pkg/signature/kms/azure/client_test.go @@ -71,17 +71,17 @@ func (c *testKVClient) Verify(_ context.Context, _, _ string, _ azkeys.VerifyPar type keyNotFoundClient struct { testKVClient - key azkeys.JSONWebKey - getKeyReturnsErr bool + key azkeys.JSONWebKey + getKeyReturnsErr bool getKeyCallThreshold int - getKeyCallCount int + getKeyCallCount int } func (c *keyNotFoundClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (azkeys.GetKeyResponse, error) { if c.getKeyReturnsErr && c.getKeyCallCount < c.getKeyCallThreshold { c.getKeyCallCount++ return azkeys.GetKeyResponse{}, &azcore.ResponseError{ - StatusCode: http.StatusNotFound, + StatusCode: http.StatusNotFound, RawResponse: &http.Response{}, } } @@ -95,7 +95,7 @@ func (c *keyNotFoundClient) GetKey(_ context.Context, _, _ string, _ *azkeys.Get type nonResponseErrClient struct { testKVClient - keyCache *ttlcache.Cache[string, crypto.PublicKey] + keyCache *ttlcache.Cache[string, crypto.PublicKey] } func (c *nonResponseErrClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (result azkeys.GetKeyResponse, err error) { @@ -105,7 +105,7 @@ func (c *nonResponseErrClient) GetKey(_ context.Context, _, _ string, _ *azkeys. type non404RespClient struct { testKVClient - keyCache *ttlcache.Cache[string, crypto.PublicKey] + keyCache *ttlcache.Cache[string, crypto.PublicKey] } func (c *non404RespClient) GetKey(_ context.Context, _, _ string, _ *azkeys.GetKeyOptions) (result azkeys.GetKeyResponse, err error) { @@ -212,8 +212,8 @@ func TestAzureVaultClientFetchPublicKey(t *testing.T) { func TestAzureVaultClientCreateKey(t *testing.T) { type test struct { - name string - client kvClient + name string + client kvClient expectSuccess bool } @@ -226,27 +226,27 @@ func TestAzureVaultClientCreateKey(t *testing.T) { { name: "Successfully create key if it doesn't exist", client: &keyNotFoundClient{ - key: key, - getKeyReturnsErr: true, + key: key, + getKeyReturnsErr: true, getKeyCallThreshold: 1, }, expectSuccess: true, }, { name: "Return public key if it already exists", - client: &testKVClient{ + client: &testKVClient{ key: key, }, expectSuccess: true, }, { - name: "Fail to create key due to unknown error", - client: &nonResponseErrClient{}, + name: "Fail to create key due to unknown error", + client: &nonResponseErrClient{}, expectSuccess: false, }, { - name: "Fail to create key due to non-404 status code error", - client: &non404RespClient{}, + name: "Fail to create key due to non-404 status code error", + client: &non404RespClient{}, expectSuccess: false, }, } diff --git a/pkg/signature/kms/azure/integration_test.go b/pkg/signature/kms/azure/integration_test.go index 63f7a6f2f..2f8080181 100644 --- a/pkg/signature/kms/azure/integration_test.go +++ b/pkg/signature/kms/azure/integration_test.go @@ -117,7 +117,7 @@ func TestCreateKey(t *testing.T) { if azureVaultURL == "" { t.Fatalf("VAULT_URL must be set") } - + newKeyRef := fmt.Sprintf("azurekms://%s.vault.azure.net/%s", azureVaultURL, "new-test-key") sv, err := LoadSignerVerifier(context.Background(), newKeyRef) diff --git a/test/fuzz/pem/fuzzcert_test.go b/test/fuzz/pem/fuzzcert_test.go index b93c5a479..14b44cccc 100644 --- a/test/fuzz/pem/fuzzcert_test.go +++ b/test/fuzz/pem/fuzzcert_test.go @@ -18,8 +18,9 @@ package pem import ( "bytes" "encoding/pem" - "github.com/sigstore/sigstore/pkg/cryptoutils" "testing" + + "github.com/sigstore/sigstore/pkg/cryptoutils" ) func FuzzLoadCertificates(f *testing.F) { diff --git a/test/fuzz/signature/fuzz_signature_test.go b/test/fuzz/signature/fuzz_signature_test.go index c998d373f..26ef303b9 100644 --- a/test/fuzz/signature/fuzz_signature_test.go +++ b/test/fuzz/signature/fuzz_signature_test.go @@ -33,7 +33,6 @@ import ( func FuzzECDSASigner(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { - x := ecdsa.PrivateKey{} z := new(big.Int) z.SetBytes(data) @@ -63,6 +62,7 @@ func FuzzECDSASigner(f *testing.F) { } }) } + func FuzzComputeDigest(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { hashFuncs := []crypto.Hash{ diff --git a/test/fuzz/tools.go b/test/fuzz/tools.go index 37215cfcc..454d133c8 100644 --- a/test/fuzz/tools.go +++ b/test/fuzz/tools.go @@ -1,3 +1,4 @@ +//go:build tools // +build tools // Copyright 2021 The Sigstore Authors.