From f86e08c1b959780239f6b1f47d96de429c3f0dbb Mon Sep 17 00:00:00 2001 From: Noah Kreiger Date: Sun, 4 Feb 2024 19:41:54 -0500 Subject: [PATCH 1/5] add client credential flow Signed-off-by: Noah Kreiger --- pkg/oauthflow/client_credentials.go | 140 +++++++++++++++++++++++ pkg/oauthflow/client_credentials_test.go | 95 +++++++++++++++ pkg/oauthflow/device_test.go | 2 + 3 files changed, 237 insertions(+) create mode 100644 pkg/oauthflow/client_credentials.go create mode 100644 pkg/oauthflow/client_credentials_test.go diff --git a/pkg/oauthflow/client_credentials.go b/pkg/oauthflow/client_credentials.go new file mode 100644 index 000000000..21cbabc19 --- /dev/null +++ b/pkg/oauthflow/client_credentials.go @@ -0,0 +1,140 @@ +package oauthflow + +import ( + "context" + "encoding/json" + "fmt" + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// 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..ca534473d --- /dev/null +++ b/pkg/oauthflow/client_credentials_test.go @@ -0,0 +1,95 @@ +// +// 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 oauthflow + +import ( + "context" + "encoding/json" + "fmt" + "github.com/coreos/go-oidc/v3/oidc" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +type testccDriver struct { + msgs []string + respCh chan interface{} + t *testing.T +} + +func (td *testccDriver) writeMsg(s string) { + td.msgs = append(td.msgs, s) +} + +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/oauthflow/device_test.go b/pkg/oauthflow/device_test.go index a2edd7046..f4972da24 100644 --- a/pkg/oauthflow/device_test.go +++ b/pkg/oauthflow/device_test.go @@ -106,6 +106,8 @@ func (td *testDriver) handler(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/token", "/device/code": _, _ = w.Write(b) + case "/token/authorize": + _, _ = w.Write(b) default: w.WriteHeader(http.StatusBadRequest) return From 72de717abcceb5f8e7211c0895223f55d6ec3d8f Mon Sep 17 00:00:00 2001 From: Noah Kreiger Date: Sun, 4 Feb 2024 19:42:31 -0500 Subject: [PATCH 2/5] cleanup test Signed-off-by: Noah Kreiger --- pkg/oauthflow/device_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/oauthflow/device_test.go b/pkg/oauthflow/device_test.go index f4972da24..a2edd7046 100644 --- a/pkg/oauthflow/device_test.go +++ b/pkg/oauthflow/device_test.go @@ -106,8 +106,6 @@ func (td *testDriver) handler(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/token", "/device/code": _, _ = w.Write(b) - case "/token/authorize": - _, _ = w.Write(b) default: w.WriteHeader(http.StatusBadRequest) return From 0098a5ae1b83b6f90393677b471a342a851c6c42 Mon Sep 17 00:00:00 2001 From: Noah Kreiger Date: Mon, 5 Feb 2024 10:47:25 -0500 Subject: [PATCH 3/5] fix license header Signed-off-by: Noah Kreiger --- pkg/oauthflow/client_credentials_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/oauthflow/client_credentials_test.go b/pkg/oauthflow/client_credentials_test.go index ca534473d..7e5ff7f22 100644 --- a/pkg/oauthflow/client_credentials_test.go +++ b/pkg/oauthflow/client_credentials_test.go @@ -1,5 +1,5 @@ // -// Copyright 2021 The Sigstore Authors. +// 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. From 885314187287500fe7f820607de6074d03530861 Mon Sep 17 00:00:00 2001 From: Noah Kreiger Date: Mon, 5 Feb 2024 10:49:06 -0500 Subject: [PATCH 4/5] add license header Signed-off-by: Noah Kreiger --- pkg/oauthflow/client_credentials.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/oauthflow/client_credentials.go b/pkg/oauthflow/client_credentials.go index 21cbabc19..1ce9caef5 100644 --- a/pkg/oauthflow/client_credentials.go +++ b/pkg/oauthflow/client_credentials.go @@ -1,3 +1,18 @@ +// +// 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 ( From 10d5c9b162cdbe192c1dba4a633c8c87916e1f8a Mon Sep 17 00:00:00 2001 From: Noah Kreiger Date: Mon, 5 Feb 2024 11:42:16 -0500 Subject: [PATCH 5/5] fix linting issues Signed-off-by: Noah Kreiger --- pkg/oauthflow/client_credentials.go | 5 ++-- pkg/oauthflow/client_credentials_test.go | 8 ++---- pkg/signature/kms/azure/client_test.go | 30 ++++++++++----------- pkg/signature/kms/azure/integration_test.go | 2 +- test/fuzz/pem/fuzzcert_test.go | 3 ++- test/fuzz/signature/fuzz_signature_test.go | 2 +- test/fuzz/tools.go | 1 + 7 files changed, 25 insertions(+), 26 deletions(-) diff --git a/pkg/oauthflow/client_credentials.go b/pkg/oauthflow/client_credentials.go index 1ce9caef5..8a1a4e0a0 100644 --- a/pkg/oauthflow/client_credentials.go +++ b/pkg/oauthflow/client_credentials.go @@ -19,13 +19,14 @@ import ( "context" "encoding/json" "fmt" - "github.com/coreos/go-oidc/v3/oidc" - "golang.org/x/oauth2" "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 diff --git a/pkg/oauthflow/client_credentials_test.go b/pkg/oauthflow/client_credentials_test.go index 7e5ff7f22..b65f6a3fe 100644 --- a/pkg/oauthflow/client_credentials_test.go +++ b/pkg/oauthflow/client_credentials_test.go @@ -19,23 +19,19 @@ import ( "context" "encoding/json" "fmt" - "github.com/coreos/go-oidc/v3/oidc" "net/http" "net/http/httptest" "strings" "testing" + + "github.com/coreos/go-oidc/v3/oidc" ) type testccDriver struct { - msgs []string respCh chan interface{} t *testing.T } -func (td *testccDriver) writeMsg(s string) { - td.msgs = append(td.msgs, s) -} - 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" { 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.