diff --git a/.golangci.yml b/.golangci.yml index 308f663187..34baa5c268 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -57,6 +57,9 @@ linters: - whitespace - zerologlint settings: + govet: + disable: + - inline misspell: ignore-rules: - cancelled diff --git a/.tekton/go.yaml b/.tekton/go.yaml index d91df64432..cce976ef24 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.25.0 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.25.0 - 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: docker.io/golangci/golangci-lint:v2.10.1 + image: docker.io/golang:1.25 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.25.0 - name: GOLANGCILINT_CACHE value: $(workspaces.source.path)/go-build-cache/golangci-cache script: | diff --git a/Makefile b/Makefile index 765dd6b378..277ca177e2 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.25.0 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 # Safe file list helpers using null-delimited output # Usage: $(call GIT_LS_FILES,,) @@ -91,13 +101,21 @@ lint-e2e-naming: ## check e2e test naming conventions @./hack/check-e2e-test-naming.sh .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: ## runs yamllint on all yaml files @echo "Linting yaml files..." @@ -156,9 +174,9 @@ fix-python-errors: ## fix all python errors generated by ruff [[ -n "$$STATUS" ]] && { echo "Python files has been cleaned 🧹. Cleaned Files: "; echo "$$STATUS" ;} || 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) \ diff --git a/go.mod b/go.mod index 3314a75649..85f93c4eca 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - github.com/tektoncd/pipeline v1.9.2 + github.com/tektoncd/pipeline v1.9.3 gitlab.com/gitlab-org/api/client-go v1.14.0 go.opencensus.io v0.24.0 go.uber.org/zap v1.27.1 @@ -54,8 +54,8 @@ require ( github.com/cloudevents/sdk-go/sql/v2 v2.16.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v3 v3.0.5 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-openapi/swag/cmdutils v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/fileutils v0.25.4 // indirect @@ -154,7 +154,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 2777cf7cbf..349176a5d6 100644 --- a/go.sum +++ b/go.sum @@ -140,10 +140,10 @@ github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZv github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 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.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= -github.com/go-jose/go-jose/v3 v3.0.4/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/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.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= @@ -482,8 +482,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= -github.com/tektoncd/pipeline v1.9.2 h1:uKEt6CGLmkeKLdKIZnel0gn8lfQ1P7+398yystdBuHU= -github.com/tektoncd/pipeline v1.9.2/go.mod h1:PTlIZ4Mhr8HZDx404O7spJtafiynetTMedCsXStjtHk= +github.com/tektoncd/pipeline v1.9.3 h1:7Z+V2VX5wjz9LoNa16E1RbgH9mpYy5B1KnAMm3H0czc= +github.com/tektoncd/pipeline v1.9.3/go.mod h1:pEruzPp4JM8JK8Nnnih46IPgdtxRPot/i9pUZo8lA9I= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/pkg/adapter/incoming.go b/pkg/adapter/incoming.go index 31d1a9e17c..97c2b95386 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 821f277190..80a86420c1 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)) @@ -1121,7 +1122,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, @@ -1137,7 +1138,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") @@ -1219,7 +1220,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") @@ -1323,7 +1324,7 @@ func Test_parseIncomingPayload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest(tt.method, tt.url, strings.NewReader(tt.body)) + req := httptest.NewRequestWithContext(context.Background(), tt.method, tt.url, strings.NewReader(tt.body)) if tt.headers != nil { req.Header = tt.headers } diff --git a/pkg/adapter/sinker.go b/pkg/adapter/sinker.go index 7b9dae8510..96b33a0c23 100644 --- a/pkg/adapter/sinker.go +++ b/pkg/adapter/sinker.go @@ -66,12 +66,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/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 5552b0c7cc..0db553ace4 100644 --- a/pkg/provider/github/github.go +++ b/pkg/provider/github/github.go @@ -325,6 +325,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 } @@ -850,7 +878,8 @@ func (v *Provider) debugCommentPhase(event *info.Event, trace commentTraceLogCon pr = event.PullRequestNumber } - baseFields := []any{ + baseFields := make([]any, 0, 18+len(kv)) + baseFields = append(baseFields, "phase", phase, "organization", org, "repository", repo, @@ -860,7 +889,7 @@ func (v *Provider) debugCommentPhase(event *info.Event, trace commentTraceLogCon "marker_hash", trace.markerHash, "marker_len", trace.markerLen, "controller_label", trace.controllerLabel, - } + ) v.Logger.Debugw("github comment dedup flow", append(baseFields, kv...)...) } diff --git a/pkg/provider/github/parse_payload.go b/pkg/provider/github/parse_payload.go index 4c63502906..2ebd3b3335 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" @@ -29,6 +31,10 @@ const ( maxRetriesForMergeCommit = 3 githubNoreplyEmail = "noreply@github.com" githubWebFlowUser = "web-flow" + + 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. @@ -97,6 +103,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 == "" { @@ -118,18 +153,88 @@ 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 -1, nil + 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 + } + 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 @@ -146,32 +251,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 @@ -479,6 +598,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() @@ -510,6 +630,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") } @@ -623,6 +744,7 @@ func (v *Provider) handleCommitCommentEvent(ctx context.Context, event *github.C runevent.TriggerTarget = triggertype.Push opscomments.SetEventTypeAndTargetPR(runevent, event.GetComment().GetBody()) v.userType = event.GetSender().GetType() + 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 86f3d9446a..6cf0e7ffb7 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" @@ -9,7 +12,6 @@ import ( "github.com/google/go-github/v81/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" @@ -70,6 +72,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{ @@ -1200,6 +1208,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{ { @@ -1210,6 +1219,7 @@ func TestAppTokenGeneration(t *testing.T) { Data: map[string][]byte{ "github-application-id": []byte("12345"), "github-private-key": []byte(fakePrivateKey), + "webhook.secret": []byte(webhookSecret), }, }, }, @@ -1226,6 +1236,7 @@ func TestAppTokenGeneration(t *testing.T) { Data: map[string][]byte{ "github-application-id": []byte("abcd"), "github-private-key": []byte(fakePrivateKey), + "webhook.secret": []byte(webhookSecret), }, }, }, @@ -1242,26 +1253,59 @@ func TestAppTokenGeneration(t *testing.T) { Data: map[string][]byte{ "github-application-id": []byte("12345"), "github-private-key": []byte("invalid-key"), + "webhook.secret": []byte(webhookSecret), }, }, }, }) tests := []struct { - ctx context.Context - ctxNS string - name string - wantErrSubst string - nilClient bool - seedData testclient.Clients - envs map[string]string + ctx context.Context + ctxNS string + name string + wantErrSubst string + nilClient bool + seedData testclient.Clients + 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, @@ -1292,8 +1336,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{ @@ -1301,24 +1343,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, }, @@ -1327,6 +1366,16 @@ func TestAppTokenGeneration(t *testing.T) { }, } + 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) @@ -1334,6 +1383,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 3d4b5c7e9a..e9aa9e7ef5 100644 --- a/pkg/resolve/remote.go +++ b/pkg/resolve/remote.go @@ -119,8 +119,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) @@ -178,7 +178,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) @@ -210,7 +210,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 7bf4378e27..feeb8a0ad8 100644 --- a/pkg/resolve/remote_test.go +++ b/pkg/resolve/remote_test.go @@ -465,6 +465,84 @@ 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") +} + func TestAssembleTaskFQDNs(t *testing.T) { tests := []struct { name string diff --git a/vendor/github.com/go-jose/go-jose/v3/asymmetric.go b/vendor/github.com/go-jose/go-jose/v3/asymmetric.go index d4d4961b24..79d9ee57cf 100644 --- a/vendor/github.com/go-jose/go-jose/v3/asymmetric.go +++ b/vendor/github.com/go-jose/go-jose/v3/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/v3/cipher/key_wrap.go b/vendor/github.com/go-jose/go-jose/v3/cipher/key_wrap.go index b9effbca8a..a2f86e3db9 100644 --- a/vendor/github.com/go-jose/go-jose/v3/cipher/key_wrap.go +++ b/vendor/github.com/go-jose/go-jose/v3/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/v3/symmetric.go b/vendor/github.com/go-jose/go-jose/v3/symmetric.go index 10d8e19fd1..4a4bba2cb6 100644 --- a/vendor/github.com/go-jose/go-jose/v3/symmetric.go +++ b/vendor/github.com/go-jose/go-jose/v3/symmetric.go @@ -364,11 +364,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 +393,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien parts := &aeadParts{ iv: iv.bytes(), - ciphertext: recipient.encryptedKey, + ciphertext: encryptedKey, tag: tag.bytes(), } @@ -399,7 +409,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 } @@ -440,7 +450,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/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/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1/container_validation.go b/vendor/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1/container_validation.go index becf35411d..5167e8bb9b 100644 --- a/vendor/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1/container_validation.go +++ b/vendor/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1/container_validation.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "path/filepath" "regexp" "slices" "strings" @@ -197,8 +198,9 @@ func (s *Step) Validate(ctx context.Context) (errs *apis.FieldError) { } for j, vm := range s.VolumeMounts { - if strings.HasPrefix(vm.MountPath, "/tekton/") && - !strings.HasPrefix(vm.MountPath, "/tekton/home") { + cleanMountPath := filepath.Clean(vm.MountPath) + if strings.HasPrefix(cleanMountPath, "/tekton/") && + !strings.HasPrefix(cleanMountPath, "/tekton/home") { errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("volumeMount cannot be mounted under /tekton/ (volumeMount %q mounted at %q)", vm.Name, vm.MountPath), "mountPath").ViaFieldIndex("volumeMounts", j)) } if strings.HasPrefix(vm.Name, "tekton-internal-") { diff --git a/vendor/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1/task_validation.go b/vendor/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1/task_validation.go index c2d918d4b3..8578375af7 100644 --- a/vendor/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1/task_validation.go +++ b/vendor/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1/task_validation.go @@ -434,8 +434,9 @@ func validateStep(ctx context.Context, s Step, names sets.String) (errs *apis.Fi } for j, vm := range s.VolumeMounts { - if strings.HasPrefix(vm.MountPath, "/tekton/") && - !strings.HasPrefix(vm.MountPath, "/tekton/home") { + cleanMountPath := filepath.Clean(vm.MountPath) + if strings.HasPrefix(cleanMountPath, "/tekton/") && + !strings.HasPrefix(cleanMountPath, "/tekton/home") { errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("volumeMount cannot be mounted under /tekton/ (volumeMount %q mounted at %q)", vm.Name, vm.MountPath), "mountPath").ViaFieldIndex("volumeMounts", j)) } if strings.HasPrefix(vm.Name, "tekton-internal-") { diff --git a/vendor/modules.txt b/vendor/modules.txt index 8a72ba4b6f..bb2516fc86 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -124,14 +124,14 @@ github.com/fxamacker/cbor/v2 # github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e ## explicit; go 1.13 github.com/go-fed/httpsig -# github.com/go-jose/go-jose/v3 v3.0.4 +# github.com/go-jose/go-jose/v3 v3.0.5 ## explicit; go 1.12 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.3 => 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 @@ -399,8 +399,8 @@ github.com/spf13/pflag ## explicit; go 1.17 github.com/stretchr/testify/assert github.com/stretchr/testify/assert/yaml -# github.com/tektoncd/pipeline v1.9.2 -## explicit; go 1.24.0 +# github.com/tektoncd/pipeline v1.9.3 +## explicit; go 1.24.13 github.com/tektoncd/pipeline/internal/artifactref github.com/tektoncd/pipeline/pkg/apis/config github.com/tektoncd/pipeline/pkg/apis/pipeline @@ -1315,7 +1315,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