This commit is contained in:
TheFox0x7 2025-04-12 21:44:21 -07:00 committed by GitHub
commit 4d8b4686bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 215 additions and 25 deletions

View File

@ -5,13 +5,17 @@
package cmd
import (
"bufio"
"encoding/pem"
"fmt"
"os"
"strings"
"code.gitea.io/gitea/modules/generate"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh"
)
var (
@ -21,6 +25,7 @@ var (
Usage: "Generate Gitea's secrets/keys/tokens",
Subcommands: []*cli.Command{
subcmdSecret,
subcmdKeygen,
},
}
@ -33,6 +38,17 @@ var (
microcmdGenerateSecretKey,
},
}
keygenFlags = []cli.Flag{
&cli.StringFlag{Name: "bits", Aliases: []string{"b"}, Usage: "Number of bits in the key, ignored when key is ed25519"},
&cli.StringFlag{Name: "type", Aliases: []string{"t"}, Value: "ed25519", Usage: "Keytype to generate"},
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Specifies the filename of the key file", Required: true},
}
subcmdKeygen = &cli.Command{
Name: "ssh-keygen",
Usage: "Generate a ssh keypair",
Flags: keygenFlags,
Action: runGenerateKeyPair,
}
microcmdGenerateInternalToken = &cli.Command{
Name: "INTERNAL_TOKEN",
@ -98,3 +114,49 @@ func runGenerateSecretKey(c *cli.Context) error {
return nil
}
func runGenerateKeyPair(c *cli.Context) error {
file := c.String("file")
// Check if file exists to prevent overwrites
if _, err := os.Stat(file); err == nil {
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("%s already exists.\nOverwrite (y/n)? ", file)
scanner.Scan()
if strings.ToLower(strings.TrimSpace(scanner.Text())) != "y" {
fmt.Println("Aborting")
return nil
}
}
keytype := c.String("type")
bits := c.Int("bits")
// provide defaults for bits, ed25519 ignores bit length so it's omitted
if bits == 0 {
if keytype == "rsa" {
bits = 3072
} else {
bits = 256
}
}
pub, priv, err := generate.NewSSHKey(keytype, bits)
if err != nil {
return err
}
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
}
defer f.Close()
err = pem.Encode(f, priv)
if err != nil {
return err
}
fmt.Printf("Your identification has been saved in %s\n", file)
err = os.WriteFile(file+".pub", ssh.MarshalAuthorizedKey(pub), 0o644)
if err != nil {
return err
}
fmt.Printf("Your public key has been saved in %s", file+".pub")
return nil
}

View File

@ -5,8 +5,14 @@
package generate
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"time"
@ -14,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/ssh"
)
// NewInternalToken generate a new value intended to be used by INTERNAL_TOKEN.
@ -72,3 +79,59 @@ func NewSecretKey() (string, error) {
return secretKey, nil
}
func NewSSHKey(keytype string, bits int) (ssh.PublicKey, *pem.Block, error) {
pub, priv, err := commonKeyGen(keytype, bits)
if err != nil {
return nil, nil, err
}
pemPriv, err := ssh.MarshalPrivateKey(priv, "")
if err != nil {
return nil, nil, err
}
sshPub, err := ssh.NewPublicKey(pub)
if err != nil {
return nil, nil, err
}
return sshPub, pemPriv, nil
}
// commonKeyGen is an abstraction over rsa, ecdsa and ed25519 generating functions
func commonKeyGen(keytype string, bits int) (publicKey, privateKey crypto.PublicKey, err error) {
switch keytype {
case "rsa":
privateKey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return &privateKey.PublicKey, privateKey, nil
case "ed25519":
return ed25519.GenerateKey(rand.Reader)
case "ecdsa":
curve, err := getElipticCurve(bits)
if err != nil {
return nil, nil, err
}
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, err
}
return &privateKey.PublicKey, privateKey, nil
default:
return nil, nil, fmt.Errorf("unknown keytype: %s", keytype)
}
}
func getElipticCurve(bits int) (elliptic.Curve, error) {
switch bits {
case 256:
return elliptic.P256(), nil
case 384:
return elliptic.P384(), nil
case 521:
return elliptic.P521(), nil
default:
return nil, fmt.Errorf("unsupported ECDSA curve bit length: %d", bits)
}
}

View File

@ -56,7 +56,7 @@ var SSH = struct {
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
MinimumKeySizeCheck: true,
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071},
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gitea.ed25519", "ssh/gitea.ecdsa", "ssh/gogs.rsa"},
AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}",
PerWriteTimeout: PerWriteTimeout,
PerWritePerKbTimeout: PerWritePerKbTimeout,

View File

@ -6,9 +6,6 @@ package ssh
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"io"
@ -23,6 +20,7 @@ import (
"syscall"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
@ -54,6 +52,14 @@ import (
const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id"
type KeyType string
const (
RSA KeyType = "rsa"
ECDSA KeyType = "ecdsa"
ED25519 KeyType = "ed25519"
)
func getExitStatusFromError(err error) int {
if err == nil {
return 0
@ -366,18 +372,19 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) {
}
if len(keys) == 0 {
filePath := filepath.Dir(setting.SSH.ServerHostKeys[0])
if err := os.MkdirAll(filePath, os.ModePerm); err != nil {
log.Error("Failed to create dir %s: %v", filePath, err)
for i := range 3 {
filename := setting.SSH.ServerHostKeys[i]
filePath := filepath.Dir(filename)
if err := os.MkdirAll(filePath, os.ModePerm); err != nil {
log.Error("Failed to create dir %s: %v", filePath, err)
}
err := GenKeyPair(filename)
if err != nil {
log.Fatal("Failed to generate private key: %v", err)
}
log.Trace("New private key is generated: %s", filename)
keys = append(keys, filename)
}
err := GenKeyPair(setting.SSH.ServerHostKeys[0])
if err != nil {
log.Fatal("Failed to generate private key: %v", err)
}
log.Trace("New private key is generated: %s", setting.SSH.ServerHostKeys[0])
keys = append(keys, setting.SSH.ServerHostKeys[0])
}
for _, key := range keys {
@ -387,7 +394,6 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) {
log.Error("Failed to set Host Key. %s", err)
}
}
go func() {
_, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Service: Built-in SSH server", process.SystemProcessType, true)
defer finished()
@ -399,12 +405,21 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) {
// Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file.
// Private Key generated is PEM encoded
func GenKeyPair(keyPath string) error {
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
bits := 4096
keytype := filepath.Ext(keyPath)
if keytype == ".ed25519" {
keytype = "ed25519"
} else if keytype == ".ecdsa" {
bits = 256
keytype = "ecdsa"
} else {
keytype = "rsa"
}
publicKey, privateKeyPEM, err := generate.NewSSHKey(keytype, bits)
if err != nil {
return err
}
privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err
@ -419,13 +434,7 @@ func GenKeyPair(keyPath string) error {
return err
}
// generate public key
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return err
}
public := gossh.MarshalAuthorizedKey(pub)
public := gossh.MarshalAuthorizedKey(publicKey)
p, err := os.OpenFile(keyPath+".pub", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
return err

56
modules/ssh/ssh_test.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package ssh_test
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"io"
"os"
"path/filepath"
"testing"
"code.gitea.io/gitea/modules/ssh"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
)
func TestGenKeyPair(t *testing.T) {
testCases := []struct {
keyPath string
expectedType any
}{
{
keyPath: "/gitea.rsa",
expectedType: &rsa.PrivateKey{},
},
{
keyPath: "/gitea.ed25519",
expectedType: &ed25519.PrivateKey{},
},
{
keyPath: "/gitea.ecdsa",
expectedType: &ecdsa.PrivateKey{},
},
}
for _, tC := range testCases {
t.Run("Generate "+filepath.Ext(tC.keyPath), func(t *testing.T) {
path := t.TempDir() + tC.keyPath
require.NoError(t, ssh.GenKeyPair(path))
file, err := os.Open(path)
require.NoError(t, err)
bytes, err := io.ReadAll(file)
require.NoError(t, err)
privateKey, err := gossh.ParseRawPrivateKey(bytes)
require.NoError(t, err)
assert.IsType(t, tC.expectedType, privateKey)
})
}
}