Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add asymmetric JWT signing #16010

Merged
merged 16 commits into from
Jun 17, 2021
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Load signing key from settings.
  • Loading branch information
KN4CK3R committed May 30, 2021
commit 7e814d06a6c317c750fbfe5a3d6e7f8e00540d0b
2 changes: 1 addition & 1 deletion cmd/generate.go
Original file line number Diff line number Diff line change
@@ -71,7 +71,7 @@ func runGenerateInternalToken(c *cli.Context) error {
}

func runGenerateLfsJwtSecret(c *cli.Context) error {
JWTSecretBase64, err := generate.NewJwtSecret()
JWTSecretBase64, err := generate.NewJwtSecretBase64()
if err != nil {
return err
}
136 changes: 131 additions & 5 deletions modules/auth/oauth2/jwtsigningkey.go
Original file line number Diff line number Diff line change
@@ -6,14 +6,26 @@ package oauth2

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"strings"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"

"github.com/dgrijalva/jwt-go"
ini "gopkg.in/ini.v1"
)

// ErrInvalidAlgorithmType represents an invalid algorithm error.
@@ -178,13 +190,127 @@ func CreateJWTSingingKey(algorithm string, key interface{}) (JWTSigningKey, erro
var DefaultSigningKey JWTSigningKey

// InitSigningKey creates the default signing key from settings or creates a random key.
func InitSigningKey() error {
key, err := CreateJWTSingingKey("HS256", setting.OAuth2.JWTSecretBytes)
func InitSigningKey() (err error) {
var key interface{}

switch setting.OAuth2.JWTSigningAlgorithm {
case "HS256":
fallthrough
case "HS384":
fallthrough
case "HS512":
key, err = loadOrCreateSymmetricKey()

case "RS256":
fallthrough
case "RS384":
fallthrough
case "RS512":
fallthrough
case "ES256":
fallthrough
case "ES384":
fallthrough
case "ES512":
key, err = loadOrCreateAsymmetricKey()

default:
return ErrInvalidAlgorithmType{setting.OAuth2.JWTSigningAlgorithm}
}

if err != nil {
return err
log.Error("Error while loading or creating symmetric key: %v", err)
return
}

DefaultSigningKey = key
signingKey, err := CreateJWTSingingKey(setting.OAuth2.JWTSigningAlgorithm, key)
if err != nil {
return
}

DefaultSigningKey = signingKey

return
}

func loadOrCreateSymmetricKey() (interface{}, error) {
key := make([]byte, 32)
n, err := base64.RawURLEncoding.Decode(key, []byte(setting.OAuth2.JWTSecretBase64))
if err != nil || n != 32 {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently only 32 bytes are allowed. Should we really enforce this? Smaller keys are valid and larger keys are hashed by the internal hashing algorithm to create a secret with the correct length.
https://github.com/golang/go/blob/2ebe77a2fda1ee9ff6fd9a3e08933ad1ebaea039/src/crypto/hmac/hmac.go#L148-L152

key, err = generate.NewJwtSecret()
if err != nil {
log.Fatal("error generating JWT secret: %v", err)
return nil, err
}

setting.CreateOrAppendToCustomConf(func(cfg *ini.File) {
secretBase64 := base64.RawURLEncoding.EncodeToString(key)
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64)
})
}

return key, nil
}

func loadOrCreateAsymmetricKey() (interface{}, 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() (interface{}, error) {
if strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS") {
return rsa.GenerateKey(rand.Reader, 4096)
}
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, 0600)
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 := ioutil.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 nil
return x509.ParsePKCS8PrivateKey(block.Bytes)
}
19 changes: 14 additions & 5 deletions modules/generate/generate.go
Original file line number Diff line number Diff line change
@@ -38,14 +38,23 @@ func NewInternalToken() (string, error) {
return internalToken, nil
}

// NewJwtSecret generate a new value intended to be used by LFS_JWT_SECRET.
func NewJwtSecret() (string, error) {
JWTSecretBytes := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, JWTSecretBytes)
// NewJwtSecret generates a new value intended to be used for JWT secrets.
func NewJwtSecret() ([]byte, error) {
bytes := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, bytes)
if err != nil {
return nil, err
}
return bytes, nil
}

// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets.
func NewJwtSecretBase64() (string, error) {
bytes, err := NewJwtSecret()
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(JWTSecretBytes), nil
return base64.RawURLEncoding.EncodeToString(bytes), nil
}

// NewSecretKey generate a new value intended to be used by SECRET_KEY.
2 changes: 1 addition & 1 deletion modules/setting/lfs.go
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ func newLFSService() {
n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64))

if err != nil || n != 32 {
LFS.JWTSecretBase64, err = generate.NewJwtSecret()
LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64()
if err != nil {
log.Fatal("Error generating JWT Secret for custom config: %v", err)
return
39 changes: 6 additions & 33 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
@@ -361,14 +361,17 @@ var (
AccessTokenExpirationTime int64
RefreshTokenExpirationTime int64
InvalidateRefreshTokens bool
JWTSecretBytes []byte `ini:"-"`
JWTSecretBase64 string `ini:"JWT_SECRET"`
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
MaxTokenLength int
}{
Enable: true,
AccessTokenExpirationTime: 3600,
RefreshTokenExpirationTime: 730,
InvalidateRefreshTokens: false,
JWTSigningAlgorithm: "RS256",
JWTSigningPrivateKeyFile: "jwt/private.pem",
MaxTokenLength: math.MaxInt16,
}

@@ -783,38 +786,8 @@ func NewContext() {
return
}

if OAuth2.Enable {
OAuth2.JWTSecretBytes = make([]byte, 32)
n, err := base64.RawURLEncoding.Decode(OAuth2.JWTSecretBytes, []byte(OAuth2.JWTSecretBase64))

if err != nil || n != 32 {
OAuth2.JWTSecretBase64, err = generate.NewJwtSecret()
if err != nil {
log.Fatal("error generating JWT secret: %v", err)
return
}
cfg := ini.Empty()
isFile, err := util.IsFile(CustomConf)
if err != nil {
log.Error("Unable to check if %s is a file. Error: %v", CustomConf, err)
}
if isFile {
if err := cfg.Append(CustomConf); err != nil {
log.Error("failed to load custom conf %s: %v", CustomConf, err)
return
}
}
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)

if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil {
log.Fatal("failed to create '%s': %v", CustomConf, err)
return
}
if err := cfg.SaveTo(CustomConf); err != nil {
log.Fatal("error saving generating JWT secret to custom config: %v", err)
return
}
}
if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
}

sec = Cfg.Section("admin")
2 changes: 1 addition & 1 deletion routers/install.go
Original file line number Diff line number Diff line change
@@ -333,7 +333,7 @@ func InstallPost(ctx *context.Context) {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath)
var secretKey string
if secretKey, err = generate.NewJwtSecret(); err != nil {
if secretKey, err = generate.NewJwtSecretBase64(); err != nil {
ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
return
}