From d69a2f859206f97fc5d576bb9cf52883b5bba72c Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 22 Feb 2026 08:45:23 +0100 Subject: [PATCH 1/6] Update tool dependencies and fix new lint issues Update golangci-lint v2.9.0 to v2.10.1, misspell v0.7.0 to v0.8.0, actionlint v1.7.10 to v1.7.11. Fix new QF1012 staticcheck findings by using fmt.Fprintf instead of WriteString(fmt.Sprintf(...)). Add nolint for SA1019 on ecdsa.PublicKey.X/Y deprecated in Go 1.26. Co-Authored-By: Claude Opus 4.6 --- Makefile | 6 +++--- models/repo/repo.go | 2 +- modules/git/foreachref/format.go | 2 +- routers/web/repo/setting/lfs.go | 4 ++-- services/gitdiff/gitdiff.go | 8 ++++---- services/oauth2_provider/jwtsigningkey.go | 4 ++-- services/release/notes.go | 8 ++++---- services/webhook/discord.go | 2 +- services/webhook/feishu.go | 2 +- services/webhook/matrix.go | 4 ++-- services/webhook/msteams.go | 4 ++-- services/webhook/slack.go | 2 +- services/webhook/telegram.go | 2 +- services/webhook/wechatwork.go | 4 ++-- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index cb7742c5c74f3..d8fce11ee23df 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,13 @@ XGO_VERSION := go-1.25.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 -ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10 +ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest diff --git a/models/repo/repo.go b/models/repo/repo.go index 07b9bf30ccd5e..7b7f5adb41379 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -281,7 +281,7 @@ func (repo *Repository) SizeDetailsString() string { var str strings.Builder sizeDetails := repo.SizeDetails() for _, detail := range sizeDetails { - str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) + fmt.Fprintf(&str, "%s: %s, ", detail.Name, base.FileSize(detail.Size)) } return strings.TrimSuffix(str.String(), ", ") } diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index d2f9998fe818c..cee21c5b66846 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -53,7 +53,7 @@ func (f Format) Flag() string { var formatFlag strings.Builder for i, field := range f.fieldNames { // field key and field value - formatFlag.WriteString(fmt.Sprintf("%s %%(%s)", field, field)) + fmt.Fprintf(&formatFlag, "%s %%(%s)", field, field) if i < len(f.fieldNames)-1 { // note: escape delimiters to allow control characters as diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 8a8015035f4b9..a3a60963d438f 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -301,13 +301,13 @@ func LFSFileGet(ctx *context.Context) { if index != len(lines)-1 { line += "\n" } - output.WriteString(fmt.Sprintf(`
  • %s
  • `, index+1, index+1, line)) + fmt.Fprintf(&output, `
  • %s
  • `, index+1, index+1, line) } ctx.Data["FileContent"] = gotemplate.HTML(output.String()) output.Reset() for i := 0; i < len(lines); i++ { - output.WriteString(fmt.Sprintf(`%d`, i+1, i+1)) + fmt.Fprintf(&output, `%d`, i+1, i+1) } ctx.Data["LineNums"] = gotemplate.HTML(output.String()) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 7777cf4a1c35a..b23e5b1b1ccc8 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1594,10 +1594,10 @@ func generatePatchForUnchangedLineFromReader(reader io.Reader, treePath string, // Generate synthetic patch var patchBuilder strings.Builder - patchBuilder.WriteString(fmt.Sprintf("diff --git a/%s b/%s\n", treePath, treePath)) - patchBuilder.WriteString(fmt.Sprintf("--- a/%s\n", treePath)) - patchBuilder.WriteString(fmt.Sprintf("+++ b/%s\n", treePath)) - patchBuilder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", startLine, len(lines), startLine, len(lines))) + fmt.Fprintf(&patchBuilder, "diff --git a/%s b/%s\n", treePath, treePath) + fmt.Fprintf(&patchBuilder, "--- a/%s\n", treePath) + fmt.Fprintf(&patchBuilder, "+++ b/%s\n", treePath) + fmt.Fprintf(&patchBuilder, "@@ -%d,%d +%d,%d @@\n", startLine, len(lines), startLine, len(lines)) for _, lineContent := range lines { patchBuilder.WriteString(" ") diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 03c7403f75c1e..2bf6a15144ac6 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -219,8 +219,8 @@ func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { "alg": key.SigningMethod().Alg(), "kid": key.id, "crv": pubKey.Params().Name, - "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), - "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), + "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), //nolint:staticcheck // deprecated in Go 1.26, but no easy alternative for JWK encoding + "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), //nolint:staticcheck // deprecated in Go 1.26, but no easy alternative for JWK encoding }, nil } diff --git a/services/release/notes.go b/services/release/notes.go index c9dc75af70d08..92ee22e6043cb 100644 --- a/services/release/notes.go +++ b/services/release/notes.go @@ -113,7 +113,7 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, for _, pr := range prs { prURL := pr.Issue.HTMLURL(ctx) - builder.WriteString(fmt.Sprintf("* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL)) + fmt.Fprintf(&builder, "* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL) } builder.WriteString("\n") @@ -121,7 +121,7 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, if len(contributors) > 0 { builder.WriteString("## Contributors\n") for _, contributor := range contributors { - builder.WriteString(fmt.Sprintf("* @%s\n", contributor.Name)) + fmt.Fprintf(&builder, "* @%s\n", contributor.Name) } builder.WriteString("\n") } @@ -130,14 +130,14 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, builder.WriteString("## New Contributors\n") for _, contributor := range newContributors { prURL := contributor.Issue.HTMLURL(ctx) - builder.WriteString(fmt.Sprintf("* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL)) + fmt.Fprintf(&builder, "* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL) } builder.WriteString("\n") } builder.WriteString("**Full Changelog**: ") compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) - builder.WriteString(fmt.Sprintf("[%s...%s](%s)", baseRef, tagName, compareURL)) + fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL) builder.WriteByte('\n') return builder.String() } diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 19af779120da4..c0af7c0243337 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -169,7 +169,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) { if utf8.RuneCountInString(message) > 50 { message = fmt.Sprintf("%.47s...", message) } - text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name)) + fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("\n") diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index ecce9acc43621..ac581df85a366 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -77,7 +77,7 @@ func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) { ) var text strings.Builder - text.WriteString(fmt.Sprintf("[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc)) + fmt.Fprintf(&text, "[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc) // for each commit, generate attachment text for i, commit := range p.Commits { var authorName string diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 63fbbf40a962f..fa01ecd0b198b 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -174,11 +174,11 @@ func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) { repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) var text strings.Builder - text.WriteString(fmt.Sprintf("[%s] %s pushed %s to %s:
    ", repoLink, p.Pusher.UserName, commitDesc, branchLink)) + fmt.Fprintf(&text, "[%s] %s pushed %s to %s:
    ", repoLink, p.Pusher.UserName, commitDesc, branchLink) // for each commit, generate a new line text for i, commit := range p.Commits { - text.WriteString(fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)) + fmt.Fprintf(&text, "%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("
    ") diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index fa39e7228e3ad..34db903712113 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -134,8 +134,8 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) { var text strings.Builder // for each commit, generate attachment text for i, commit := range p.Commits { - text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, - strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)) + fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL, + strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("\n\n") diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 0b3dda467cc09..94d41d2179082 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -211,7 +211,7 @@ func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { var attachmentText strings.Builder // for each commit, generate attachment text for i, commit := range p.Commits { - attachmentText.WriteString(fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name))) + fmt.Fprintf(&attachmentText, "%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)) // add linebreak to each commit but the last if i < len(p.Commits)-1 { attachmentText.WriteString("\n") diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 2abc743fabd3f..8e9a53a5de556 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -96,7 +96,7 @@ func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) { var htmlCommits strings.Builder for _, commit := range p.Commits { - htmlCommits.WriteString(fmt.Sprintf("\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n")))) + fmt.Fprintf(&htmlCommits, "\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n"))) if commit.Author != nil { htmlCommits.WriteString(" - " + html.EscapeString(commit.Author.Name)) } diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index da9c6b584c288..cac6a700c027a 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -86,8 +86,8 @@ func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error } message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n") - text.WriteString(fmt.Sprintf(" > [%s](%s) \r\n >%s \n >%s", commit.ID[:7], commit.URL, - message, authorName)) + fmt.Fprintf(&text, " > [%s](%s) \r\n >%s \n >%s", commit.ID[:7], commit.URL, + message, authorName) // add linebreak to each commit but the last if i < len(p.Commits)-1 { From 94eb57c545c3e40aca72d4e9da5901ad9c493f79 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 22 Feb 2026 08:49:52 +0100 Subject: [PATCH 2/6] Use PublicKey.Bytes() instead of deprecated X/Y fields for JWK encoding Extract EC coordinates from the uncompressed SEC 1 byte representation (0x04 || X || Y) returned by PublicKey.Bytes() instead of directly accessing the deprecated pubKey.X and pubKey.Y fields. Co-Authored-By: Claude Opus 4.6 --- services/oauth2_provider/jwtsigningkey.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 2bf6a15144ac6..9b0b683b707b8 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -214,13 +214,20 @@ func (key ecdsaSingingKey) VerifyKey() any { func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { pubKey := key.key.Public().(*ecdsa.PublicKey) + // PublicKey.Bytes returns the uncompressed SEC 1 format: 0x04 || X || Y + pubKeyBytes, err := pubKey.Bytes() + if err != nil { + return nil, err + } + coordLen := (len(pubKeyBytes) - 1) / 2 + return map[string]string{ "kty": "EC", "alg": key.SigningMethod().Alg(), "kid": key.id, "crv": pubKey.Params().Name, - "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), //nolint:staticcheck // deprecated in Go 1.26, but no easy alternative for JWK encoding - "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), //nolint:staticcheck // deprecated in Go 1.26, but no easy alternative for JWK encoding + "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+coordLen]), + "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+coordLen:]), }, nil } From 2b7c33f6b20951922c06191fddebcb9859923d13 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 22 Feb 2026 08:53:01 +0100 Subject: [PATCH 3/6] Use PublicKey.Bytes() instead of deprecated X/Y fields for JWK encoding Extract EC coordinates from the uncompressed SEC 1 byte representation (0x04 || X || Y) returned by PublicKey.Bytes(), and derive the curve name from coordinate length, avoiding all deprecated ecdsa.PublicKey fields (X, Y, Curve). This also fixes a latent RFC 7518 compliance issue where big.Int.Bytes() could produce shorter-than-expected coordinates by stripping leading zeros (e.g. for P-521). Co-Authored-By: Claude Opus 4.6 --- services/oauth2_provider/jwtsigningkey.go | 9 ++- .../oauth2_provider/jwtsigningkey_test.go | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 services/oauth2_provider/jwtsigningkey_test.go diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 9b0b683b707b8..0b4ea7a19da4a 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -211,6 +211,9 @@ func (key ecdsaSingingKey) VerifyKey() any { return key.key.Public() } +// ecdsaCurveByCoordLen maps EC coordinate byte length to JWK curve name +var ecdsaCurveByCoordLen = map[int]string{32: "P-256", 48: "P-384", 66: "P-521"} + func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { pubKey := key.key.Public().(*ecdsa.PublicKey) @@ -220,12 +223,16 @@ func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { return nil, err } coordLen := (len(pubKeyBytes) - 1) / 2 + curveName, ok := ecdsaCurveByCoordLen[coordLen] + if !ok { + return nil, fmt.Errorf("unsupported EC coordinate length: %d", coordLen) + } return map[string]string{ "kty": "EC", "alg": key.SigningMethod().Alg(), "kid": key.id, - "crv": pubKey.Params().Name, + "crv": curveName, "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+coordLen]), "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+coordLen:]), }, nil diff --git a/services/oauth2_provider/jwtsigningkey_test.go b/services/oauth2_provider/jwtsigningkey_test.go new file mode 100644 index 0000000000000..55de81dfd88c1 --- /dev/null +++ b/services/oauth2_provider/jwtsigningkey_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2_provider + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "math/big" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestECDSASigningKeyToJWK(t *testing.T) { + for _, tc := range []struct { + curve elliptic.Curve + signingMethod jwt.SigningMethod + expectedAlg string + expectedCrv string + coordLen int + }{ + {elliptic.P256(), jwt.SigningMethodES256, "ES256", "P-256", 32}, + {elliptic.P384(), jwt.SigningMethodES384, "ES384", "P-384", 48}, + {elliptic.P521(), jwt.SigningMethodES512, "ES512", "P-521", 66}, + } { + t.Run(tc.expectedCrv, func(t *testing.T) { + privKey, err := ecdsa.GenerateKey(tc.curve, rand.Reader) + require.NoError(t, err) + + signingKey, err := newECDSASingingKey(tc.signingMethod, privKey) + require.NoError(t, err) + + jwk, err := signingKey.ToJWK() + require.NoError(t, err) + + assert.Equal(t, "EC", jwk["kty"]) + assert.Equal(t, tc.expectedAlg, jwk["alg"]) + assert.Equal(t, tc.expectedCrv, jwk["crv"]) + assert.NotEmpty(t, jwk["kid"]) + + // Verify coordinates are the correct fixed length per RFC 7518 / SEC 1 + xBytes, err := base64.RawURLEncoding.DecodeString(jwk["x"]) + require.NoError(t, err) + assert.Len(t, xBytes, tc.coordLen) + + yBytes, err := base64.RawURLEncoding.DecodeString(jwk["y"]) + require.NoError(t, err) + assert.Len(t, yBytes, tc.coordLen) + + // Verify the decoded coordinates reconstruct the original public key point + pubKey := privKey.Public().(*ecdsa.PublicKey) + assert.Equal(t, 0, new(big.Int).SetBytes(xBytes).Cmp(pubKey.X)) + assert.Equal(t, 0, new(big.Int).SetBytes(yBytes).Cmp(pubKey.Y)) + }) + } +} From 5d87d6dc71924158a2308c039ec9bc9572e4555f Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 22 Feb 2026 09:16:13 +0100 Subject: [PATCH 4/6] Validate SEC 1 encoding and derive curve from signing algorithm in ToJWK Instead of inferring the curve name from coordinate length (which could be ambiguous), derive it from the JWT signing algorithm. Also validate that the SEC 1 point encoding has the 0x04 uncompressed prefix and the expected total length. Co-Authored-By: Claude Opus 4.6 --- services/oauth2_provider/jwtsigningkey.go | 33 ++++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 0b4ea7a19da4a..5b1d338255d8e 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -211,10 +211,23 @@ func (key ecdsaSingingKey) VerifyKey() any { return key.key.Public() } -// ecdsaCurveByCoordLen maps EC coordinate byte length to JWK curve name -var ecdsaCurveByCoordLen = map[int]string{32: "P-256", 48: "P-384", 66: "P-521"} +// ecdsaCurveParams maps JWT signing algorithm to JWK curve name and coordinate byte length. +var ecdsaCurveParams = map[string]struct { + curveName string + coordLen int +}{ + "ES256": {"P-256", 32}, + "ES384": {"P-384", 48}, + "ES512": {"P-521", 66}, +} func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { + alg := key.SigningMethod().Alg() + params, ok := ecdsaCurveParams[alg] + if !ok { + return nil, fmt.Errorf("unsupported ECDSA signing algorithm: %s", alg) + } + pubKey := key.key.Public().(*ecdsa.PublicKey) // PublicKey.Bytes returns the uncompressed SEC 1 format: 0x04 || X || Y @@ -222,19 +235,19 @@ func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { if err != nil { return nil, err } - coordLen := (len(pubKeyBytes) - 1) / 2 - curveName, ok := ecdsaCurveByCoordLen[coordLen] - if !ok { - return nil, fmt.Errorf("unsupported EC coordinate length: %d", coordLen) + + expectedLen := 1 + 2*params.coordLen + if len(pubKeyBytes) != expectedLen || pubKeyBytes[0] != 0x04 { + return nil, fmt.Errorf("invalid SEC 1 uncompressed point encoding for %s", alg) } return map[string]string{ "kty": "EC", - "alg": key.SigningMethod().Alg(), + "alg": alg, "kid": key.id, - "crv": curveName, - "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+coordLen]), - "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+coordLen:]), + "crv": params.curveName, + "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+params.coordLen]), + "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+params.coordLen:]), }, nil } From d999c49cd83be9ff59849506df753c990355bdd0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 22 Feb 2026 12:04:33 +0100 Subject: [PATCH 5/6] Simplify ECDSA ToJWK by removing unnecessary SEC 1 validation Co-Authored-By: Claude Opus 4.6 --- services/oauth2_provider/jwtsigningkey.go | 30 ++++------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 5b1d338255d8e..4898d54166a47 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -211,23 +211,7 @@ func (key ecdsaSingingKey) VerifyKey() any { return key.key.Public() } -// ecdsaCurveParams maps JWT signing algorithm to JWK curve name and coordinate byte length. -var ecdsaCurveParams = map[string]struct { - curveName string - coordLen int -}{ - "ES256": {"P-256", 32}, - "ES384": {"P-384", 48}, - "ES512": {"P-521", 66}, -} - func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { - alg := key.SigningMethod().Alg() - params, ok := ecdsaCurveParams[alg] - if !ok { - return nil, fmt.Errorf("unsupported ECDSA signing algorithm: %s", alg) - } - pubKey := key.key.Public().(*ecdsa.PublicKey) // PublicKey.Bytes returns the uncompressed SEC 1 format: 0x04 || X || Y @@ -236,18 +220,14 @@ func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { return nil, err } - expectedLen := 1 + 2*params.coordLen - if len(pubKeyBytes) != expectedLen || pubKeyBytes[0] != 0x04 { - return nil, fmt.Errorf("invalid SEC 1 uncompressed point encoding for %s", alg) - } - + coordLen := (len(pubKeyBytes) - 1) / 2 return map[string]string{ "kty": "EC", - "alg": alg, + "alg": key.SigningMethod().Alg(), "kid": key.id, - "crv": params.curveName, - "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+params.coordLen]), - "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+params.coordLen:]), + "crv": pubKey.Params().Name, + "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+coordLen]), + "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+coordLen:]), }, nil } From d5ddf1768d3c0f3ecbefddcc554d12163bd41613 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 22 Feb 2026 12:47:08 +0100 Subject: [PATCH 6/6] Remove dead staticcheck linter exclusion Co-Authored-By: Claude Opus 4.6 --- .golangci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2b85c89fdce12..4e01169dc6826 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -141,9 +141,6 @@ linters: - linters: - unused text: (?i)swagger - - linters: - - staticcheck - text: (?i)argument x is overwritten before first use - linters: - gocritic text: '(?i)commentFormatting: put a space between `//` and comment text'