Merge pull request #2637 from Gibheer/ssh-publickeys

allow native and ssh-keygen public key check
This commit is contained in:
Unknwon 2016-02-27 18:55:14 -05:00
commit 83c74878df
4 changed files with 230 additions and 12 deletions

View File

@ -71,6 +71,13 @@ SSH_PORT = 22
SSH_LISTEN_PORT = %(SSH_PORT)s SSH_LISTEN_PORT = %(SSH_PORT)s
; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'. ; Root path of SSH directory, default is '~/.ssh', but you have to use '/home/git/.ssh'.
SSH_ROOT_PATH = SSH_ROOT_PATH =
; override engine choice to check public keys (default: 'ssh-keygen' when
; DISABLE_SSH is set to false else 'native')
SSH_PUBLICKEY_CHECK =
; directory to create temporary files when using ssh-keygen (default: /tmp)
SSH_WORK_PATH =
; path to ssh-keygen (default: result of `which ssh-keygen`)
SSH_KEYGEN_PATH =
; Disable CDN even in "prod" mode ; Disable CDN even in "prod" mode
OFFLINE_MODE = false OFFLINE_MODE = false
DISABLE_ROUTER_LOG = false DISABLE_ROUTER_LOG = false
@ -132,6 +139,15 @@ ENABLE_REVERSE_PROXY_AUTHENTICATION = false
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
; Enable captcha validation for registration ; Enable captcha validation for registration
ENABLE_CAPTCHA = true ENABLE_CAPTCHA = true
; Do not check minimum key size with corresponding type
ENABLE_MINIMUM_KEY_SIZE_CHECK = false
; define allowed algorithms and their minimum key length (use -1 to disable a type)
[service.minimum_key_sizes]
ED25519 = 256
ECDSA = 256
RSA = 2048
DSA = 1024
[webhook] [webhook]
; Hook task queue length ; Hook task queue length

View File

@ -12,9 +12,11 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math/big"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -33,7 +35,10 @@ const (
_TPL_PUBLICK_KEY = `command="%s serv key-%d --config='%s'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n" _TPL_PUBLICK_KEY = `command="%s serv key-%d --config='%s'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
) )
var sshOpLocker = sync.Mutex{} var (
sshOpLocker = sync.Mutex{}
SSHUnknownKeyType = fmt.Errorf("unknown key type")
)
type KeyType int type KeyType int
@ -153,7 +158,110 @@ func parseKeyString(content string) (string, error) {
return keyType + " " + keyContent + " " + keyComment, nil return keyType + " " + keyContent + " " + keyComment, nil
} }
// extract key type and length using ssh-keygen
func SSHKeyGenParsePublicKey(key string) (string, int, error) {
// The ssh-keygen in Windows does not print key type, so no need go further.
if setting.IsWindows {
return "", 0, nil
}
tmpFile, err := ioutil.TempFile(setting.SSHWorkPath, "gogs_keytest")
if err != nil {
return "", 0, err
}
tmpName := tmpFile.Name()
defer os.Remove(tmpName)
if ln, err := tmpFile.WriteString(key); err != nil {
tmpFile.Close()
return "", 0, err
} else if ln != len(key) {
tmpFile.Close()
return "", 0, fmt.Errorf("could not write complete public key (written: %d, should be: %d): %s", ln, len(key), key)
}
tmpFile.Close()
stdout, stderr, err := process.Exec("CheckPublicKeyString", setting.SSHKeyGenPath, "-lf", tmpName)
if err != nil {
return "", 0, fmt.Errorf("public key check failed with error '%s': %s", err, stderr)
}
if strings.HasSuffix(stdout, "is not a public key file.") {
return "", 0, SSHUnknownKeyType
}
fields := strings.Split(stdout, " ")
if len(fields) < 4 {
return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
}
length, err := strconv.Atoi(fields[0])
if err != nil {
return "", 0, err
}
keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
return strings.ToLower(keyType), length, nil
}
// extract the key type and length using the golang ssh library
func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
fields := strings.Fields(keyLine)
if len(fields) < 2 {
return "", 0, fmt.Errorf("not enough fields in public key line: %s", string(keyLine))
}
raw, err := base64.StdEncoding.DecodeString(fields[1])
if err != nil {
return "", 0, err
}
pkey, err := ssh.ParsePublicKey(raw)
if err != nil {
if strings.HasPrefix(err.Error(), "ssh: unknown key algorithm") {
return "", 0, SSHUnknownKeyType
}
return "", 0, err
}
// The ssh library can parse the key, so next we find out what key exactly we
// have.
switch pkey.Type() {
case ssh.KeyAlgoDSA:
rawPub := struct {
Name string
P, Q, G, Y *big.Int
}{}
if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
return "", 0, err
}
// as per https://bugzilla.mindrot.org/show_bug.cgi?id=1647 we should never
// see dsa keys != 1024 bit, but as it seems to work, we will not check here
return "dsa", rawPub.P.BitLen(), nil // use P as per crypto/dsa/dsa.go (is L)
case ssh.KeyAlgoRSA:
rawPub := struct {
Name string
E *big.Int
N *big.Int
}{}
if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
return "", 0, err
}
return "rsa", rawPub.N.BitLen(), nil // use N as per crypto/rsa/rsa.go (is bits)
case ssh.KeyAlgoECDSA256:
return "ecdsa", 256, nil
case ssh.KeyAlgoECDSA384:
return "ecdsa", 384, nil
case ssh.KeyAlgoECDSA521:
return "ecdsa", 521, nil
case "ssh-ed25519": // TODO replace with ssh constant when available
return "ed25519", 256, nil
default:
return "", 0, fmt.Errorf("no support for key length detection for type %s", pkey.Type())
}
return "", 0, fmt.Errorf("SSHNativeParsePublicKey failed horribly, please investigate why")
}
// CheckPublicKeyString checks if the given public key string is recognized by SSH. // CheckPublicKeyString checks if the given public key string is recognized by SSH.
//
// The function returns the actual public key line on success.
func CheckPublicKeyString(content string) (_ string, err error) { func CheckPublicKeyString(content string) (_ string, err error) {
content, err = parseKeyString(content) content, err = parseKeyString(content)
if err != nil { if err != nil {
@ -168,23 +276,35 @@ func CheckPublicKeyString(content string) (_ string, err error) {
// remove any unnecessary whitespace now // remove any unnecessary whitespace now
content = strings.TrimSpace(content) content = strings.TrimSpace(content)
fields := strings.Fields(content) var (
if len(fields) < 2 { keyType string
return "", errors.New("too less fields") length int
)
if setting.SSHPublicKeyCheck == setting.SSH_PUBLICKEY_CHECK_NATIVE {
keyType, length, err = SSHNativeParsePublicKey(content)
} else if setting.SSHPublicKeyCheck == setting.SSH_PUBLICKEY_CHECK_KEYGEN {
keyType, length, err = SSHKeyGenParsePublicKey(content)
} else {
log.Error(4, "invalid public key check type: %s", setting.SSHPublicKeyCheck)
return "", fmt.Errorf("invalid public key check type")
} }
key, err := base64.StdEncoding.DecodeString(fields[1])
if err != nil {
return "", fmt.Errorf("StdEncoding.DecodeString: %v", err)
}
pkey, err := ssh.ParsePublicKey([]byte(key))
if err != nil { if err != nil {
log.Trace("invalid public key of type '%s' with length %d: %s", keyType, length, err)
return "", fmt.Errorf("ParsePublicKey: %v", err) return "", fmt.Errorf("ParsePublicKey: %v", err)
} }
log.Trace("Key type: %s", pkey.Type()) log.Trace("Key type: %s", keyType)
if !setting.Service.EnableMinimumKeySizeCheck {
return content, nil return content, nil
} }
if minLen, found := setting.Service.MinimumKeySizes[keyType]; found && length >= minLen {
return content, nil
} else if found && length < minLen {
return "", fmt.Errorf("key not large enough - got %d, needs %d", length, minLen)
}
return "", fmt.Errorf("key type '%s' is not allowed", keyType)
}
// saveAuthorizedKeyFile writes SSH key content to authorized_keys file. // saveAuthorizedKeyFile writes SSH key content to authorized_keys file.
func saveAuthorizedKeyFile(keys ...*PublicKey) error { func saveAuthorizedKeyFile(keys ...*PublicKey) error {
@ -247,7 +367,7 @@ func addKey(e Engine, key *PublicKey) (err error) {
} }
stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath) stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
if err != nil { if err != nil {
return errors.New("ssh-keygen -lf: " + stderr) return fmt.Errorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr)
} else if len(stdout) < 2 { } else if len(stdout) < 2 {
return errors.New("not enough output for calculating fingerprint: " + stdout) return errors.New("not enough output for calculating fingerprint: " + stdout)
} }
@ -267,6 +387,7 @@ func addKey(e Engine, key *PublicKey) (err error) {
// AddPublicKey adds new public key to database and authorized_keys file. // AddPublicKey adds new public key to database and authorized_keys file.
func AddPublicKey(ownerID int64, name, content string) (*PublicKey, error) { func AddPublicKey(ownerID int64, name, content string) (*PublicKey, error) {
log.Trace(content)
if err := checkKeyContent(content); err != nil { if err := checkKeyContent(content); err != nil {
return nil, err return nil, err
} }

39
models/ssh_key_test.go Normal file
View File

@ -0,0 +1,39 @@
package models
import (
"github.com/gogits/gogs/modules/setting"
"testing"
)
func TestSSHKeyVerification(t *testing.T) {
setting.SSHWorkPath = "/tmp"
setting.SSHKeyGenPath = "/usr/bin/ssh-keygen"
keys := map[string]string{
"dsa-1024": string("ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment"),
"rsa-1024": string("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n"),
"rsa-2048": string("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMZXh+1OBUwSH9D45wTaxErQIN9IoC9xl7MKJkqvTvv6O5RR9YW/IK9FbfjXgXsppYGhsCZo1hFOOsXHMnfOORqu/xMDx4yPuyvKpw4LePEcg4TDipaDFuxbWOqc/BUZRZcXu41QAWfDLrInwsltWZHSeG7hjhpacl4FrVv9V1pS6Oc5Q1NxxEzTzuNLS/8diZrTm/YAQQ/+B+mzWI3zEtF4miZjjAljWd1LTBPvU23d29DcBmmFahcZ441XZsTeAwGxG/Q6j8NgNXj9WxMeWwxXV2jeAX/EBSpZrCVlCQ1yJswT6xCp8TuBnTiGWYMBNTbOZvPC4e0WI2/yZW/s5F nocomment"),
"ecdsa-256": string("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFQacN3PrOll7PXmN5B/ZNVahiUIqI05nbBlZk1KXsO3d06ktAWqbNflv2vEmA38bTFTfJ2sbn2B5ksT52cDDbA= nocomment"),
"ecdsa-384": string("ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBINmioV+XRX1Fm9Qk2ehHXJ2tfVxW30ypUWZw670Zyq5GQfBAH6xjygRsJ5wWsHXBsGYgFUXIHvMKVAG1tpw7s6ax9oA+dJOJ7tj+vhn8joFqT+sg3LYHgZkHrfqryRasQ== nocomment"),
"ecdsa-512": string("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACGt3UG3EzRwNOI17QR84l6PgiAcvCE7v6aXPj/SC6UWKg4EL8vW9ZBcdYL9wzs4FZXh4MOV8jAzu3KRWNTwb4k2wFNUpGOt7l28MztFFEtH5BDDrtAJSPENPy8pvPLMfnPg5NhvWycqIBzNcHipem5wSJFN5PdpNOC2xMrPWKNqj+ZjQ== nocomment"),
}
for name, pubkey := range keys {
keyTypeN, lengthN, errN := SSHNativeParsePublicKey(pubkey)
if errN != nil {
if errN != SSHUnknownKeyType {
t.Errorf("error parsing public key '%s': %s", name, errN)
continue
}
}
keyTypeK, lengthK, errK := SSHKeyGenParsePublicKey(pubkey)
if errK != nil {
t.Errorf("error parsing public key '%s': %s", name, errK)
continue
}
// we know that ed25519 is currently not supported by native and returns SSHUnknownKeyType
if (keyTypeN != keyTypeK || lengthN != lengthK) && errN != SSHUnknownKeyType {
t.Errorf("key mismatch for '%s': native: %s(%d), ssh-keygen: %s(%d)", name, keyTypeN, lengthN, keyTypeK, lengthK)
}
}
}

View File

@ -27,6 +27,11 @@ import (
"github.com/gogits/gogs/modules/user" "github.com/gogits/gogs/modules/user"
) )
const (
SSH_PUBLICKEY_CHECK_NATIVE = "native"
SSH_PUBLICKEY_CHECK_KEYGEN = "ssh-keygen"
)
type Scheme string type Scheme string
const ( const (
@ -67,6 +72,9 @@ var (
SSHPort int SSHPort int
SSHListenPort int SSHListenPort int
SSHRootPath string SSHRootPath string
SSHPublicKeyCheck string
SSHWorkPath string
SSHKeyGenPath string
OfflineMode bool OfflineMode bool
DisableRouterLog bool DisableRouterLog bool
CertFile, KeyFile string CertFile, KeyFile string
@ -330,6 +338,29 @@ func NewContext() {
if err := os.MkdirAll(SSHRootPath, 0700); err != nil { if err := os.MkdirAll(SSHRootPath, 0700); err != nil {
log.Fatal(4, "Fail to create '%s': %v", SSHRootPath, err) log.Fatal(4, "Fail to create '%s': %v", SSHRootPath, err)
} }
checkDefault := SSH_PUBLICKEY_CHECK_KEYGEN
if StartSSHServer {
checkDefault = SSH_PUBLICKEY_CHECK_NATIVE
}
SSHPublicKeyCheck = sec.Key("SSH_PUBLICKEY_CHECK").MustString(checkDefault)
if SSHPublicKeyCheck != SSH_PUBLICKEY_CHECK_NATIVE &&
SSHPublicKeyCheck != SSH_PUBLICKEY_CHECK_KEYGEN {
log.Fatal(4, "SSH_PUBLICKEY_CHECK must be ssh-keygen or native")
}
SSHWorkPath = sec.Key("SSH_WORK_PATH").MustString(os.TempDir())
if !DisableSSH && (!StartSSHServer || SSHPublicKeyCheck == SSH_PUBLICKEY_CHECK_KEYGEN) {
if tmpDirStat, err := os.Stat(SSHWorkPath); err != nil || !tmpDirStat.IsDir() {
log.Fatal(4, "directory '%s' set in SSHWorkPath is not a directory: %s", SSHWorkPath, err)
}
}
SSHKeyGenPath = sec.Key("SSH_KEYGEN_PATH").MustString("")
if !DisableSSH && !StartSSHServer &&
SSHKeyGenPath == "" && SSHPublicKeyCheck == SSH_PUBLICKEY_CHECK_KEYGEN {
SSHKeyGenPath, err = exec.LookPath("ssh-keygen")
if err != nil {
log.Fatal(4, "could not find ssh-keygen, maybe set DISABLE_SSH to use the internal ssh server")
}
}
OfflineMode = sec.Key("OFFLINE_MODE").MustBool() OfflineMode = sec.Key("OFFLINE_MODE").MustBool()
DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool() DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool()
StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(workDir) StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(workDir)
@ -461,6 +492,8 @@ var Service struct {
EnableReverseProxyAuth bool EnableReverseProxyAuth bool
EnableReverseProxyAutoRegister bool EnableReverseProxyAutoRegister bool
EnableCaptcha bool EnableCaptcha bool
EnableMinimumKeySizeCheck bool
MinimumKeySizes map[string]int
} }
func newService() { func newService() {
@ -473,6 +506,15 @@ func newService() {
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool() Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool()
Service.EnableMinimumKeySizeCheck = sec.Key("ENABLE_MINIMUM_KEY_SIZE_CHECK").MustBool()
Service.MinimumKeySizes = map[string]int{}
minimumKeySizes := Cfg.Section("service.minimum_key_sizes").Keys()
for _, key := range minimumKeySizes {
if key.MustInt() != -1 {
Service.MinimumKeySizes[strings.ToLower(key.Name())] = key.MustInt()
}
}
} }
var logLevels = map[string]string{ var logLevels = map[string]string{