Improve public key handling

This commit is contained in:
kayos@tcp.direct 2023-01-08 13:18:02 -08:00
parent 570f7147c6
commit 271496a91d
Signed by: kayos
GPG Key ID: 4B841471B4BEE979
3 changed files with 128 additions and 41 deletions

View File

@ -1,12 +1,14 @@
package data package data
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"sync" "sync"
"git.tcp.direct/kayos/common/entropy" "git.tcp.direct/kayos/common/entropy"
"git.tcp.direct/kayos/common/squish"
"github.com/davecgh/go-spew/spew"
"github.com/gliderlabs/ssh"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -31,9 +33,13 @@ func AuthMethodFromMap(m map[string]string) AuthMethod {
Password: m["password"], Password: m["password"],
} }
case "publickey": case "publickey":
pkParsed, err := ssh.ParsePublicKey(squish.B64d(m["pubkey"]))
if err != nil {
return nil
}
return &PubKey{ return &PubKey{
Username: m["pub_username"], Username: m["pub_username"],
Pub: []byte(m["pubkey"]), Pub: pkParsed,
} }
} }
return nil return nil
@ -107,8 +113,15 @@ func (up *UserPass) Authenticate() error {
} }
type PubKey struct { type PubKey struct {
Username string `json:"pub_username"` Username string `json:"pub_username"`
Pub []byte `json:"pubkey"` Pub ssh.PublicKey `json:"pubkey"`
}
func NewPubKey(username string, pubkey ssh.PublicKey) *PubKey {
return &PubKey{
Username: username,
Pub: pubkey,
}
} }
func (pk *PubKey) Name() string { func (pk *PubKey) Name() string {
@ -119,7 +132,7 @@ func (pk *PubKey) Map() map[string]string {
return map[string]string{ return map[string]string{
"type": "publickey", "type": "publickey",
"pub_username": pk.Username, "pub_username": pk.Username,
"pubkey": string(pk.Pub), "pubkey": squish.B64e(pk.Pub.Marshal()),
} }
} }
@ -137,7 +150,22 @@ func (pk *PubKey) Authenticate() error {
for _, method := range user.AuthMethods { for _, method := range user.AuthMethods {
switch method["type"] { switch method["type"] {
case "publickey": case "publickey":
if method["pub_username"] == pk.Username && bytes.Equal([]byte(method["pubkey"]), pk.Pub) { if method["pub_username"] != pk.Username {
log.Warn().Str("username", pk.Username).Msg("username mismatch")
continue
}
pkdat, ok := method["pubkey"]
if !ok {
log.Warn().Str("username", pk.Username).Msg("pubkey not found")
continue
}
pubkeyParsed, err := ssh.ParsePublicKey(squish.B64d(pkdat))
if err != nil {
log.Warn().Err(err).Str("username", pk.Username).Msg("error parsing public key")
spew.Dump(method)
continue
}
if ssh.KeysEqual(pubkeyParsed, pk.Pub) {
return nil return nil
} }
default: default:
@ -187,7 +215,7 @@ func NewUser(username string, authMethods ...AuthMethod) (*User, error) {
if len(usableMethod.Username) == 0 { if len(usableMethod.Username) == 0 {
return nil, errors.New("username cannot be empty") return nil, errors.New("username cannot be empty")
} }
if len(usableMethod.Pub) == 0 { if len(usableMethod.Pub.Marshal()) == 0 {
return nil, errors.New("public key cannot be empty") return nil, errors.New("public key cannot be empty")
} }
methods = append(methods, method.Map()) methods = append(methods, method.Map())
@ -226,16 +254,19 @@ func DelUser(username string) error {
return db.With("users").Delete([]byte(username)) return db.With("users").Delete([]byte(username))
} }
func (user *User) DelPubKey(pubkey []byte) (*User, error) { func (user *User) DelPubKey(pubkey ssh.PublicKey) (*User, error) {
user.Lock() user.Lock()
defer user.Unlock() defer user.Unlock()
var found = false var found = false
var methods []map[string]string var methods []map[string]string
for _, method := range user.AuthMethods { for _, method := range user.AuthMethods {
m := AuthMethodFromMap(method) m := AuthMethodFromMap(method)
if m == nil {
continue
}
if m.Name() == "publickey" { if m.Name() == "publickey" {
pubKey := m.(*PubKey) pubKey := m.(*PubKey)
if bytes.Equal(pubKey.Pub, pubkey) { if ssh.KeysEqual(pubKey.Pub, pubkey) {
found = true found = true
continue continue
} }
@ -260,6 +291,9 @@ func (user *User) ChangePassword(newPassword string) (*User, error) {
var methods []map[string]string var methods []map[string]string
for _, method := range user.AuthMethods { for _, method := range user.AuthMethods {
m := AuthMethodFromMap(method) m := AuthMethodFromMap(method)
if m == nil {
continue
}
if m.Name() == "password" { if m.Name() == "password" {
ponce.Do(func() { ponce.Do(func() {
hashed, err := HashPassword(newPassword) hashed, err := HashPassword(newPassword)

View File

@ -3,8 +3,33 @@ package data
import ( import (
"os" "os"
"testing" "testing"
"golang.org/x/crypto/ssh"
) )
var (
testPublicKey1 ssh.PublicKey
testPublicKey2 ssh.PublicKey
testPublicKey3 ssh.PublicKey
)
func init() {
var err error
// generate public keys for testing
testPublicKey1, _, _, _, err = ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6EFqmelEJ6MELBPHUEFTGmlJBfhS7Jeq5B5BCrFSun"))
if err != nil {
panic(err)
}
testPublicKey2, _, _, _, err = ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH+ZTIMTWwYWHUEJlHfhT7dcYhgETGWgwEpDLdURaTPb"))
if err != nil {
panic(err)
}
testPublicKey3, _, _, _, err = ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHUEFpqqYCfBkVLRwgYlGbZyzgnEcMLpT0o97JUHNpIt"))
if err != nil {
panic(err)
}
}
func TestUsers(t *testing.T) { func TestUsers(t *testing.T) {
testMode() testMode()
Start() Start()
@ -48,19 +73,33 @@ func TestUsers(t *testing.T) {
if user == nil { if user == nil {
t.Fatal("expected user to not be nil") t.Fatal("expected user to not be nil")
} }
if user, err = user.AddAuthMethod(&PubKey{Username: "test2", Pub: []byte("pub")}); err != nil { if user, err = user.AddAuthMethod(NewPubKey(user.Username, testPublicKey1)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(user.AuthMethods) != 2 { if len(user.AuthMethods) != 2 {
t.Fatalf("expected 2 auth methods, got %d", len(user.AuthMethods)) t.Fatalf("expected 2 auth methods, got %d", len(user.AuthMethods))
} }
pk := &PubKey{Username: "test2", Pub: []byte("pub")} pk := NewPubKey("test2", testPublicKey1)
if err = pk.Authenticate(); err != nil { if err = pk.Authenticate(); err != nil {
t.Fatal("expected pub key to authenticate") t.Fatal("expected pub key 1 to authenticate")
} }
if user, err = user.AddAuthMethod(&PubKey{Username: "test2", Pub: []byte("pub2")}); err != nil { pk = NewPubKey("test2", testPublicKey2)
if err = pk.Authenticate(); err == nil {
t.Fatal("expected pub key 2 to not authenticate")
}
if user, err = user.AddAuthMethod(NewPubKey(user.Username, testPublicKey2)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
pk = NewPubKey("test2", testPublicKey1)
if err = pk.Authenticate(); err != nil {
t.Fatal("expected pub key 1 to authenticate")
}
pk = NewPubKey("test2", testPublicKey2)
if err = pk.Authenticate(); err != nil {
t.Fatal("expected pub key 2 to authenticate")
}
if len(user.AuthMethods) != 3 { if len(user.AuthMethods) != 3 {
t.Fatalf("expected 2 auth methods, got %d", len(user.AuthMethods)) t.Fatalf("expected 2 auth methods, got %d", len(user.AuthMethods))
} }
@ -70,36 +109,33 @@ func TestUsers(t *testing.T) {
if user.AuthMethods[1]["type"] != "publickey" { if user.AuthMethods[1]["type"] != "publickey" {
t.Fatalf("expected auth method to be 'publickey', got '%s'", user.AuthMethods[1]["type"]) t.Fatalf("expected auth method to be 'publickey', got '%s'", user.AuthMethods[1]["type"])
} }
auth := &PubKey{
Username: "test2",
Pub: []byte("pub"),
}
if err = auth.Authenticate(); err != nil {
t.Fatalf("expected auth to succeed, got: %v", err)
}
auth.Pub = []byte("asdjfas")
if err = auth.Authenticate(); err == nil {
t.Fatal("expected auth to fail")
}
}) })
t.Run("DelPubKey", func(t *testing.T) { t.Run("DelPubKey", func(t *testing.T) {
user, err := GetUser("test2") user, err := GetUser("test2")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if user, err = user.DelPubKey([]byte("fdsafdas")); err == nil { if user, err = user.DelPubKey(testPublicKey3); err == nil {
t.Fatal("expected error deleting non-existent key") t.Fatal("expected error deleting non-existent key")
} }
if user == nil { if user == nil {
t.Fatal("expected user to not be nil") t.Fatal("expected user to not be nil")
} }
if user, err = user.DelPubKey([]byte("pub2")); err != nil { if user, err = user.DelPubKey(testPublicKey2); err != nil {
t.Fatal(err) t.Fatal(err)
} }
auth := NewUserPass(false, "test2", "test2") auth := NewUserPass(false, "test2", "test2")
if err = auth.Authenticate(); err != nil { if err = auth.Authenticate(); err != nil {
t.Fatalf("expected userpass to still be there after deleting public key, got: %v", err) t.Fatalf("expected userpass to still be there after deleting public key, got: %v", err)
} }
pk := &PubKey{"test2", testPublicKey2}
if err = pk.Authenticate(); err == nil {
t.Fatal("expected public key 2 to be deleted")
}
pk = &PubKey{"test2", testPublicKey1}
if err = pk.Authenticate(); err != nil {
t.Fatal("expected public key 1 to not be deleted")
}
}) })
t.Run("ChangePassword", func(t *testing.T) { t.Run("ChangePassword", func(t *testing.T) {
user, err := GetUser("test2") user, err := GetUser("test2")

View File

@ -3,6 +3,7 @@ package sshui
import ( import (
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -12,33 +13,49 @@ import (
"git.tcp.direct/kayos/ziggs/internal/data" "git.tcp.direct/kayos/ziggs/internal/data"
) )
func newHostKey() error {
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err
}
if err = privateKey.Validate(); err != nil {
return err
}
dir, _ := filepath.Split(config.Filename)
newFile := filepath.Join(dir, "host_rsa")
if err = os.WriteFile(newFile, encodePrivateKeyToPEM(privateKey), 0600); err != nil {
return err
}
config.Snek.Set("ssh.host_key", newFile)
config.SSHHostKey = newFile
if err = config.Snek.WriteConfig(); err != nil {
return fmt.Errorf("viper config save error: %v", err)
}
return nil
}
func ServeSSH() error { func ServeSSH() error {
var opts []ssh.Option var opts []ssh.Option
switch config.SSHHostKey { if config.SSHHostKey == "" {
case "": if err := newHostKey(); err != nil {
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return err return err
} }
if err = privateKey.Validate(); err != nil {
return err
}
dir, _ := filepath.Split(config.Filename)
newFile := filepath.Join(dir, "host_rsa")
if err = os.WriteFile(newFile, encodePrivateKeyToPEM(privateKey), 0600); err != nil {
return err
}
config.Snek.Set("ssh.host_key", newFile)
default:
opts = append(opts, ssh.HostKeyFile(config.SSHHostKey))
} }
opts = append(opts, ssh.HostKeyFile(config.SSHHostKey))
opts = append(opts, ssh.PasswordAuth(func(ctx ssh.Context, password string) bool { opts = append(opts, ssh.PasswordAuth(func(ctx ssh.Context, password string) bool {
attempt := data.NewUserPass(false, ctx.User(), password) attempt := data.NewUserPass(false, ctx.User(), password)
err := attempt.Authenticate() err := attempt.Authenticate()
return err == nil return err == nil
})) }))
opts = append(opts, ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
attempt := data.NewPubKey(ctx.User(), key)
err := attempt.Authenticate()
return err == nil
}))
return ssh.ListenAndServe(config.SSHListen, nil, opts...) return ssh.ListenAndServe(config.SSHListen, nil, opts...)
} }