diff options
author | Gibheer <gibheer+git@zero-knowledge.org> | 2016-02-16 23:01:56 +0100 |
---|---|---|
committer | Gibheer <gibheer+git@zero-knowledge.org> | 2016-02-16 23:01:56 +0100 |
commit | 12403bdfb098d8118df734275c302c8c5de20ee4 (patch) | |
tree | b289b891ed4d1f56aa6ee98a875efb9c131ee14d /models/ssh_key.go | |
parent | 3af1d3c5810bd94b2f3f80831f2be3a97fc1ceb1 (diff) | |
download | gitea-12403bdfb098d8118df734275c302c8c5de20ee4.tar.gz gitea-12403bdfb098d8118df734275c302c8c5de20ee4.zip |
allow native and ssh-keygen public key check
This commit adds the possibibility to use either the native golang
libraries or ssh-keygen to check public keys. The check is adjusted
depending on the settings, so that only supported keys are let through.
This commit also brings back the blacklist feature, which was removed in
7ef9a055886574655d9f2be70c957bc16bf30500. This allows to blacklist
algorythms or keys based on the key length. This works with the native
and the ssh-keygen way.
Because of #2179 it also includes a way to adjust the path to
ssh-keygen and the working directory for ssh-keygen. With this,
sysadmins should be able to adjust the settings in a way, that SELinux
is okay with it. In the worst case, they can switch to the native
implementation and only loose support for ed25519 keys at the moment.
There are some other places which need adjustment to utilize the
parameters and the native implementation, but this sets the ground work.
Diffstat (limited to 'models/ssh_key.go')
-rw-r--r-- | models/ssh_key.go | 145 |
1 files changed, 133 insertions, 12 deletions
diff --git a/models/ssh_key.go b/models/ssh_key.go index 325a40a481..0e5226076b 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -12,9 +12,11 @@ import ( "fmt" "io" "io/ioutil" + "math/big" "os" "path" "path/filepath" + "strconv" "strings" "sync" "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" ) -var sshOpLocker = sync.Mutex{} +var ( + sshOpLocker = sync.Mutex{} + SSH_UNKNOWN_KEY_TYPE = fmt.Errorf("unknown key type") +) type KeyType int @@ -153,7 +158,110 @@ func parseKeyString(content string) (string, error) { 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, SSH_UNKNOWN_KEY_TYPE + } + 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, SSH_UNKNOWN_KEY_TYPE + } + 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. +// +// The function returns the actual public key line on success. func CheckPublicKeyString(content string) (_ string, err error) { content, err = parseKeyString(content) if err != nil { @@ -168,22 +276,34 @@ func CheckPublicKeyString(content string) (_ string, err error) { // remove any unnecessary whitespace now content = strings.TrimSpace(content) - fields := strings.Fields(content) - if len(fields) < 2 { - return "", errors.New("too less fields") + var ( + keyType string + 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 { + log.Trace("invalid public key of type '%s' with length %d: %s", keyType, length, err) return "", fmt.Errorf("ParsePublicKey: %v", err) } - log.Trace("Key type: %s", pkey.Type()) + log.Trace("Key type: %s", keyType) - return content, nil + if !setting.Service.EnableMinimumKeySizeCheck { + 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. @@ -247,7 +367,7 @@ func addKey(e Engine, key *PublicKey) (err error) { } stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath) 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 { 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. func AddPublicKey(ownerID int64, name, content string) (*PublicKey, error) { + log.Trace(content) if err := checkKeyContent(content); err != nil { return nil, err } |