diff --git a/.golangci.yml b/.golangci.yml index 1cad27edae..50dac6c54a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -57,6 +57,9 @@ linters: - whitespace - zerologlint settings: + govet: + disable: + - inline misspell: ignore-words: - cancelled diff --git a/.tekton/go.yaml b/.tekton/go.yaml index 4c721477b5..d8a1ce3cd9 100644 --- a/.tekton/go.yaml +++ b/.tekton/go.yaml @@ -62,6 +62,8 @@ spec: value: $(workspaces.source.path)/go-build-cache/cache - name: GOMODCACHE value: $(workspaces.source.path)/go-build-cache/mod + - name: GOTOOLCHAIN + value: go1.24.13 workingDir: $(workspaces.source.path) script: | #!/usr/bin/env bash @@ -82,6 +84,8 @@ spec: value: $(workspaces.source.path)/go-build-cache/cache - name: GOMODCACHE value: $(workspaces.source.path)/go-build-cache/mod + - name: GOTOOLCHAIN + value: go1.24.13 - name: GITHUB_REPOSITORY value: "{{repo_owner}}/{{repo_name}}" - name: GITHUB_PULL_REQUEST_ID @@ -97,13 +101,15 @@ spec: chmod +x ./codecov ./codecov -P $GITHUB_PULL_REQUEST_ID -C {{revision}} -v - name: lint - image: golangci/golangci-lint:latest + image: golang:1.24 workingDir: $(workspaces.source.path) env: - name: GOCACHE value: $(workspaces.source.path)/go-build-cache/cache - name: GOMODCACHE value: $(workspaces.source.path)/go-build-cache/mod + - name: GOTOOLCHAIN + value: go1.24.13 - name: GOLANGCILINT_CACHE value: $(workspaces.source.path)/go-build-cache/golangci-cache script: | diff --git a/Makefile b/Makefile index 935be90e12..81be39160e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ TARGET_NAMESPACE=pipelines-as-code HUGO_VERSION=0.96.0 -GOLANGCI_LINT=golangci-lint +GOLANGCI_LINT_VERSION ?= v2.12.2 +GO_TOOLCHAIN ?= go1.24.13 GOFUMPT=gofumpt TKN_BINARY_NAME := tkn TKN_BINARY_URL := https://tekton.dev/docs/cli/\#installation @@ -11,11 +12,20 @@ TIMEOUT_UNIT = 20m TIMEOUT_E2E = 45m DEFAULT_GO_TEST_FLAGS := -race -failfast GO_TEST_FLAGS := +GOTOOLCHAIN ?= $(GO_TOOLCHAIN) +export GOTOOLCHAIN SHELL := bash TOPDIR := $(shell git rev-parse --show-toplevel) TMPDIR := $(TOPDIR)/tmp HUGO_BIN := $(TMPDIR)/hugo/hugo +GOLANGCI_LINT_OS ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') +GOLANGCI_LINT_ARCH ?= $(shell uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/') +GOLANGCI_LINT_PACKAGE := golangci-lint-$(patsubst v%,%,$(GOLANGCI_LINT_VERSION))-$(GOLANGCI_LINT_OS)-$(GOLANGCI_LINT_ARCH) +GOLANGCI_LINT_DIR := $(TMPDIR)/golangci-lint/$(GOLANGCI_LINT_VERSION) +GOLANGCI_LINT_BIN := $(GOLANGCI_LINT_DIR)/golangci-lint +GOLANGCI_LINT ?= $(GOLANGCI_LINT_BIN) +GOLANGCI_LINT_EXTRA_ARGS ?= --concurrency=1 PY_FILES := $(shell find . -type f -regex ".*\.py" -not -regex ".*\.venv/.*" -print) SH_FILES := $(shell find hack/ -type f -regex ".*\.sh" -not -regex ".*\.venv/.*" -print) YAML_FILES := $(shell find . -not -regex '^./vendor/.*' -type f -regex ".*y[a]ml" -print) @@ -81,13 +91,21 @@ html-coverage: ## generate html coverage lint: lint-go lint-yaml lint-md lint-python lint-shell ## run all linters .PHONY: lint-go -lint-go: ## runs go linter on all go files +lint-go: golangci-lint ## runs go linter on all go files @echo "Linting go files..." - @$(GOLANGCI_LINT) run ./pkg/... ./test/... --modules-download-mode=vendor \ + @$(GOLANGCI_LINT) run $(GOLANGCI_LINT_EXTRA_ARGS) ./pkg/... ./test/... --modules-download-mode=vendor \ --max-issues-per-linter=0 \ --max-same-issues=0 \ --timeout $(TIMEOUT_UNIT) +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT_BIN) ## download pinned golangci-lint into tmp + +$(GOLANGCI_LINT_BIN): + @mkdir -p $(GOLANGCI_LINT_DIR) + @echo "Downloading golangci-lint $(GOLANGCI_LINT_VERSION) for $(GOLANGCI_LINT_OS)-$(GOLANGCI_LINT_ARCH)" + @curl -fsSL "https://github.com/golangci/golangci-lint/releases/download/$(GOLANGCI_LINT_VERSION)/$(GOLANGCI_LINT_PACKAGE).tar.gz" | tar -xz -C "$(GOLANGCI_LINT_DIR)" --strip-components=1 "$(GOLANGCI_LINT_PACKAGE)/golangci-lint" + .PHONY: lint-yaml lint-yaml: ${YAML_FILES} ## runs yamllint on all yaml files @echo "Linting yaml files..." @@ -144,9 +162,9 @@ fix-python-errors: ## fix all python errors generated by ruff @[[ -n `git status --porcelain $(PY_FILES)` ]] && { echo "Python files has been cleaned ๐Ÿงน. Cleaned Files: ";git status --porcelain $(PY_FILES) ;} || echo "Python files are clean โœจ" .PHONY: fix-golangci-lint -fix-golangci-lint: ## run golangci-lint and fix on all go files +fix-golangci-lint: golangci-lint ## run golangci-lint and fix on all go files @echo "Fixing some golangi-lint files..." - @$(GOLANGCI_LINT) run ./... --modules-download-mode=vendor \ + @$(GOLANGCI_LINT) run $(GOLANGCI_LINT_EXTRA_ARGS) ./... --modules-download-mode=vendor \ --max-issues-per-linter=0 \ --max-same-issues=0 \ --timeout $(TIMEOUT_UNIT) \ @@ -210,5 +228,3 @@ dev-docs: download-hugo ## preview live your docs with hugo .PHONY: clean clean: ## clean build artifacts rm -fR bin - - diff --git a/go.mod b/go.mod index 0cf1a3e1d1..9ef80dfaee 100644 --- a/go.mod +++ b/go.mod @@ -157,7 +157,7 @@ require ( ) replace ( - github.com/go-jose/go-jose/v4 => github.com/go-jose/go-jose/v4 v4.0.5 + github.com/go-jose/go-jose/v4 => github.com/go-jose/go-jose/v4 v4.1.4 github.com/google/gnostic-models => github.com/google/gnostic-models v0.6.9 k8s.io/api => k8s.io/api v0.32.8 k8s.io/apimachinery => k8s.io/apimachinery v0.32.8 diff --git a/go.sum b/go.sum index 097b8d9105..83c25e1bdd 100644 --- a/go.sum +++ b/go.sum @@ -144,8 +144,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= diff --git a/pkg/adapter/incoming.go b/pkg/adapter/incoming.go index 14f3840ae7..2e7d2ba807 100644 --- a/pkg/adapter/incoming.go +++ b/pkg/adapter/incoming.go @@ -117,7 +117,7 @@ func (l *listener) detectIncoming(ctx context.Context, req *http.Request, payloa return false, nil, nil } - l.logger.Infof("incoming request has been requested: %v", req.URL) + l.logger.Infof("incoming request has been requested: %v", req.URL.Path) payload, err := parseIncomingPayload(req, payloadBody) if payload.legacyMode { // Log this, even if the request is invalid @@ -183,6 +183,7 @@ func (l *listener) detectIncoming(ctx context.Context, req *http.Request, payloa return false, nil, err } l.event.Provider.URL = enterpriseURL + l.event.GHEURL = enterpriseURL l.event.Provider.Token = token l.event.InstallationID = installationID // Github app is not installed for provided repository url diff --git a/pkg/adapter/incoming_test.go b/pkg/adapter/incoming_test.go index 0a4ce8a2b6..1cc86d6997 100644 --- a/pkg/adapter/incoming_test.go +++ b/pkg/adapter/incoming_test.go @@ -1,6 +1,7 @@ package adapter import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -833,7 +834,7 @@ func Test_listener_detectIncoming(t *testing.T) { } // make a new request - req := httptest.NewRequest(tt.args.method, + req := httptest.NewRequestWithContext(context.Background(), tt.args.method, fmt.Sprintf("http://localhost%s?repository=%s&secret=%s&pipelinerun=%s&branch=%s&namespace=%s", tt.args.queryURL, tt.args.queryRepository, tt.args.querySecret, tt.args.queryPipelineRun, tt.args.queryBranch, tt.args.queryNamespace), strings.NewReader(tt.args.incomingBody)) @@ -1107,7 +1108,7 @@ func Test_detectIncoming_legacy_warning(t *testing.T) { }{ { name: "legacy mode - params in URL", - req: httptest.NewRequest(http.MethodPost, + req: httptest.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost/incoming?repository=test-good&secret=verysecrete&pipelinerun=pipelinerun1&branch=main", strings.NewReader("")), body: nil, @@ -1123,7 +1124,7 @@ func Test_detectIncoming_legacy_warning(t *testing.T) { "secret": "verysecrete", "params": {"foo": "bar"} }` - r := httptest.NewRequest(http.MethodPost, + r := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost/incoming", strings.NewReader(payload)) r.Header.Set("Content-Type", "application/json") @@ -1205,7 +1206,7 @@ func Test_detectIncoming_body_params_are_parsed(t *testing.T) { "secret": "verysecrete", "params": {"foo": "bar", "bar": "baz"} }` - req := httptest.NewRequest(http.MethodPost, + req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost/incoming", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") diff --git a/pkg/adapter/sinker.go b/pkg/adapter/sinker.go index f52746f47b..07ed5739d7 100644 --- a/pkg/adapter/sinker.go +++ b/pkg/adapter/sinker.go @@ -60,12 +60,7 @@ func (s *sinker) processEventPayload(ctx context.Context, request *http.Request) } func (s *sinker) processEvent(ctx context.Context, request *http.Request) error { - if s.event.EventType == "incoming" { - if request.Header.Get("X-GitHub-Enterprise-Host") != "" { - s.event.Provider.URL = request.Header.Get("X-GitHub-Enterprise-Host") - s.event.GHEURL = request.Header.Get("X-GitHub-Enterprise-Host") - } - } else { + if s.event.EventType != "incoming" { if err := s.processEventPayload(ctx, request); err != nil { return err } diff --git a/pkg/llm/providers/common.go b/pkg/llm/providers/common.go index 8b9c19bcf4..545dff0edc 100644 --- a/pkg/llm/providers/common.go +++ b/pkg/llm/providers/common.go @@ -106,7 +106,7 @@ func BuildPrompt(request *ltypes.AnalysisRequest) (string, error) { promptBuilder.WriteString("Context Information:\n") for key, value := range request.Context { - promptBuilder.WriteString(fmt.Sprintf("=== %s ===\n", strings.ToUpper(key))) + fmt.Fprintf(&promptBuilder, "=== %s ===\n", strings.ToUpper(key)) switch v := value.(type) { case string: @@ -118,7 +118,7 @@ func BuildPrompt(request *ltypes.AnalysisRequest) (string, error) { } promptBuilder.Write(jsonData) default: - promptBuilder.WriteString(fmt.Sprintf("%v", v)) + fmt.Fprintf(&promptBuilder, "%v", v) } promptBuilder.WriteString("\n\n") diff --git a/pkg/provider/github/app/token.go b/pkg/provider/github/app/token.go index 949b4eecd4..ee0d9ff209 100644 --- a/pkg/provider/github/app/token.go +++ b/pkg/provider/github/app/token.go @@ -9,6 +9,7 @@ import ( "time" "github.com/golang-jwt/jwt/v4" + "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys" "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1" "github.com/openshift-pipelines/pipelines-as-code/pkg/params" "github.com/openshift-pipelines/pipelines-as-code/pkg/provider/github" @@ -67,9 +68,12 @@ func (ip *Install) GetAndUpdateInstallationID(ctx context.Context) (string, stri return "", "", 0, fmt.Errorf("github client APIURL is nil") } apiURL := *ip.ghClient.APIURL - enterpriseHost := ip.request.Header.Get("X-GitHub-Enterprise-Host") - if enterpriseHost != "" { - apiURL = fmt.Sprintf("https://%s/api/v3", strings.TrimSuffix(enterpriseHost, "/")) + enterpriseHost := "" + if repoURL.Host != "" && repoURL.Host != "github.com" { + enterpriseHost = repoURL.Host + if apiURL == keys.PublicGithubAPIURL { + apiURL = fmt.Sprintf("https://%s/api/v3", strings.TrimSuffix(enterpriseHost, "/")) + } } client, _, _ := github.MakeClient(ctx, apiURL, jwtToken) diff --git a/pkg/provider/github/app/token_test.go b/pkg/provider/github/app/token_test.go index 8bea5e84dc..3ab6a421c1 100644 --- a/pkg/provider/github/app/token_test.go +++ b/pkg/provider/github/app/token_test.go @@ -1,6 +1,7 @@ package app import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -144,7 +145,7 @@ func Test_GenerateJWT(t *testing.T) { }, } - ip := NewInstallation(httptest.NewRequest(http.MethodGet, "http://localhost", strings.NewReader("")), run, &v1alpha1.Repository{}, &github.Provider{}, tt.namespace.GetName()) + ip := NewInstallation(httptest.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", strings.NewReader("")), run, &v1alpha1.Repository{}, &github.Provider{}, tt.namespace.GetName()) token, err := ip.GenerateJWT(ctx) if tt.wantErr { assert.Assert(t, err != nil) @@ -206,10 +207,10 @@ func Test_GetAndUpdateInstallationID(t *testing.T) { ctx = info.StoreCurrentControllerName(ctx, "default") ctx = info.StoreNS(ctx, testNamespace.GetName()) - ip := NewInstallation(httptest.NewRequest(http.MethodGet, "http://localhost", strings.NewReader("")), run, &v1alpha1.Repository{}, &github.Provider{}, testNamespace.GetName()) + ip := NewInstallation(httptest.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", strings.NewReader("")), run, &v1alpha1.Repository{}, &github.Provider{}, testNamespace.GetName()) jwtToken, err := ip.GenerateJWT(ctx) assert.NilError(t, err) - req := httptest.NewRequest(http.MethodGet, "http://localhost", strings.NewReader("")) + req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", strings.NewReader("")) repo := &v1alpha1.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "repo", @@ -264,6 +265,75 @@ func Test_GetAndUpdateInstallationID(t *testing.T) { assert.Equal(t, token, wantToken) } +func TestGetAndUpdateInstallationIDIgnoresEnterpriseHostHeader(t *testing.T) { + tdata := testclient.Data{ + Namespaces: []*corev1.Namespace{testNamespace}, + Secret: []*corev1.Secret{validSecret}, + } + wantToken := "GOODTOKEN" + wantID := int64(120) + orgName := "org" + repoName := "repo" + + fakeghclient, mux, serverURL, teardown := ghtesthelper.SetupGH() + defer teardown() + + ctx, _ := rtesting.SetupFakeContext(t) + stdata, _ := testclient.SeedTestData(t, ctx, tdata) + logger, _ := logger.GetLogger() + run := ¶ms.Run{ + Clients: clients.Clients{ + Log: logger, + PipelineAsCode: stdata.PipelineAsCode, + Kube: stdata.Kube, + }, + Info: info.Info{ + Pac: &info.PacOpts{ + Settings: settings.Settings{}, + }, + Controller: &info.ControllerInfo{Secret: validSecret.GetName()}, + }, + } + ctx = info.StoreCurrentControllerName(ctx, "default") + ctx = info.StoreNS(ctx, testNamespace.GetName()) + + jwtInstallation := &Install{run: run, namespace: testNamespace.GetName()} + jwtToken, err := jwtInstallation.GenerateJWT(ctx) + assert.NilError(t, err) + + mux.HandleFunc(fmt.Sprintf("/repos/%s/%s/installation", orgName, repoName), func(w http.ResponseWriter, _ *http.Request) { + _, _ = fmt.Fprintf(w, `{"id": %d}`, wantID) + }) + mux.HandleFunc(fmt.Sprintf("/app/installations/%d/access_tokens", wantID), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r) + w.Header().Set("Authorization", "Bearer "+jwtToken) + w.Header().Set("Accept", "application/vnd.github+json") + _, _ = fmt.Fprintf(w, `{"token": "%s"}`, wantToken) + }) + + repo := &v1alpha1.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "repo", + }, + Spec: v1alpha1.RepositorySpec{ + URL: fmt.Sprintf("https://github.com/%s/%s", orgName, repoName), + }, + } + + gprovider := &github.Provider{APIURL: &serverURL, Run: run} + gprovider.SetGithubClient(fakeghclient) + t.Setenv("PAC_GIT_PROVIDER_TOKEN_APIURL", serverURL+"/api/v3") + + req := httptest.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", strings.NewReader("")) + req.Header.Set("X-GitHub-Enterprise-Host", "127.0.0.1:1") + ip := NewInstallation(req, run, repo, gprovider, testNamespace.GetName()) + enterpriseURL, token, installationID, err := ip.GetAndUpdateInstallationID(ctx) + assert.NilError(t, err) + assert.Equal(t, enterpriseURL, "") + assert.Equal(t, installationID, wantID) + assert.Equal(t, token, wantToken) +} + func testMethod(t *testing.T, r *http.Request) { want := "POST" t.Helper() @@ -291,6 +361,7 @@ func TestGetAndUpdateInstallationID_Fallbacks(t *testing.T) { wantErr bool wantInstallationID int64 wantToken string + wantEnterpriseHost string skip bool expectedErrorString string }{ @@ -314,6 +385,7 @@ func TestGetAndUpdateInstallationID_Fallbacks(t *testing.T) { wantErr: false, wantInstallationID: orgID, wantToken: wantToken, + wantEnterpriseHost: "matched", }, { name: "repo and org installation fail, user installation succeeds", @@ -338,6 +410,7 @@ func TestGetAndUpdateInstallationID_Fallbacks(t *testing.T) { wantErr: false, wantInstallationID: userID, wantToken: wantToken, + wantEnterpriseHost: "matched", }, { name: "all installations fail", @@ -393,7 +466,7 @@ func TestGetAndUpdateInstallationID_Fallbacks(t *testing.T) { ctx = info.StoreCurrentControllerName(ctx, "default") ctx = info.StoreNS(ctx, testNamespace.GetName()) - ip := NewInstallation(httptest.NewRequest(http.MethodGet, "http://localhost", strings.NewReader("")), run, &v1alpha1.Repository{}, &github.Provider{}, testNamespace.GetName()) + ip := NewInstallation(httptest.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", strings.NewReader("")), run, &v1alpha1.Repository{}, &github.Provider{}, testNamespace.GetName()) jwtToken, err := ip.GenerateJWT(ctx) assert.NilError(t, err) @@ -412,8 +485,8 @@ func TestGetAndUpdateInstallationID_Fallbacks(t *testing.T) { gprovider.SetGithubClient(fakeghclient) t.Setenv("PAC_GIT_PROVIDER_TOKEN_APIURL", serverURL) - ip = NewInstallation(httptest.NewRequest(http.MethodGet, "http://localhost", strings.NewReader("")), run, repo, gprovider, testNamespace.GetName()) - _, token, installationID, err := ip.GetAndUpdateInstallationID(ctx) + ip = NewInstallation(httptest.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", strings.NewReader("")), run, repo, gprovider, testNamespace.GetName()) + enterpriseHost, token, installationID, err := ip.GetAndUpdateInstallationID(ctx) if tt.wantErr { assert.Assert(t, err != nil) @@ -426,6 +499,7 @@ func TestGetAndUpdateInstallationID_Fallbacks(t *testing.T) { assert.NilError(t, err) assert.Equal(t, installationID, tt.wantInstallationID) assert.Equal(t, token, tt.wantToken) + assert.Equal(t, enterpriseHost, tt.wantEnterpriseHost) }) } } diff --git a/pkg/provider/github/github.go b/pkg/provider/github/github.go index dc74ebec04..69be721b35 100644 --- a/pkg/provider/github/github.go +++ b/pkg/provider/github/github.go @@ -319,6 +319,34 @@ func (v *Provider) SetClient(ctx context.Context, run *params.Run, event *info.E } } + // Handle GitHub App token scoping for both global and repo-level configuration + if event.InstallationID > 0 { + token := "" + if repo != nil && v.pacInfo != nil && v.Logger != nil && v.eventEmitter != nil { + v.Logger.Debugf("setupAuthenticatedClient: scoping github app token") + scopedToken, err := ScopeTokenToListOfRepos(ctx, v, v.pacInfo, repo, run, event, v.eventEmitter, v.Logger) + if err != nil { + return fmt.Errorf("failed to scope token: %w", err) + } + token = scopedToken + } + // If Global and Repo level configurations are not provided then lets not override the provider token. + if token != "" { + event.Provider.Token = token + } else if len(v.RepositoryIDs) > 0 { + // We need to keep the token unscoped until ScopeTokenToListOfRepos so that CreateToken can + // look up the extra repos from the configmap. + // Token is scoped to only the calling repo if no additional scoping repos are configured + // so that no unwanted remote tasks are executed. + ns := info.GetNS(ctx) + scopedToken, err := v.GetAppToken(ctx, run.Clients.Kube, event.Provider.URL, event.InstallationID, ns) + if err != nil { + return fmt.Errorf("failed to scope token to triggering repository: %w", err) + } + event.Provider.Token = scopedToken + } + } + return nil } diff --git a/pkg/provider/github/parse_payload.go b/pkg/provider/github/parse_payload.go index e532920d87..0190fc7cff 100644 --- a/pkg/provider/github/parse_payload.go +++ b/pkg/provider/github/parse_payload.go @@ -1,11 +1,13 @@ package github import ( + "bytes" "context" "encoding/json" "errors" "fmt" "net/http" + "net/url" "os" "path" "strconv" @@ -24,6 +26,12 @@ import ( "k8s.io/client-go/kubernetes" ) +const ( + githubAppTokenMintBlockedLog = "[SECURITY] Blocked GitHub App token minting before webhook signature validation completed" + githubAppTokenExfiltrationBlockedLog = "[SECURITY][CRITICAL] Averted GitHub App credential exfiltration attempt before token minting" + controllerWebhookSecretKey = "webhook.secret" +) + // GetAppIDAndPrivateKey retrieves the GitHub application ID and private key from a secret in the specified namespace. // It takes a context, namespace, and Kubernetes client as input parameters. // It returns the application ID (int64), private key ([]byte), and an error if any. @@ -90,6 +98,35 @@ func (v *Provider) GetAppToken(ctx context.Context, kube kubernetes.Interface, g return token, err } +func validateAppWebhookSignature(ctx context.Context, run *params.Run, event *info.Event) error { + signature := event.Request.Header.Get(github.SHA256SignatureHeader) + if signature == "" { + signature = event.Request.Header.Get(github.SHA1SignatureHeader) + } + if signature == "" || signature == "sha1=" { + return fmt.Errorf("no signature has been detected, for security reason we are not allowing webhooks that has no secret") + } + + var err error + event.Provider.WebhookSecret, err = getCurrentNSWebhookSecret(ctx, run) + if err != nil { + return err + } + if event.Provider.WebhookSecret == "" { + return fmt.Errorf("no webhook secret has been set in controller secret") + } + return github.ValidateSignature(signature, event.Request.Payload, []byte(event.Provider.WebhookSecret)) +} + +func getCurrentNSWebhookSecret(ctx context.Context, run *params.Run) (string, error) { + ns := info.GetNS(ctx) + secret, err := run.Clients.Kube.CoreV1().Secrets(ns).Get(ctx, run.Info.Controller.Secret, metav1.GetOptions{}) + if err != nil { + return "", err + } + return strings.TrimSpace(string(secret.Data[controllerWebhookSecretKey])), nil +} + func (v *Provider) parseEventType(request *http.Request, event *info.Event) error { event.EventType = request.Header.Get("X-GitHub-Event") if event.EventType == "" { @@ -111,18 +148,91 @@ type Payload struct { Installation struct { ID *int64 `json:"id"` } `json:"installation"` + Repository struct { + HTMLURL string `json:"html_url"` + ID *int64 `json:"id"` + } `json:"repository"` } -func getInstallationIDFromPayload(payload string) (int64, error) { +func getInstallationAndRepoIDFromPayload(payload string) (int64, int64, error) { var data Payload err := json.Unmarshal([]byte(payload), &data) if err != nil { - return -1, err + return -1, -1, err } + + var installationID int64 = -1 + var repoID int64 = -1 if data.Installation.ID != nil { - return *data.Installation.ID, nil + installationID = *data.Installation.ID + } + + if data.Repository.ID != nil { + repoID = *data.Repository.ID + } + + return installationID, repoID, nil +} + +func validateEnterpriseHostMatchesPayload(gheURL, payload string) error { + if gheURL == "" { + return nil + } + if !strings.HasPrefix(gheURL, "https://") && !strings.HasPrefix(gheURL, "http://") { + gheURL = "https://" + gheURL + } + enterpriseURL, err := url.Parse(gheURL) + if err != nil || enterpriseURL.Host == "" { + return fmt.Errorf("invalid X-GitHub-Enterprise-Host header") + } + + var data Payload + if err := json.Unmarshal([]byte(payload), &data); err != nil { + return err } - return -1, nil + if data.Repository.HTMLURL == "" { + return fmt.Errorf("repository HTML URL is missing in payload, cannot validate enterprise host") + } + repoURL, err := url.Parse(data.Repository.HTMLURL) + if err != nil || repoURL.Host == "" { + return fmt.Errorf("invalid repository URL in GitHub payload") + } + if !strings.EqualFold(enterpriseURL.Host, repoURL.Host) { + return fmt.Errorf("github enterprise host %q does not match repository host %q", enterpriseURL.Host, repoURL.Host) + } + return nil +} + +func (v *Provider) logBlockedGitHubAppTokenMint(request *http.Request, event *info.Event, installationID int64, reason string, err error) { + if v.Logger == nil { + return + } + v.Logger.Errorw(githubAppTokenExfiltrationBlockedLog, + "severity", "critical", + "security-impact", "github-app-jwt-exfiltration-blocked", + "reason", reason, + "error", err, + "event-type", event.EventType, + "installation-id", installationID, + "github-enterprise-host-present", request.Header.Get("X-GitHub-Enterprise-Host") != "", + "remote-addr", request.RemoteAddr, + ) +} + +func (v *Provider) logGitHubAppTokenMintValidationFailure(request *http.Request, event *info.Event, installationID int64, reason string, err error) { + if v.Logger == nil { + return + } + v.Logger.Warnw(githubAppTokenMintBlockedLog, + "severity", "warning", + "security-impact", "github-app-token-mint-blocked", + "reason", reason, + "error", err, + "event-type", event.EventType, + "installation-id", installationID, + "github-enterprise-host-present", request.Header.Get("X-GitHub-Enterprise-Host") != "", + "remote-addr", request.RemoteAddr, + ) } // ParsePayload will parse the payload and return the event @@ -139,32 +249,46 @@ func getInstallationIDFromPayload(payload string) (int64, error) { // app on a github org which has a mixed of private and public repos and some of // the public users should not have access to the private repos. // -// Another thing: The payload is protected by the webhook signature so it cannot be tempered but even tho if it's -// tempered with and somehow a malicious user found the token and set their own github endpoint to hijack and -// exfiltrate the token, it would fail since the jwt token generation will fail, so we are safe here. -// a bit too far fetched but i don't see any way we can exploit this. +// Validate the webhook signature before generating an app token because the token +// request signs a GitHub App JWT locally and sends it to the selected API host. func (v *Provider) ParsePayload(ctx context.Context, run *params.Run, request *http.Request, payload string) (*info.Event, error) { // ParsePayload is really happening before SetClient so let's set this at first here. // Only apply for GitHub provider since we do fancy token creation at payload parsing v.Run = run event := info.NewEvent() + event.Request = &info.Request{ + Header: request.Header, + Payload: bytes.TrimSpace([]byte(payload)), + } systemNS := info.GetNS(ctx) if err := v.parseEventType(request, event); err != nil { return nil, err } - installationIDFrompayload, err := getInstallationIDFromPayload(payload) + installationIDFrompayload, repoIDFromPayload, err := getInstallationAndRepoIDFromPayload(payload) if err != nil { return nil, err } if installationIDFrompayload != -1 { var err error + if err := validateAppWebhookSignature(ctx, run, event); err != nil { + v.logGitHubAppTokenMintValidationFailure(request, event, installationIDFrompayload, "webhook-signature-validation-failed", err) + return nil, err + } + if err := validateEnterpriseHostMatchesPayload(event.Provider.URL, payload); err != nil { + v.logBlockedGitHubAppTokenMint(request, event, installationIDFrompayload, "enterprise-host-validation-failed", err) + return nil, err + } // TODO: move this out of here when we move al config inside context if event.Provider.Token, err = v.GetAppToken(ctx, run.Clients.Kube, event.Provider.URL, installationIDFrompayload, systemNS); err != nil { return nil, err } } + if repoIDFromPayload > 0 { + v.RepositoryIDs = []int64{repoIDFromPayload} + } + eventInt, err := github.ParseWebHook(event.EventType, []byte(payload)) if err != nil { return nil, err @@ -453,6 +577,7 @@ func (v *Provider) handleReRequestEvent(ctx context.Context, event *github.Check // fine because you can't do a rereq without being a github owner? runevent.Sender = event.GetSender().GetLogin() v.userType = event.GetSender().GetType() + v.RepositoryIDs = []int64{event.GetRepo().GetID()} return runevent, nil } runevent.PullRequestNumber = event.GetCheckRun().GetCheckSuite().PullRequests[0].GetNumber() @@ -484,6 +609,7 @@ func (v *Provider) handleCheckSuites(ctx context.Context, event *github.CheckSui // fine because you can't do a rereq without being a github owner? runevent.Sender = event.GetSender().GetLogin() v.userType = event.GetSender().GetType() + v.RepositoryIDs = []int64{event.GetRepo().GetID()} return runevent, nil // return nil, fmt.Errorf("check suite event is not supported for push events") } @@ -596,6 +722,7 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C runevent.BaseURL = runevent.HeadURL runevent.TriggerTarget = triggertype.Push opscomments.SetEventTypeAndTargetPR(runevent, event.GetComment().GetBody()) + v.RepositoryIDs = []int64{event.GetRepo().GetID()} defaultBranch := event.GetRepo().GetDefaultBranch() // Set Event.Repository.DefaultBranch as default branch to runevent.HeadBranch, runevent.BaseBranch diff --git a/pkg/provider/github/parse_payload_test.go b/pkg/provider/github/parse_payload_test.go index 55b77b38e1..b14fc9ee18 100644 --- a/pkg/provider/github/parse_payload_test.go +++ b/pkg/provider/github/parse_payload_test.go @@ -2,6 +2,9 @@ package github import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -11,7 +14,6 @@ import ( "github.com/google/go-github/v74/github" "gotest.tools/v3/assert" - "gotest.tools/v3/env" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" rtesting "knative.dev/pkg/reconciler/testing" @@ -72,6 +74,12 @@ var samplePRevent = github.PullRequestEvent{ Repo: sampleRepo, } +func githubSHA256Signature(secret string, payload []byte) string { + hm := hmac.New(sha256.New, []byte(secret)) + hm.Write(payload) + return "sha256=" + hex.EncodeToString(hm.Sum(nil)) +} + var samplePR = github.PullRequest{ Number: github.Ptr(54321), Head: &github.PullRequestBranch{ @@ -1064,6 +1072,7 @@ func TestAppTokenGeneration(t *testing.T) { secretName := "pipelines-as-code-secret" ctx, _ := rtesting.SetupFakeContext(t) + webhookSecret := "webhook-secret" vaildSecret, _ := testclient.SeedTestData(t, ctx, testclient.Data{ Secret: []*corev1.Secret{ { @@ -1074,6 +1083,7 @@ func TestAppTokenGeneration(t *testing.T) { Data: map[string][]byte{ "github-application-id": []byte("12345"), "github-private-key": []byte(fakePrivateKey), + "webhook.secret": []byte(webhookSecret), }, }, }, @@ -1090,6 +1100,7 @@ func TestAppTokenGeneration(t *testing.T) { Data: map[string][]byte{ "github-application-id": []byte("abcd"), "github-private-key": []byte(fakePrivateKey), + "webhook.secret": []byte(webhookSecret), }, }, }, @@ -1106,6 +1117,7 @@ func TestAppTokenGeneration(t *testing.T) { Data: map[string][]byte{ "github-application-id": []byte("12345"), "github-private-key": []byte("invalid-key"), + "webhook.secret": []byte(webhookSecret), }, }, }, @@ -1118,17 +1130,49 @@ func TestAppTokenGeneration(t *testing.T) { wantErrSubst string nilClient bool seedData testclient.Clients - envs map[string]string resultBaseURL string checkInstallIDs []int64 extraRepoInstallIDs map[string]string + omitSignature bool + enterpriseHost string + payload string + wantLogMessage string }{ { - name: "secret not found", - ctx: ctxNoSecret, - ctxNS: "foo", - seedData: noSecret, - wantErrSubst: `secrets "pipelines-as-code-secret" not found`, + name: "secret not found", + ctx: ctxNoSecret, + ctxNS: "foo", + seedData: noSecret, + wantErrSubst: `secrets "pipelines-as-code-secret" not found`, + wantLogMessage: githubAppTokenMintBlockedLog, + }, + { + ctx: ctx, + name: "missing webhook signature", + ctxNS: testNamespace, + seedData: vaildSecret, + omitSignature: true, + wantErrSubst: "no signature has been detected", + wantLogMessage: githubAppTokenMintBlockedLog, + }, + { + ctx: ctx, + name: "enterprise host does not match signed repository payload", + ctxNS: testNamespace, + seedData: vaildSecret, + enterpriseHost: "127.0.0.1:1", + wantErrSubst: `github enterprise host "127.0.0.1:1" does not match repository host "github.com"`, + wantLogMessage: githubAppTokenExfiltrationBlockedLog, + }, + { + ctx: ctx, + name: "enterprise host with missing repository HTML URL", + ctxNS: testNamespace, + seedData: vaildSecret, + enterpriseHost: "127.0.0.1:1", + payload: fmt.Sprintf(`{"installation":{"id":%d},"repository":{}}`, testInstallationID), + wantErrSubst: "repository HTML URL is missing in payload, cannot validate enterprise host", + wantLogMessage: githubAppTokenExfiltrationBlockedLog, }, { ctx: ctx, @@ -1176,8 +1220,6 @@ func TestAppTokenGeneration(t *testing.T) { mux.HandleFunc(fmt.Sprintf("/app/installations/%d/access_tokens", testInstallationID), func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, "{}") }) - envRemove := env.PatchAll(t, tt.envs) - defer envRemove() // adding installation id to event to enforce client creation samplePRevent.Installation = &github.Installation{ @@ -1196,24 +1238,21 @@ func TestAppTokenGeneration(t *testing.T) { } jeez, _ := json.Marshal(samplePRevent) - logger, _ := logger.GetLogger() + if tt.payload != "" { + jeez = []byte(tt.payload) + } + testLogger, observedLogs := logger.GetLogger() gprovider := Provider{ - Logger: logger, + Logger: testLogger, ghClient: fakeghclient, pacInfo: &info.PacOpts{ Settings: settings.Settings{}, }, } - request := &http.Request{Header: map[string][]string{}} - request.Header.Set("X-GitHub-Event", "pull_request") - // a bit of a pain but works - request.Header.Set("X-GitHub-Enterprise-Host", serverURL) - tt.envs = make(map[string]string) - tt.envs["PAC_GIT_PROVIDER_TOKEN_APIURL"] = serverURL + "/api/v3" run := ¶ms.Run{ Clients: clients.Clients{ - Log: logger, + Log: testLogger, Kube: tt.seedData.Kube, }, @@ -1242,6 +1281,16 @@ func TestAppTokenGeneration(t *testing.T) { gprovider.pacInfo.SecretGhAppTokenScopedExtraRepos = extras } + request := &http.Request{Header: map[string][]string{}} + request.Header.Set("X-GitHub-Event", "pull_request") + if !tt.omitSignature { + request.Header.Set(github.SHA256SignatureHeader, githubSHA256Signature(webhookSecret, jeez)) + } + if tt.enterpriseHost != "" { + request.Header.Set("X-GitHub-Enterprise-Host", tt.enterpriseHost) + } + t.Setenv("PAC_GIT_PROVIDER_TOKEN_APIURL", serverURL+"/api/v3") + tt.ctx = info.StoreCurrentControllerName(tt.ctx, "default") tt.ctx = info.StoreNS(tt.ctx, tt.ctxNS) @@ -1249,6 +1298,16 @@ func TestAppTokenGeneration(t *testing.T) { if tt.wantErrSubst != "" { assert.Assert(t, err != nil) assert.ErrorContains(t, err, tt.wantErrSubst) + if tt.wantLogMessage != "" { + found := false + for _, entry := range observedLogs.All() { + if entry.Message == tt.wantLogMessage { + found = true + break + } + } + assert.Assert(t, found, "expected log message %q for blocked GitHub App token mint", tt.wantLogMessage) + } return } assert.NilError(t, err) diff --git a/pkg/resolve/remote.go b/pkg/resolve/remote.go index f162966212..67ada6bef1 100644 --- a/pkg/resolve/remote.go +++ b/pkg/resolve/remote.go @@ -101,8 +101,8 @@ func resolveRemoteResources(ctx context.Context, rt *matcher.RemoteTasks, types // making sure that the pipeline with same annotation name is not fetched if alreadyFetchedResource(fetchedResourcesForEvent.Pipelines, remotePipeline) { rt.Logger.Debugf("skipping already fetched pipeline %s in annotations on pipelinerun %s", remotePipeline, pipelinerun.GetName()) - // already fetched, then just get the pipeline to add to run specific Resources - pipeline = fetchedResourcesForEvent.Pipelines[remotePipeline] + // already fetched, deep-copy so inlining for this run doesn't mutate the cached original + pipeline = fetchedResourcesForEvent.Pipelines[remotePipeline].DeepCopy() } else { // seems like a new pipeline, fetch it based on name in annotation pipeline, err = rt.GetPipelineFromAnnotationName(ctx, remotePipeline) @@ -157,7 +157,7 @@ func resolveRemoteResources(ctx context.Context, rt *matcher.RemoteTasks, types // if task is already fetched in the event, then just copy the task if alreadyFetchedResource(fetchedResourcesForEvent.Tasks, remoteTask) { rt.Logger.Debugf("skipping already fetched task %s in annotations on pipelinerun %s", remoteTask, pipelinerun.GetName()) - task = fetchedResourcesForEvent.Tasks[remoteTask] + task = fetchedResourcesForEvent.Tasks[remoteTask].DeepCopy() } else { // get the task from annotation name task, err = rt.GetTaskFromAnnotationName(ctx, remoteTask) @@ -188,7 +188,7 @@ func resolveRemoteResources(ctx context.Context, rt *matcher.RemoteTasks, types // if PipelineRef is used then, first resolve pipeline and replace all taskRef{Finally/Task} of Pipeline, then put inlinePipeline in PipelineRun if pipelinerun.Spec.PipelineRef != nil && pipelinerun.Spec.PipelineRef.Resolver == "" { - pipelineResolved := fetchedResourcesForPipelineRun.Pipeline + pipelineResolved := fetchedResourcesForPipelineRun.Pipeline.DeepCopy() turns, err := inlineTasks(pipelineResolved.Spec.Tasks, ropt, fetchedResourcesForPipelineRun) if err != nil { return nil, err diff --git a/pkg/resolve/remote_test.go b/pkg/resolve/remote_test.go index 60d819f4d1..bfb378589a 100644 --- a/pkg/resolve/remote_test.go +++ b/pkg/resolve/remote_test.go @@ -464,3 +464,81 @@ func TestRemote(t *testing.T) { }) } } + +// Verifies that cached remote pipelines are deep-copied before modification. +// Without deep copy, when the first PipelineRun applies its task annotation, +// it would mutate the cached pipeline and leak that task into the second run. +func TestSharedRemotePipelineCacheNotMutated(t *testing.T) { + taskName := "shared-task" + + pipelineTask := tektonv1.TaskSpec{ + Steps: []tektonv1.Step{ + {Name: "from-pipeline", Image: "alpine", Command: []string{"true"}}, + }, + } + pipelinerunTask := tektonv1.TaskSpec{ + Steps: []tektonv1.Step{ + {Name: "from-pipelinerun", Image: "busybox", Command: []string{"false"}}, + }, + } + + // remote pipeline with a single TaskRef + pipeline := ttkn.MakePipeline("shared-pipeline", []tektonv1.PipelineTask{ + {Name: taskName, TaskRef: &tektonv1.TaskRef{Name: taskName}}, + }, map[string]string{ + apipac.Task: "http://remote/shared-task", + }) + pipelineB, err := yaml.Marshal(pipeline) + assert.NilError(t, err) + + pipelineTaskB, err := ttkn.MakeTaskB(taskName, pipelineTask) + assert.NilError(t, err) + pipelinerunTaskB, err := ttkn.MakeTaskB(taskName, pipelinerunTask) + assert.NilError(t, err) + + remoteURLS := map[string]map[string]string{ + "http://remote/shared-pipeline": {"body": string(pipelineB), "code": "200"}, + "http://remote/shared-task": {"body": string(pipelineTaskB), "code": "200"}, + "http://remote/pr-task": {"body": string(pipelinerunTaskB), "code": "200"}, + } + + // first run: overrides the task via pipelinerun annotation + // second run: same pipeline, no override โ€” should get the original task + pipelineruns := []*tektonv1.PipelineRun{ + ttkn.MakePR("first-run", map[string]string{ + apipac.Pipeline: "http://remote/shared-pipeline", + apipac.Task: "http://remote/pr-task", + }, tektonv1.PipelineRunSpec{ + PipelineRef: &tektonv1.PipelineRef{Name: "shared-pipeline"}, + }), + ttkn.MakePR("second-run", map[string]string{ + apipac.Pipeline: "http://remote/shared-pipeline", + }, tektonv1.PipelineRunSpec{ + PipelineRef: &tektonv1.PipelineRef{Name: "shared-pipeline"}, + }), + } + + observer, _ := zapobserver.New(zap.InfoLevel) + logger := zap.New(observer).Sugar() + ctx, _ := rtesting.SetupFakeContext(t) + httpTestClient := httptesthelper.MakeHTTPTestClient(remoteURLS) + rt := &matcher.RemoteTasks{ + ProviderInterface: &testprovider.TestProviderImp{}, + Logger: logger, + Run: ¶ms.Run{ + Clients: clients.Clients{HTTP: *httpTestClient}, + }, + } + + ret, err := resolveRemoteResources(ctx, rt, TektonTypes{PipelineRuns: pipelineruns}, &Opts{RemoteTasks: true, GenerateName: true}) + assert.NilError(t, err) + assert.Equal(t, len(ret), 2) + + firstStep := ret[0].Spec.PipelineSpec.Tasks[0].TaskSpec.Steps[0] + assert.Equal(t, firstStep.Name, "from-pipelinerun") + assert.Equal(t, firstStep.Image, "busybox") + + secondStep := ret[1].Spec.PipelineSpec.Tasks[0].TaskSpec.Steps[0] + assert.Equal(t, secondStep.Name, "from-pipeline") + assert.Equal(t, secondStep.Image, "alpine") +} diff --git a/pkg/test/nonoai/main.go b/pkg/test/nonoai/main.go index 041a12739a..5304af077c 100644 --- a/pkg/test/nonoai/main.go +++ b/pkg/test/nonoai/main.go @@ -216,7 +216,7 @@ func healthHandler(w http.ResponseWriter, _ *http.Request) { func openaiHandler(w http.ResponseWriter, r *http.Request) { if *verbose { - log.Printf("๐Ÿ“จ OpenAI request from %s", r.RemoteAddr) + log.Printf("๐Ÿ“จ OpenAI request from %q", r.RemoteAddr) //nolint:gosec // Test server logs request source for debugging. } // Check authorization header @@ -314,7 +314,7 @@ func openaiHandler(w http.ResponseWriter, r *http.Request) { func geminiHandler(w http.ResponseWriter, r *http.Request) { if *verbose { - log.Printf("๐Ÿ“จ Gemini request from %s", r.RemoteAddr) + log.Printf("๐Ÿ“จ Gemini request from %q", r.RemoteAddr) //nolint:gosec // Test server logs request source for debugging. } // Simulate latency diff --git a/test/pkg/github/instrumentation.go b/test/pkg/github/instrumentation.go index 7a80e3d59e..59d9ed4507 100644 --- a/test/pkg/github/instrumentation.go +++ b/test/pkg/github/instrumentation.go @@ -243,7 +243,7 @@ func parseAPICallLog(logLine string) *InstrumentationAPICall { // outputTestResultToFile writes the test result to a JSON file. func (g *PRTest) outputTestResultToFile(outputDir string, apiCalls []string, lastOAuth2Index, totalLines int) { // Create output directory if it doesn't exist - if err := os.MkdirAll(outputDir, 0o755); err != nil { + if err := os.MkdirAll(outputDir, 0o755); err != nil { //nolint:gosec // E2E instrumentation writes to a caller-provided artifact directory. g.Logger.Warnf("Failed to create output directory %s: %v", outputDir, err) return } @@ -287,7 +287,7 @@ func (g *PRTest) outputTestResultToFile(outputDir string, apiCalls []string, las return } - if err := os.WriteFile(filepath, jsonData, 0o600); err != nil { + if err := os.WriteFile(filepath, jsonData, 0o600); err != nil { //nolint:gosec // E2E instrumentation writes a generated artifact filename under outputDir. g.Logger.Warnf("Failed to write test result to file %s: %v", filepath, err) return } diff --git a/vendor/github.com/go-jose/go-jose/v4/CHANGELOG.md b/vendor/github.com/go-jose/go-jose/v4/CHANGELOG.md deleted file mode 100644 index 6f717dbd86..0000000000 --- a/vendor/github.com/go-jose/go-jose/v4/CHANGELOG.md +++ /dev/null @@ -1,96 +0,0 @@ -# v4.0.4 - -## Fixed - - - Reverted "Allow unmarshalling JSONWebKeySets with unsupported key types" as a - breaking change. See #136 / #137. - -# v4.0.3 - -## Changed - - - Allow unmarshalling JSONWebKeySets with unsupported key types (#130) - - Document that OpaqueKeyEncrypter can't be implemented (for now) (#129) - - Dependency updates - -# v4.0.2 - -## Changed - - - Improved documentation of Verify() to note that JSONWebKeySet is a supported - argument type (#104) - - Defined exported error values for missing x5c header and unsupported elliptic - curves error cases (#117) - -# v4.0.1 - -## Fixed - - - An attacker could send a JWE containing compressed data that used large - amounts of memory and CPU when decompressed by `Decrypt` or `DecryptMulti`. - Those functions now return an error if the decompressed data would exceed - 250kB or 10x the compressed size (whichever is larger). Thanks to - Enze Wang@Alioth and Jianjun Chen@Zhongguancun Lab (@zer0yu and @chenjj) - for reporting. - -# v4.0.0 - -This release makes some breaking changes in order to more thoroughly -address the vulnerabilities discussed in [Three New Attacks Against JSON Web -Tokens][1], "Sign/encrypt confusion", "Billion hash attack", and "Polyglot -token". - -## Changed - - - Limit JWT encryption types (exclude password or public key types) (#78) - - Enforce minimum length for HMAC keys (#85) - - jwt: match any audience in a list, rather than requiring all audiences (#81) - - jwt: accept only Compact Serialization (#75) - - jws: Add expected algorithms for signatures (#74) - - Require specifying expected algorithms for ParseEncrypted, - ParseSigned, ParseDetached, jwt.ParseEncrypted, jwt.ParseSigned, - jwt.ParseSignedAndEncrypted (#69, #74) - - Usually there is a small, known set of appropriate algorithms for a program - to use and it's a mistake to allow unexpected algorithms. For instance the - "billion hash attack" relies in part on programs accepting the PBES2 - encryption algorithm and doing the necessary work even if they weren't - specifically configured to allow PBES2. - - Revert "Strip padding off base64 strings" (#82) - - The specs require base64url encoding without padding. - - Minimum supported Go version is now 1.21 - -## Added - - - ParseSignedCompact, ParseSignedJSON, ParseEncryptedCompact, ParseEncryptedJSON. - - These allow parsing a specific serialization, as opposed to ParseSigned and - ParseEncrypted, which try to automatically detect which serialization was - provided. It's common to require a specific serialization for a specific - protocol - for instance JWT requires Compact serialization. - -[1]: https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf - -# v3.0.2 - -## Fixed - - - DecryptMulti: handle decompression error (#19) - -## Changed - - - jwe/CompactSerialize: improve performance (#67) - - Increase the default number of PBKDF2 iterations to 600k (#48) - - Return the proper algorithm for ECDSA keys (#45) - -## Added - - - Add Thumbprint support for opaque signers (#38) - -# v3.0.1 - -## Fixed - - - Security issue: an attacker specifying a large "p2c" value can cause - JSONWebEncryption.Decrypt and JSONWebEncryption.DecryptMulti to consume large - amounts of CPU, causing a DoS. Thanks to Matt Schwager (@mschwager) for the - disclosure and to Tom Tervoort for originally publishing the category of attack. - https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf diff --git a/vendor/github.com/go-jose/go-jose/v4/README.md b/vendor/github.com/go-jose/go-jose/v4/README.md index 02b5749546..abd9a9e3ce 100644 --- a/vendor/github.com/go-jose/go-jose/v4/README.md +++ b/vendor/github.com/go-jose/go-jose/v4/README.md @@ -3,7 +3,6 @@ [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4) [![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt) [![license](https://img.shields.io/badge/license-apache_2.0-blue.svg?style=flat)](https://raw.githubusercontent.com/go-jose/go-jose/master/LICENSE) -[![test](https://img.shields.io/github/checks-status/go-jose/go-jose/v4)](https://github.com/go-jose/go-jose/actions) Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. This includes support for JSON Web Encryption, @@ -29,17 +28,20 @@ libraries in other languages. ### Versions -[Version 4](https://github.com/go-jose/go-jose) -([branch](https://github.com/go-jose/go-jose/tree/main), -[doc](https://pkg.go.dev/github.com/go-jose/go-jose/v4), [releases](https://github.com/go-jose/go-jose/releases)) is the current stable version: +The forthcoming Version 5 will be released with several breaking API changes, +and will require Golang's `encoding/json/v2`, which is currently requires +Go 1.25 built with GOEXPERIMENT=jsonv2. + +Version 4 is the current stable version: import "github.com/go-jose/go-jose/v4" -The old [square/go-jose](https://github.com/square/go-jose) repo contains the prior v1 and v2 versions, which -are still useable but not actively developed anymore. +It supports at least the current and previous Golang release. Currently it +requires Golang 1.24. + +Version 3 is only receiving critical security updates. Migration to Version 4 is recommended. -Version 3, in this repo, is still receiving security fixes but not functionality -updates. +Versions 1 and 2 are obsolete, but can be found in the old repository, [square/go-jose](https://github.com/square/go-jose). ### Supported algorithms @@ -47,36 +49,36 @@ See below for a table of supported algorithms. Algorithm identifiers match the names in the [JSON Web Algorithms](https://dx.doi.org/10.17487/RFC7518) standard where possible. The Godoc reference has a list of constants. - Key encryption | Algorithm identifier(s) - :------------------------- | :------------------------------ - RSA-PKCS#1v1.5 | RSA1_5 - RSA-OAEP | RSA-OAEP, RSA-OAEP-256 - AES key wrap | A128KW, A192KW, A256KW - AES-GCM key wrap | A128GCMKW, A192GCMKW, A256GCMKW - ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW - ECDH-ES (direct) | ECDH-ES1 - Direct encryption | dir1 +| Key encryption | Algorithm identifier(s) | +|:-----------------------|:-----------------------------------------------| +| RSA-PKCS#1v1.5 | RSA1_5 | +| RSA-OAEP | RSA-OAEP, RSA-OAEP-256 | +| AES key wrap | A128KW, A192KW, A256KW | +| AES-GCM key wrap | A128GCMKW, A192GCMKW, A256GCMKW | +| ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW | +| ECDH-ES (direct) | ECDH-ES1 | +| Direct encryption | dir1 | 1. Not supported in multi-recipient mode - Signing / MAC | Algorithm identifier(s) - :------------------------- | :------------------------------ - RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 - RSASSA-PSS | PS256, PS384, PS512 - HMAC | HS256, HS384, HS512 - ECDSA | ES256, ES384, ES512 - Ed25519 | EdDSA2 +| Signing / MAC | Algorithm identifier(s) | +|:------------------|:------------------------| +| RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 | +| RSASSA-PSS | PS256, PS384, PS512 | +| HMAC | HS256, HS384, HS512 | +| ECDSA | ES256, ES384, ES512 | +| Ed25519 | EdDSA2 | 2. Only available in version 2 of the package - Content encryption | Algorithm identifier(s) - :------------------------- | :------------------------------ - AES-CBC+HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 - AES-GCM | A128GCM, A192GCM, A256GCM +| Content encryption | Algorithm identifier(s) | +|:-------------------|:--------------------------------------------| +| AES-CBC+HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 | +| AES-GCM | A128GCM, A192GCM, A256GCM | - Compression | Algorithm identifiers(s) - :------------------------- | ------------------------------- - DEFLATE (RFC 1951) | DEF +| Compression | Algorithm identifiers(s) | +|:-------------------|--------------------------| +| DEFLATE (RFC 1951) | DEF | ### Supported key types @@ -85,12 +87,12 @@ library, and can be passed to corresponding functions such as `NewEncrypter` or `NewSigner`. Each of these keys can also be wrapped in a JWK if desired, which allows attaching a key id. - Algorithm(s) | Corresponding types - :------------------------- | ------------------------------- - RSA | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey) - ECDH, ECDSA | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey) - EdDSA1 | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey) - AES, HMAC | []byte +| Algorithm(s) | Corresponding types | +|:------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| RSA | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey) | +| ECDH, ECDSA | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey) | +| EdDSA1 | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey) | +| AES, HMAC | []byte | 1. Only available in version 2 or later of the package diff --git a/vendor/github.com/go-jose/go-jose/v4/asymmetric.go b/vendor/github.com/go-jose/go-jose/v4/asymmetric.go index f8d5774ef5..7784cd4584 100644 --- a/vendor/github.com/go-jose/go-jose/v4/asymmetric.go +++ b/vendor/github.com/go-jose/go-jose/v4/asymmetric.go @@ -414,6 +414,9 @@ func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { // Decrypt the given payload and return the content encryption key. func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { + if recipient == nil { + return nil, errors.New("go-jose/go-jose: missing recipient") + } epk, err := headers.getEPK() if err != nil { return nil, errors.New("go-jose/go-jose: invalid epk header") @@ -461,13 +464,18 @@ func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientI return nil, ErrUnsupportedAlgorithm } + encryptedKey := recipient.encryptedKey + if len(encryptedKey) == 0 { + return nil, errors.New("go-jose/go-jose: missing JWE Encrypted Key") + } + key := deriveKey(string(algorithm), keySize) block, err := aes.NewCipher(key) if err != nil { return nil, err } - return josecipher.KeyUnwrap(block, recipient.encryptedKey) + return josecipher.KeyUnwrap(block, encryptedKey) } func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { diff --git a/vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go b/vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go index b9effbca8a..a2f86e3db9 100644 --- a/vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go +++ b/vendor/github.com/go-jose/go-jose/v4/cipher/key_wrap.go @@ -66,12 +66,20 @@ func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { } // KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher. +// +// https://datatracker.ietf.org/doc/html/rfc7518#section-4.4 +// https://datatracker.ietf.org/doc/html/rfc7518#section-4.6 +// https://datatracker.ietf.org/doc/html/rfc7518#section-4.8 func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { + n := (len(ciphertext) / 8) - 1 + if n <= 0 { + return nil, errors.New("go-jose/go-jose: JWE Encrypted Key too short") + } + if len(ciphertext)%8 != 0 { return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks") } - n := (len(ciphertext) / 8) - 1 r := make([][]byte, n) for i := range r { diff --git a/vendor/github.com/go-jose/go-jose/v4/crypter.go b/vendor/github.com/go-jose/go-jose/v4/crypter.go index d81b03b447..31290fc871 100644 --- a/vendor/github.com/go-jose/go-jose/v4/crypter.go +++ b/vendor/github.com/go-jose/go-jose/v4/crypter.go @@ -286,6 +286,10 @@ func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKey return newSymmetricRecipient(alg, encryptionKey) case string: return newSymmetricRecipient(alg, []byte(encryptionKey)) + case JSONWebKey: + recipient, err := makeJWERecipient(alg, encryptionKey.Key) + recipient.keyID = encryptionKey.KeyID + return recipient, err case *JSONWebKey: recipient, err := makeJWERecipient(alg, encryptionKey.Key) recipient.keyID = encryptionKey.KeyID @@ -450,13 +454,9 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) return nil, errors.New("go-jose/go-jose: too many recipients in payload; expecting only one") } - critical, err := headers.getCritical() + err := headers.checkNoCritical() if err != nil { - return nil, fmt.Errorf("go-jose/go-jose: invalid crit header") - } - - if len(critical) > 0 { - return nil, fmt.Errorf("go-jose/go-jose: unsupported crit header") + return nil, err } key, err := tryJWKS(decryptionKey, obj.Header) @@ -523,13 +523,9 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) { globalHeaders := obj.mergedHeaders(nil) - critical, err := globalHeaders.getCritical() + err := globalHeaders.checkNoCritical() if err != nil { - return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: invalid crit header") - } - - if len(critical) > 0 { - return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: unsupported crit header") + return -1, Header{}, nil, err } key, err := tryJWKS(decryptionKey, obj.Header) diff --git a/vendor/github.com/go-jose/go-jose/v4/jwe.go b/vendor/github.com/go-jose/go-jose/v4/jwe.go index 9f1322dccc..6102f91000 100644 --- a/vendor/github.com/go-jose/go-jose/v4/jwe.go +++ b/vendor/github.com/go-jose/go-jose/v4/jwe.go @@ -274,7 +274,7 @@ func validateAlgEnc(headers rawHeader, keyAlgorithms []KeyAlgorithm, contentEncr if alg != "" && !containsKeyAlgorithm(keyAlgorithms, alg) { return fmt.Errorf("unexpected key algorithm %q; expected %q", alg, keyAlgorithms) } - if alg != "" && !containsContentEncryption(contentEncryption, enc) { + if enc != "" && !containsContentEncryption(contentEncryption, enc) { return fmt.Errorf("unexpected content encryption algorithm %q; expected %q", enc, contentEncryption) } return nil @@ -288,11 +288,20 @@ func ParseEncryptedCompact( keyAlgorithms []KeyAlgorithm, contentEncryption []ContentEncryption, ) (*JSONWebEncryption, error) { - // Five parts is four separators - if strings.Count(input, ".") != 4 { - return nil, fmt.Errorf("go-jose/go-jose: compact JWE format must have five parts") + var parts [5]string + var ok bool + + for i := range 4 { + parts[i], input, ok = strings.Cut(input, ".") + if !ok { + return nil, errors.New("go-jose/go-jose: compact JWE format must have five parts") + } + } + // Validate that the last part does not contain more dots + if strings.ContainsRune(input, '.') { + return nil, errors.New("go-jose/go-jose: compact JWE format must have five parts") } - parts := strings.SplitN(input, ".", 5) + parts[4] = input rawProtected, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { diff --git a/vendor/github.com/go-jose/go-jose/v4/jwk.go b/vendor/github.com/go-jose/go-jose/v4/jwk.go index 9e57e93ba2..164d6a1619 100644 --- a/vendor/github.com/go-jose/go-jose/v4/jwk.go +++ b/vendor/github.com/go-jose/go-jose/v4/jwk.go @@ -175,6 +175,8 @@ func (k JSONWebKey) MarshalJSON() ([]byte, error) { } // UnmarshalJSON reads a key from its JSON representation. +// +// Returns ErrUnsupportedKeyType for unrecognized or unsupported "kty" header values. func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) { var raw rawJSONWebKey err = json.Unmarshal(data, &raw) @@ -228,7 +230,7 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) { } key, err = raw.symmetricKey() case "OKP": - if raw.Crv == "Ed25519" && raw.X != nil { + if raw.Crv == "Ed25519" { if raw.D != nil { key, err = raw.edPrivateKey() if err == nil { @@ -238,17 +240,27 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) { key, err = raw.edPublicKey() keyPub = key } - } else { - return fmt.Errorf("go-jose/go-jose: unknown curve %s'", raw.Crv) } - default: - return fmt.Errorf("go-jose/go-jose: unknown json web key type '%s'", raw.Kty) + case "": + // kty MUST be present + err = fmt.Errorf("go-jose/go-jose: missing json web key type") } if err != nil { return } + if key == nil { + // RFC 7517: + // 5. JWK Set Format + // ... + // Implementations SHOULD ignore JWKs within a JWK Set that use "kty" + // (key type) values that are not understood by them, that are missing + // required members, or for which values are out of the supported + // ranges. + return ErrUnsupportedKeyType + } + if certPub != nil && keyPub != nil { if !reflect.DeepEqual(certPub, keyPub) { return errors.New("go-jose/go-jose: invalid JWK, public keys in key and x5c fields do not match") @@ -581,10 +593,10 @@ func fromEcPublicKey(pub *ecdsa.PublicKey) (*rawJSONWebKey, error) { func (key rawJSONWebKey) edPrivateKey() (ed25519.PrivateKey, error) { var missing []string - switch { - case key.D == nil: + if key.D == nil { missing = append(missing, "D") - case key.X == nil: + } + if key.X == nil { missing = append(missing, "X") } @@ -611,19 +623,21 @@ func (key rawJSONWebKey) edPublicKey() (ed25519.PublicKey, error) { func (key rawJSONWebKey) rsaPrivateKey() (*rsa.PrivateKey, error) { var missing []string - switch { - case key.N == nil: + if key.N == nil { missing = append(missing, "N") - case key.E == nil: + } + if key.E == nil { missing = append(missing, "E") - case key.D == nil: + } + if key.D == nil { missing = append(missing, "D") - case key.P == nil: + } + if key.P == nil { missing = append(missing, "P") - case key.Q == nil: + } + if key.Q == nil { missing = append(missing, "Q") } - if len(missing) > 0 { return nil, fmt.Errorf("go-jose/go-jose: invalid RSA private key, missing %s value(s)", strings.Join(missing, ", ")) } @@ -698,8 +712,19 @@ func (key rawJSONWebKey) ecPrivateKey() (*ecdsa.PrivateKey, error) { return nil, fmt.Errorf("go-jose/go-jose: unsupported elliptic curve '%s'", key.Crv) } - if key.X == nil || key.Y == nil || key.D == nil { - return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, missing x/y/d values") + var missing []string + if key.X == nil { + missing = append(missing, "X") + } + if key.Y == nil { + missing = append(missing, "Y") + } + if key.D == nil { + missing = append(missing, "D") + } + + if len(missing) > 0 { + return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, missing %s value(s)", strings.Join(missing, ", ")) } // The length of this octet string MUST be the full size of a coordinate for diff --git a/vendor/github.com/go-jose/go-jose/v4/jws.go b/vendor/github.com/go-jose/go-jose/v4/jws.go index d09d8ba507..c40bd3ec10 100644 --- a/vendor/github.com/go-jose/go-jose/v4/jws.go +++ b/vendor/github.com/go-jose/go-jose/v4/jws.go @@ -75,7 +75,14 @@ type Signature struct { original *rawSignatureInfo } -// ParseSigned parses a signed message in JWS Compact or JWS JSON Serialization. +// ParseSigned parses a signed message in JWS Compact or JWS JSON Serialization. Validation fails if +// the JWS is signed with an algorithm that isn't in the provided list of signature algorithms. +// Applications should decide for themselves which signature algorithms are acceptable. If you're +// not sure which signature algorithms your application might receive, consult the documentation of +// the program which provides them or the protocol that you are implementing. You can also try +// getting an example JWS and decoding it with a tool like https://jwt.io to see what its "alg" +// header parameter indicates. The signature on the JWS does not get validated during parsing. Call +// Verify() after parsing to validate the signature and obtain the payload. // // https://datatracker.ietf.org/doc/html/rfc7515#section-7 func ParseSigned( @@ -90,7 +97,14 @@ func ParseSigned( return parseSignedCompact(signature, nil, signatureAlgorithms) } -// ParseSignedCompact parses a message in JWS Compact Serialization. +// ParseSignedCompact parses a message in JWS Compact Serialization. Validation fails if the JWS is +// signed with an algorithm that isn't in the provided list of signature algorithms. Applications +// should decide for themselves which signature algorithms are acceptable.If you're not sure which +// signature algorithms your application might receive, consult the documentation of the program +// which provides them or the protocol that you are implementing. You can also try getting an +// example JWS and decoding it with a tool like https://jwt.io to see what its "alg" header +// parameter indicates. The signature on the JWS does not get validated during parsing. Call +// Verify() after parsing to validate the signature and obtain the payload. // // https://datatracker.ietf.org/doc/html/rfc7515#section-7.1 func ParseSignedCompact( @@ -101,6 +115,15 @@ func ParseSignedCompact( } // ParseDetached parses a signed message in compact serialization format with detached payload. +// Validation fails if the JWS is signed with an algorithm that isn't in the provided list of +// signature algorithms. Applications should decide for themselves which signature algorithms are +// acceptable. If you're not sure which signature algorithms your application might receive, consult +// the documentation of the program which provides them or the protocol that you are implementing. +// You can also try getting an example JWS and decoding it with a tool like https://jwt.io to see +// what its "alg" header parameter indicates. The signature on the JWS does not get validated during +// parsing. Call Verify() after parsing to validate the signature and obtain the payload. +// +// https://datatracker.ietf.org/doc/html/rfc7515#appendix-F func ParseDetached( signature string, payload []byte, @@ -181,6 +204,25 @@ func containsSignatureAlgorithm(haystack []SignatureAlgorithm, needle SignatureA return false } +// ErrUnexpectedSignatureAlgorithm is returned when the signature algorithm in +// the JWS header does not match one of the expected algorithms. +type ErrUnexpectedSignatureAlgorithm struct { + // Got is the signature algorithm found in the JWS header. + Got SignatureAlgorithm + expected []SignatureAlgorithm +} + +func (e *ErrUnexpectedSignatureAlgorithm) Error() string { + return fmt.Sprintf("unexpected signature algorithm %q; expected %q", e.Got, e.expected) +} + +func newErrUnexpectedSignatureAlgorithm(got SignatureAlgorithm, expected []SignatureAlgorithm) error { + return &ErrUnexpectedSignatureAlgorithm{ + Got: got, + expected: expected, + } +} + // sanitized produces a cleaned-up JWS object from the raw JSON. func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgorithm) (*JSONWebSignature, error) { if len(signatureAlgorithms) == 0 { @@ -236,8 +278,7 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo alg := SignatureAlgorithm(signature.Header.Algorithm) if !containsSignatureAlgorithm(signatureAlgorithms, alg) { - return nil, fmt.Errorf("go-jose/go-jose: unexpected signature algorithm %q; expected %q", - alg, signatureAlgorithms) + return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms) } if signature.header != nil { @@ -285,8 +326,7 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo alg := SignatureAlgorithm(obj.Signatures[i].Header.Algorithm) if !containsSignatureAlgorithm(signatureAlgorithms, alg) { - return nil, fmt.Errorf("go-jose/go-jose: unexpected signature algorithm %q; expected %q", - alg, signatureAlgorithms) + return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms) } if obj.Signatures[i].header != nil { @@ -321,35 +361,43 @@ func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgo return obj, nil } +const tokenDelim = "." + // parseSignedCompact parses a message in compact format. func parseSignedCompact( input string, payload []byte, signatureAlgorithms []SignatureAlgorithm, ) (*JSONWebSignature, error) { - // Three parts is two separators - if strings.Count(input, ".") != 2 { + protected, s, ok := strings.Cut(input, tokenDelim) + if !ok { // no period found + return nil, fmt.Errorf("go-jose/go-jose: compact JWS format must have three parts") + } + claims, sig, ok := strings.Cut(s, tokenDelim) + if !ok { // only one period found + return nil, fmt.Errorf("go-jose/go-jose: compact JWS format must have three parts") + } + if strings.ContainsRune(sig, '.') { // too many periods found return nil, fmt.Errorf("go-jose/go-jose: compact JWS format must have three parts") } - parts := strings.SplitN(input, ".", 3) - if parts[1] != "" && payload != nil { + if claims != "" && payload != nil { return nil, fmt.Errorf("go-jose/go-jose: payload is not detached") } - rawProtected, err := base64.RawURLEncoding.DecodeString(parts[0]) + rawProtected, err := base64.RawURLEncoding.DecodeString(protected) if err != nil { return nil, err } if payload == nil { - payload, err = base64.RawURLEncoding.DecodeString(parts[1]) + payload, err = base64.RawURLEncoding.DecodeString(claims) if err != nil { return nil, err } } - signature, err := base64.RawURLEncoding.DecodeString(parts[2]) + signature, err := base64.RawURLEncoding.DecodeString(sig) if err != nil { return nil, err } diff --git a/vendor/github.com/go-jose/go-jose/v4/shared.go b/vendor/github.com/go-jose/go-jose/v4/shared.go index 1ec3396126..35130b3aa8 100644 --- a/vendor/github.com/go-jose/go-jose/v4/shared.go +++ b/vendor/github.com/go-jose/go-jose/v4/shared.go @@ -77,6 +77,9 @@ var ( // ErrUnsupportedEllipticCurve indicates unsupported or unknown elliptic curve has been found. ErrUnsupportedEllipticCurve = errors.New("go-jose/go-jose: unsupported/unknown elliptic curve") + + // ErrUnsupportedCriticalHeader is returned when a header is marked critical but not supported by go-jose. + ErrUnsupportedCriticalHeader = errors.New("go-jose/go-jose: unsupported critical header") ) // Key management algorithms @@ -167,8 +170,8 @@ const ( ) // supportedCritical is the set of supported extensions that are understood and processed. -var supportedCritical = map[string]bool{ - headerB64: true, +var supportedCritical = map[string]struct{}{ + headerB64: {}, } // rawHeader represents the JOSE header for JWE/JWS objects (used for parsing). @@ -346,6 +349,32 @@ func (parsed rawHeader) getCritical() ([]string, error) { return q, nil } +// checkNoCritical verifies there are no critical headers present. +func (parsed rawHeader) checkNoCritical() error { + if _, ok := parsed[headerCritical]; ok { + return ErrUnsupportedCriticalHeader + } + + return nil +} + +// checkSupportedCritical verifies there are no unsupported critical headers. +// Supported headers are passed in as a set: map of names to empty structs +func (parsed rawHeader) checkSupportedCritical(supported map[string]struct{}) error { + crit, err := parsed.getCritical() + if err != nil { + return err + } + + for _, name := range crit { + if _, ok := supported[name]; !ok { + return ErrUnsupportedCriticalHeader + } + } + + return nil +} + // getS2C extracts parsed "p2c" from the raw JSON. func (parsed rawHeader) getP2C() (int, error) { v := parsed[headerP2C] diff --git a/vendor/github.com/go-jose/go-jose/v4/signing.go b/vendor/github.com/go-jose/go-jose/v4/signing.go index 3dec0112b6..5dbd04c278 100644 --- a/vendor/github.com/go-jose/go-jose/v4/signing.go +++ b/vendor/github.com/go-jose/go-jose/v4/signing.go @@ -404,15 +404,23 @@ func (obj JSONWebSignature) DetachedVerify(payload []byte, verificationKey inter } signature := obj.Signatures[0] - headers := signature.mergedHeaders() - critical, err := headers.getCritical() - if err != nil { - return err + + if signature.header != nil { + // Per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11, + // 4.1.11. "crit" (Critical) Header Parameter + // "When used, this Header Parameter MUST be integrity + // protected; therefore, it MUST occur only within the JWS + // Protected Header." + err = signature.header.checkNoCritical() + if err != nil { + return err + } } - for _, name := range critical { - if !supportedCritical[name] { - return ErrCryptoFailure + if signature.protected != nil { + err = signature.protected.checkSupportedCritical(supportedCritical) + if err != nil { + return err } } @@ -421,6 +429,7 @@ func (obj JSONWebSignature) DetachedVerify(payload []byte, verificationKey inter return ErrCryptoFailure } + headers := signature.mergedHeaders() alg := headers.getSignatureAlgorithm() err = verifier.verifyPayload(input, signature.Signature, alg) if err == nil { @@ -469,14 +478,22 @@ func (obj JSONWebSignature) DetachedVerifyMulti(payload []byte, verificationKey outer: for i, signature := range obj.Signatures { - headers := signature.mergedHeaders() - critical, err := headers.getCritical() - if err != nil { - continue + if signature.header != nil { + // Per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11, + // 4.1.11. "crit" (Critical) Header Parameter + // "When used, this Header Parameter MUST be integrity + // protected; therefore, it MUST occur only within the JWS + // Protected Header." + err = signature.header.checkNoCritical() + if err != nil { + continue outer + } } - for _, name := range critical { - if !supportedCritical[name] { + if signature.protected != nil { + // Check for only supported critical headers + err = signature.protected.checkSupportedCritical(supportedCritical) + if err != nil { continue outer } } @@ -486,6 +503,7 @@ outer: continue } + headers := signature.mergedHeaders() alg := headers.getSignatureAlgorithm() err = verifier.verifyPayload(input, signature.Signature, alg) if err == nil { diff --git a/vendor/github.com/go-jose/go-jose/v4/symmetric.go b/vendor/github.com/go-jose/go-jose/v4/symmetric.go index a69103b084..f2ff29e179 100644 --- a/vendor/github.com/go-jose/go-jose/v4/symmetric.go +++ b/vendor/github.com/go-jose/go-jose/v4/symmetric.go @@ -21,6 +21,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/hmac" + "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "crypto/sha512" @@ -30,8 +31,6 @@ import ( "hash" "io" - "golang.org/x/crypto/pbkdf2" - josecipher "github.com/go-jose/go-jose/v4/cipher" ) @@ -330,7 +329,10 @@ func (ctx *symmetricKeyCipher) encryptKey(cek []byte, alg KeyAlgorithm) (recipie // derive key keyLen, h := getPbkdf2Params(alg) - key := pbkdf2.Key(ctx.key, salt, ctx.p2c, keyLen, h) + key, err := pbkdf2.Key(h, string(ctx.key), salt, ctx.p2c, keyLen) + if err != nil { + return recipientInfo{}, nil + } // use AES cipher with derived key block, err := aes.NewCipher(key) @@ -364,11 +366,21 @@ func (ctx *symmetricKeyCipher) encryptKey(cek []byte, alg KeyAlgorithm) (recipie // Decrypt the content encryption key. func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { - switch headers.getAlgorithm() { - case DIRECT: - cek := make([]byte, len(ctx.key)) - copy(cek, ctx.key) - return cek, nil + if recipient == nil { + return nil, fmt.Errorf("go-jose/go-jose: missing recipient") + } + + alg := headers.getAlgorithm() + if alg == DIRECT { + return bytes.Clone(ctx.key), nil + } + + encryptedKey := recipient.encryptedKey + if len(encryptedKey) == 0 { + return nil, fmt.Errorf("go-jose/go-jose: missing JWE Encrypted Key") + } + + switch alg { case A128GCMKW, A192GCMKW, A256GCMKW: aead := newAESGCM(len(ctx.key)) @@ -383,7 +395,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien parts := &aeadParts{ iv: iv.bytes(), - ciphertext: recipient.encryptedKey, + ciphertext: encryptedKey, tag: tag.bytes(), } @@ -399,7 +411,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien return nil, err } - cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey) + cek, err := josecipher.KeyUnwrap(block, encryptedKey) if err != nil { return nil, err } @@ -432,7 +444,10 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien // derive key keyLen, h := getPbkdf2Params(alg) - key := pbkdf2.Key(ctx.key, salt, p2c, keyLen, h) + key, err := pbkdf2.Key(h, string(ctx.key), salt, p2c, keyLen) + if err != nil { + return nil, err + } // use AES cipher with derived key block, err := aes.NewCipher(key) @@ -440,7 +455,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien return nil, err } - cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey) + cek, err := josecipher.KeyUnwrap(block, encryptedKey) if err != nil { return nil, err } diff --git a/vendor/modules.txt b/vendor/modules.txt index a34809ce1b..ceac3f3b4c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -134,8 +134,8 @@ github.com/go-jose/go-jose/v3 github.com/go-jose/go-jose/v3/cipher github.com/go-jose/go-jose/v3/json github.com/go-jose/go-jose/v3/jwt -# github.com/go-jose/go-jose/v4 v4.1.4 => github.com/go-jose/go-jose/v4 v4.0.5 -## explicit; go 1.21 +# github.com/go-jose/go-jose/v4 v4.1.4 => github.com/go-jose/go-jose/v4 v4.1.4 +## explicit; go 1.24.0 github.com/go-jose/go-jose/v4 github.com/go-jose/go-jose/v4/cipher github.com/go-jose/go-jose/v4/json @@ -1325,7 +1325,7 @@ sigs.k8s.io/structured-merge-diff/v4/value ## explicit; go 1.22 sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 -# github.com/go-jose/go-jose/v4 => github.com/go-jose/go-jose/v4 v4.0.5 +# github.com/go-jose/go-jose/v4 => github.com/go-jose/go-jose/v4 v4.1.4 # github.com/google/gnostic-models => github.com/google/gnostic-models v0.6.9 # k8s.io/api => k8s.io/api v0.32.8 # k8s.io/apimachinery => k8s.io/apimachinery v0.32.8