1
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2024-10-02 13:29:10 +00:00

Compare commits

...

3 commits

Author SHA1 Message Date
Finn
6c10f85fb5 fix query param 2024-09-30 11:11:41 -07:00
Finn
962d726bed address linter concern 2024-09-30 11:04:23 -07:00
Finn
b14f3946e2 Use existing JWT signing key code 2024-09-30 09:11:45 -07:00
10 changed files with 432 additions and 408 deletions

File diff suppressed because one or more lines are too long

1
go.mod
View file

@ -85,7 +85,6 @@ require (
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.18.0
github.com/quasoft/websspi v1.1.2
github.com/rakutentech/jwk-go v1.1.3
github.com/redis/go-redis/v9 v9.6.1
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1

5
go.sum
View file

@ -542,13 +542,11 @@ github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5Bn
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
@ -585,8 +583,6 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
github.com/rakutentech/jwk-go v1.1.3 h1:PiLwepKyUaW+QFG3ki78DIO2+b4IVK3nMhlxM70zrQ4=
github.com/rakutentech/jwk-go v1.1.3/go.mod h1:LtzSv4/+Iti1nnNeVQiP6l5cI74GBStbhyXCYvgPZFk=
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
@ -729,7 +725,6 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View file

@ -0,0 +1,354 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package jwtx
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"github.com/golang-jwt/jwt/v5"
)
// ErrInvalidAlgorithmType represents an invalid algorithm error.
type ErrInvalidAlgorithmType struct {
Algorithm string
}
func (err ErrInvalidAlgorithmType) Error() string {
return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorithm)
}
// JWTSigningKey represents a algorithm/key pair to sign JWTs
type JWTSigningKey interface {
IsSymmetric() bool
SigningMethod() jwt.SigningMethod
SignKey() any
VerifyKey() any
ToJWK() (map[string]string, error)
PreProcessToken(*jwt.Token)
}
type hmacSigningKey struct {
signingMethod jwt.SigningMethod
secret []byte
}
func (key hmacSigningKey) IsSymmetric() bool {
return true
}
func (key hmacSigningKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key hmacSigningKey) SignKey() any {
return key.secret
}
func (key hmacSigningKey) VerifyKey() any {
return key.secret
}
func (key hmacSigningKey) ToJWK() (map[string]string, error) {
return map[string]string{
"kty": "oct",
"alg": key.SigningMethod().Alg(),
}, nil
}
func (key hmacSigningKey) PreProcessToken(*jwt.Token) {}
type rsaSingingKey struct {
signingMethod jwt.SigningMethod
key *rsa.PrivateKey
id string
}
func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) {
kid, err := util.CreatePublicKeyFingerprint(key.Public().(*rsa.PublicKey))
if err != nil {
return rsaSingingKey{}, err
}
return rsaSingingKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key rsaSingingKey) IsSymmetric() bool {
return false
}
func (key rsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key rsaSingingKey) SignKey() any {
return key.key
}
func (key rsaSingingKey) VerifyKey() any {
return key.key.Public()
}
func (key rsaSingingKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(*rsa.PublicKey)
return map[string]string{
"kty": "RSA",
"alg": key.SigningMethod().Alg(),
"kid": key.id,
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()),
"n": base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()),
}, nil
}
func (key rsaSingingKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
type eddsaSigningKey struct {
signingMethod jwt.SigningMethod
key ed25519.PrivateKey
id string
}
func newEdDSASingingKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) {
kid, err := util.CreatePublicKeyFingerprint(key.Public().(ed25519.PublicKey))
if err != nil {
return eddsaSigningKey{}, err
}
return eddsaSigningKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key eddsaSigningKey) IsSymmetric() bool {
return false
}
func (key eddsaSigningKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key eddsaSigningKey) SignKey() any {
return key.key
}
func (key eddsaSigningKey) VerifyKey() any {
return key.key.Public()
}
func (key eddsaSigningKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(ed25519.PublicKey)
return map[string]string{
"alg": key.SigningMethod().Alg(),
"kid": key.id,
"kty": "OKP",
"crv": "Ed25519",
"x": base64.RawURLEncoding.EncodeToString(pubKey),
}, nil
}
func (key eddsaSigningKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
type ecdsaSingingKey struct {
signingMethod jwt.SigningMethod
key *ecdsa.PrivateKey
id string
}
func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) {
kid, err := util.CreatePublicKeyFingerprint(key.Public().(*ecdsa.PublicKey))
if err != nil {
return ecdsaSingingKey{}, err
}
return ecdsaSingingKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key ecdsaSingingKey) IsSymmetric() bool {
return false
}
func (key ecdsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key ecdsaSingingKey) SignKey() any {
return key.key
}
func (key ecdsaSingingKey) VerifyKey() any {
return key.key.Public()
}
func (key ecdsaSingingKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(*ecdsa.PublicKey)
return map[string]string{
"kty": "EC",
"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()),
}, nil
}
func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
// CreateJWTSigningKey creates a signing key from an algorithm / key pair.
func CreateJWTSigningKey(algorithm string, key any) (JWTSigningKey, error) {
var signingMethod jwt.SigningMethod
switch algorithm {
case "HS256":
signingMethod = jwt.SigningMethodHS256
case "HS384":
signingMethod = jwt.SigningMethodHS384
case "HS512":
signingMethod = jwt.SigningMethodHS512
case "RS256":
signingMethod = jwt.SigningMethodRS256
case "RS384":
signingMethod = jwt.SigningMethodRS384
case "RS512":
signingMethod = jwt.SigningMethodRS512
case "ES256":
signingMethod = jwt.SigningMethodES256
case "ES384":
signingMethod = jwt.SigningMethodES384
case "ES512":
signingMethod = jwt.SigningMethodES512
case "EdDSA":
signingMethod = jwt.SigningMethodEdDSA
default:
return nil, ErrInvalidAlgorithmType{algorithm}
}
switch signingMethod.(type) {
case *jwt.SigningMethodEd25519:
privateKey, ok := key.(ed25519.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newEdDSASingingKey(signingMethod, privateKey)
case *jwt.SigningMethodECDSA:
privateKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newECDSASingingKey(signingMethod, privateKey)
case *jwt.SigningMethodRSA:
privateKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newRSASingingKey(signingMethod, privateKey)
default:
secret, ok := key.([]byte)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return hmacSigningKey{signingMethod, secret}, nil
}
}
// LoadOrCreateAsymmetricKey checks if the configured private key exists.
// If it does not exist a new random key gets generated and saved on the configured path.
func LoadOrCreateAsymmetricKey(keyPath, algo string) (any, error) {
isExist, err := util.IsExist(keyPath)
if err != nil {
log.Fatal("Unable to check if %s exists. Error: %v", keyPath, err)
}
if !isExist {
err := func() error {
key, err := func() (any, error) {
switch {
case strings.HasPrefix(algo, "RS"):
return rsa.GenerateKey(rand.Reader, 4096)
case algo == "EdDSA":
_, pk, err := ed25519.GenerateKey(rand.Reader)
return pk, err
default:
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
}()
if err != nil {
return err
}
bytes, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: bytes}
if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil {
return err
}
f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
defer func() {
if err = f.Close(); err != nil {
log.Error("Close: %v", err)
}
}()
return pem.Encode(f, privateKeyPEM)
}()
if err != nil {
log.Fatal("Error generating private key: %v", err)
return nil, err
}
}
bytes, err := os.ReadFile(keyPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode(bytes)
if block == nil {
return nil, fmt.Errorf("no valid PEM data found in %s", keyPath)
} else if block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("expected PRIVATE KEY, got %s in %s", block.Type, keyPath)
}
return x509.ParsePKCS8PrivateKey(block.Bytes)
}

View file

@ -5,6 +5,7 @@ package setting
import (
"fmt"
"path/filepath"
"strings"
"time"
)
@ -12,23 +13,27 @@ import (
// Actions settings
var (
Actions = struct {
Enabled bool
LogStorage *Storage // how the created logs should be stored
LogRetentionDays int64 `ini:"LOG_RETENTION_DAYS"`
LogCompression logCompression `ini:"LOG_COMPRESSION"`
ArtifactStorage *Storage // how the created artifacts should be stored
ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"`
DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"`
ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"`
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
SkipWorkflowStrings []string `ìni:"SKIP_WORKFLOW_STRINGS"`
LimitDispatchInputs int64 `ini:"LIMIT_DISPATCH_INPUTS"`
Enabled bool
LogStorage *Storage // how the created logs should be stored
LogRetentionDays int64 `ini:"LOG_RETENTION_DAYS"`
LogCompression logCompression `ini:"LOG_COMPRESSION"`
ArtifactStorage *Storage // how the created artifacts should be stored
ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"`
DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"`
ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"`
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
SkipWorkflowStrings []string `ìni:"SKIP_WORKFLOW_STRINGS"`
LimitDispatchInputs int64 `ini:"LIMIT_DISPATCH_INPUTS"`
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
}{
Enabled: true,
DefaultActionsURL: defaultActionsURLForgejo,
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
LimitDispatchInputs: 10,
Enabled: true,
DefaultActionsURL: defaultActionsURLForgejo,
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
LimitDispatchInputs: 10,
JWTSigningAlgorithm: "EdDSA",
JWTSigningPrivateKeyFile: "actions_oidc/private.pem",
}
)
@ -102,5 +107,9 @@ func loadActionsFrom(rootCfg ConfigProvider) error {
return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression)
}
if !filepath.IsAbs(Actions.JWTSigningPrivateKeyFile) {
Actions.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, Actions.JWTSigningPrivateKeyFile)
}
return nil
}

View file

@ -1,27 +1,23 @@
package actions
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/jwtx"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/rakutentech/jwk-go/jwk"
"github.com/rakutentech/jwk-go/okp"
)
type oidcRoutes struct {
ca ed25519.PrivateKey
jwks []*jwk.KeySpec
signingKey jwtx.JWTSigningKey
openIDConfiguration openIDConfiguration
}
@ -40,17 +36,18 @@ func OIDCRoutes(prefix string) *web.Route {
prefix = strings.TrimPrefix(prefix, "/")
// TODO: generate this once and store it across restarts. In the database I assume?
caPublicKey, caPrivateKey, err := ed25519.GenerateKey(rand.Reader)
rawKey, err := jwtx.LoadOrCreateAsymmetricKey(setting.Actions.JWTSigningPrivateKeyFile, setting.Actions.JWTSigningAlgorithm)
if err != nil {
panic(err)
log.Fatal("error loading jwt: %v", err)
}
key, err := jwtx.CreateJWTSigningKey(setting.Actions.JWTSigningAlgorithm, rawKey)
if err != nil {
log.Fatal("error parsing jwt: %v", err)
}
r := oidcRoutes{
ca: caPrivateKey,
jwks: []*jwk.KeySpec{ // https://token.actions.githubusercontent.com/.well-known/jwks
jwk.NewSpec(okp.NewCurve25519(caPublicKey, caPrivateKey)),
},
signingKey: key,
openIDConfiguration: openIDConfiguration{
Issuer: setting.AppURL + setting.AppSubURL + prefix, // TODO: how do i check the public domain?
JwksURI: setting.AppURL + setting.AppSubURL + prefix + "/.well-known/jwks", // TODO: how do i check the public domain?
@ -112,6 +109,8 @@ func OIDCRoutes(prefix string) *web.Route {
func (o oidcRoutes) getToken(ctx *ArtifactContext) {
task := ctx.ActionTask
aud := ctx.Req.URL.Query().Get("audience")
if err := task.Job.LoadRun(ctx); err != nil {
log.Error("Error loading run: %v", err)
ctx.Error(http.StatusInternalServerError, "Error loading run")
@ -132,17 +131,17 @@ func (o oidcRoutes) getToken(ctx *ArtifactContext) {
}
iat := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
token := jwt.NewWithClaims(jwt.GetSigningMethod(setting.Actions.JWTSigningAlgorithm), jwt.MapClaims{
"jti": uuid.New().String(),
"sub": fmt.Sprintf("repo:%s:ref:%s", repo, task.Job.Run.Ref),
"aud": "", // TODO: Allow customizing this in the query param
"aud": aud,
"ref": task.Job.Run.Ref,
"sha": task.Job.Run.CommitSHA,
"repository": repo,
"repository_owner": task.Job.Run.Repo.OwnerName,
"repository_owner_id": task.Job.Run.Repo.OwnerID,
"run_id": task.Job.RunID,
"run_number": 0, // TODO: how do i check this?
"run_number": task.Job.Run.Index,
"run_attempt": 0, // TODO: how do i check this?
"repository_visibility": repositoryVisibility,
"repository_id": task.Job.Run.Repo.ID,
@ -155,17 +154,17 @@ func (o oidcRoutes) getToken(ctx *ArtifactContext) {
"ref_protected": false, // TODO: how do i check this?
"ref_type": "branch", // TODO: how do i check this?
"workflow_ref": fmt.Sprintf("%s/.forgejo/workflow/%s@%s", repo, task.Job.Run.WorkflowID, task.Job.Run.Ref),
"workflow_sha": "", // TODO: is this just a hash of the yaml? if so that's easy enough to calculate
"workflow_sha": task.Job.Run.CommitSHA,
"job_workflow_ref": fmt.Sprintf("%s/.forgejo/workflow/%s@%s", repo, task.Job.Run.WorkflowID, task.Job.Run.Ref),
"job_workflow_sha": "", // TODO: is this just a hash of the yaml? if so that's easy enough to calculate
"runner_environment": "self-hosted", // not sure what this should be set to, github will have either "github-hosted" or "self-hosted"
"iss": setting.AppURL + "/api/actions_token", // TODO: how do i check the public domain?
"job_workflow_sha": task.Job.Run.CommitSHA,
"runner_environment": "self-hosted", // not sure what this should be set to, github will have either "github-hosted" or "self-hosted"
"iss": setting.AppURL + setting.AppSubURL + "/api/actions_token",
"nbf": jwt.NewNumericDate(iat),
"exp": jwt.NewNumericDate(iat.Add(time.Minute * 15)),
"iat": jwt.NewNumericDate(iat),
})
signedJWT, err := token.SignedString(o.ca)
signedJWT, err := token.SignedString(o.signingKey.SignKey())
if err != nil {
log.Error("Error signing JWT: %v", err)
ctx.Error(http.StatusInternalServerError, "Error signing JWT")
@ -180,7 +179,23 @@ func (o oidcRoutes) getToken(ctx *ArtifactContext) {
func (o oidcRoutes) getJWKS(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(resp).Encode(o.jwks)
jwk, err := o.signingKey.ToJWK()
if err != nil {
log.Error("Error converting signing key to JWK: %v", err)
http.Error(resp, "error converting signing key to JWT", http.StatusInternalServerError)
return
}
jwk["use"] = "sig"
jwks := map[string][]map[string]string{
"keys": {
jwk,
},
}
err = json.NewEncoder(resp).Encode(jwks)
if err != nil {
log.Error("error encoding jwks response: ", err)
http.Error(resp, "error encoding jwks response", http.StatusInternalServerError)

View file

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/jwtx"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@ -152,7 +153,7 @@ type AccessTokenResponse struct {
IDToken string `json:"id_token,omitempty"`
}
func newAccessTokenResponse(ctx go_context.Context, grant *auth.OAuth2Grant, serverKey, clientKey oauth2.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
func newAccessTokenResponse(ctx go_context.Context, grant *auth.OAuth2Grant, serverKey, clientKey jwtx.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(ctx); err != nil {
return nil, &AccessTokenError{
@ -734,7 +735,7 @@ func AccessTokenOAuth(ctx *context.Context) {
clientKey := serverKey
if serverKey.IsSymmetric() {
var err error
clientKey, err = oauth2.CreateJWTSigningKey(serverKey.SigningMethod().Alg(), []byte(form.ClientSecret))
clientKey, err = jwtx.CreateJWTSigningKey(serverKey.SigningMethod().Alg(), []byte(form.ClientSecret))
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
@ -757,7 +758,7 @@ func AccessTokenOAuth(ctx *context.Context) {
}
}
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey jwtx.JWTSigningKey) {
app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
@ -817,7 +818,7 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
ctx.JSON(http.StatusOK, accessToken)
}
func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey jwtx.JWTSigningKey) {
app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{

View file

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/jwtx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/auth/source/oauth2"
@ -19,7 +20,7 @@ import (
)
func createAndParseToken(t *testing.T, grant *auth.OAuth2Grant) *oauth2.OIDCToken {
signingKey, err := oauth2.CreateJWTSigningKey("HS256", make([]byte, 32))
signingKey, err := jwtx.CreateJWTSigningKey("HS256", make([]byte, 32))
require.NoError(t, err)
assert.NotNil(t, signingKey)

View file

@ -4,290 +4,14 @@
package oauth2
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/jwtx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/golang-jwt/jwt/v5"
)
// ErrInvalidAlgorithmType represents an invalid algorithm error.
type ErrInvalidAlgorithmType struct {
Algorithm string
}
func (err ErrInvalidAlgorithmType) Error() string {
return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorithm)
}
// JWTSigningKey represents a algorithm/key pair to sign JWTs
type JWTSigningKey interface {
IsSymmetric() bool
SigningMethod() jwt.SigningMethod
SignKey() any
VerifyKey() any
ToJWK() (map[string]string, error)
PreProcessToken(*jwt.Token)
}
type hmacSigningKey struct {
signingMethod jwt.SigningMethod
secret []byte
}
func (key hmacSigningKey) IsSymmetric() bool {
return true
}
func (key hmacSigningKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key hmacSigningKey) SignKey() any {
return key.secret
}
func (key hmacSigningKey) VerifyKey() any {
return key.secret
}
func (key hmacSigningKey) ToJWK() (map[string]string, error) {
return map[string]string{
"kty": "oct",
"alg": key.SigningMethod().Alg(),
}, nil
}
func (key hmacSigningKey) PreProcessToken(*jwt.Token) {}
type rsaSingingKey struct {
signingMethod jwt.SigningMethod
key *rsa.PrivateKey
id string
}
func newRSASingingKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSingingKey, error) {
kid, err := util.CreatePublicKeyFingerprint(key.Public().(*rsa.PublicKey))
if err != nil {
return rsaSingingKey{}, err
}
return rsaSingingKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key rsaSingingKey) IsSymmetric() bool {
return false
}
func (key rsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key rsaSingingKey) SignKey() any {
return key.key
}
func (key rsaSingingKey) VerifyKey() any {
return key.key.Public()
}
func (key rsaSingingKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(*rsa.PublicKey)
return map[string]string{
"kty": "RSA",
"alg": key.SigningMethod().Alg(),
"kid": key.id,
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()),
"n": base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()),
}, nil
}
func (key rsaSingingKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
type eddsaSigningKey struct {
signingMethod jwt.SigningMethod
key ed25519.PrivateKey
id string
}
func newEdDSASingingKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) {
kid, err := util.CreatePublicKeyFingerprint(key.Public().(ed25519.PublicKey))
if err != nil {
return eddsaSigningKey{}, err
}
return eddsaSigningKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key eddsaSigningKey) IsSymmetric() bool {
return false
}
func (key eddsaSigningKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key eddsaSigningKey) SignKey() any {
return key.key
}
func (key eddsaSigningKey) VerifyKey() any {
return key.key.Public()
}
func (key eddsaSigningKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(ed25519.PublicKey)
return map[string]string{
"alg": key.SigningMethod().Alg(),
"kid": key.id,
"kty": "OKP",
"crv": "Ed25519",
"x": base64.RawURLEncoding.EncodeToString(pubKey),
}, nil
}
func (key eddsaSigningKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
type ecdsaSingingKey struct {
signingMethod jwt.SigningMethod
key *ecdsa.PrivateKey
id string
}
func newECDSASingingKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSingingKey, error) {
kid, err := util.CreatePublicKeyFingerprint(key.Public().(*ecdsa.PublicKey))
if err != nil {
return ecdsaSingingKey{}, err
}
return ecdsaSingingKey{
signingMethod,
key,
base64.RawURLEncoding.EncodeToString(kid),
}, nil
}
func (key ecdsaSingingKey) IsSymmetric() bool {
return false
}
func (key ecdsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}
func (key ecdsaSingingKey) SignKey() any {
return key.key
}
func (key ecdsaSingingKey) VerifyKey() any {
return key.key.Public()
}
func (key ecdsaSingingKey) ToJWK() (map[string]string, error) {
pubKey := key.key.Public().(*ecdsa.PublicKey)
return map[string]string{
"kty": "EC",
"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()),
}, nil
}
func (key ecdsaSingingKey) PreProcessToken(token *jwt.Token) {
token.Header["kid"] = key.id
}
// CreateJWTSigningKey creates a signing key from an algorithm / key pair.
func CreateJWTSigningKey(algorithm string, key any) (JWTSigningKey, error) {
var signingMethod jwt.SigningMethod
switch algorithm {
case "HS256":
signingMethod = jwt.SigningMethodHS256
case "HS384":
signingMethod = jwt.SigningMethodHS384
case "HS512":
signingMethod = jwt.SigningMethodHS512
case "RS256":
signingMethod = jwt.SigningMethodRS256
case "RS384":
signingMethod = jwt.SigningMethodRS384
case "RS512":
signingMethod = jwt.SigningMethodRS512
case "ES256":
signingMethod = jwt.SigningMethodES256
case "ES384":
signingMethod = jwt.SigningMethodES384
case "ES512":
signingMethod = jwt.SigningMethodES512
case "EdDSA":
signingMethod = jwt.SigningMethodEdDSA
default:
return nil, ErrInvalidAlgorithmType{algorithm}
}
switch signingMethod.(type) {
case *jwt.SigningMethodEd25519:
privateKey, ok := key.(ed25519.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newEdDSASingingKey(signingMethod, privateKey)
case *jwt.SigningMethodECDSA:
privateKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newECDSASingingKey(signingMethod, privateKey)
case *jwt.SigningMethodRSA:
privateKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return newRSASingingKey(signingMethod, privateKey)
default:
secret, ok := key.([]byte)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return hmacSigningKey{signingMethod, secret}, nil
}
}
// DefaultSigningKey is the default signing key for JWTs.
var DefaultSigningKey JWTSigningKey
var DefaultSigningKey jwtx.JWTSigningKey
// InitSigningKey creates the default signing key from settings or creates a random key.
func InitSigningKey() error {
@ -314,16 +38,16 @@ func InitSigningKey() error {
case "ES512":
fallthrough
case "EdDSA":
key, err = loadOrCreateAsymmetricKey()
key, err = jwtx.LoadOrCreateAsymmetricKey(setting.OAuth2.JWTSigningPrivateKeyFile, setting.OAuth2.JWTSigningAlgorithm)
default:
return ErrInvalidAlgorithmType{setting.OAuth2.JWTSigningAlgorithm}
return jwtx.ErrInvalidAlgorithmType{Algorithm: setting.OAuth2.JWTSigningAlgorithm}
}
if err != nil {
return fmt.Errorf("Error while loading or creating JWT key: %w", err)
}
signingKey, err := CreateJWTSigningKey(setting.OAuth2.JWTSigningAlgorithm, key)
signingKey, err := jwtx.CreateJWTSigningKey(setting.OAuth2.JWTSigningAlgorithm, key)
if err != nil {
return err
}
@ -332,73 +56,3 @@ func InitSigningKey() error {
return nil
}
// loadOrCreateAsymmetricKey checks if the configured private key exists.
// If it does not exist a new random key gets generated and saved on the configured path.
func loadOrCreateAsymmetricKey() (any, error) {
keyPath := setting.OAuth2.JWTSigningPrivateKeyFile
isExist, err := util.IsExist(keyPath)
if err != nil {
log.Fatal("Unable to check if %s exists. Error: %v", keyPath, err)
}
if !isExist {
err := func() error {
key, err := func() (any, error) {
switch {
case strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS"):
return rsa.GenerateKey(rand.Reader, 4096)
case setting.OAuth2.JWTSigningAlgorithm == "EdDSA":
_, pk, err := ed25519.GenerateKey(rand.Reader)
return pk, err
default:
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
}()
if err != nil {
return err
}
bytes, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return err
}
privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: bytes}
if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil {
return err
}
f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
defer func() {
if err = f.Close(); err != nil {
log.Error("Close: %v", err)
}
}()
return pem.Encode(f, privateKeyPEM)
}()
if err != nil {
log.Fatal("Error generating private key: %v", err)
return nil, err
}
}
bytes, err := os.ReadFile(keyPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode(bytes)
if block == nil {
return nil, fmt.Errorf("no valid PEM data found in %s", keyPath)
} else if block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("expected PRIVATE KEY, got %s in %s", block.Type, keyPath)
}
return x509.ParsePKCS8PrivateKey(block.Bytes)
}

View file

@ -7,6 +7,7 @@ import (
"fmt"
"time"
"code.gitea.io/gitea/modules/jwtx"
"code.gitea.io/gitea/modules/timeutil"
"github.com/golang-jwt/jwt/v5"
@ -40,7 +41,7 @@ type Token struct {
}
// ParseToken parses a signed jwt string
func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) {
func ParseToken(jwtToken string, signingKey jwtx.JWTSigningKey) (*Token, error) {
parsedToken, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (any, error) {
if token.Method == nil || token.Method.Alg() != signingKey.SigningMethod().Alg() {
return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
@ -62,7 +63,7 @@ func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) {
}
// SignToken signs the token with the JWT secret
func (token *Token) SignToken(signingKey JWTSigningKey) (string, error) {
func (token *Token) SignToken(signingKey jwtx.JWTSigningKey) (string, error) {
token.IssuedAt = jwt.NewNumericDate(time.Now())
jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
signingKey.PreProcessToken(jwtToken)
@ -92,7 +93,7 @@ type OIDCToken struct {
}
// SignToken signs an id_token with the (symmetric) client secret key
func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) {
func (token *OIDCToken) SignToken(signingKey jwtx.JWTSigningKey) (string, error) {
token.IssuedAt = jwt.NewNumericDate(time.Now())
jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
signingKey.PreProcessToken(jwtToken)