mirror of
https://github.com/shazow/ssh-chat.git
synced 2025-04-15 00:20:37 +03:00
Use authorized_keys-style public keys rather than fingerprints.
Tests for whitelisting.
This commit is contained in:
parent
7a39898b36
commit
d8d5deac1c
61
auth.go
61
auth.go
@ -5,24 +5,39 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/shazow/ssh-chat/sshd"
|
"github.com/shazow/ssh-chat/sshd"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The error returned a key is checked that is not whitelisted, with whitelisting required.
|
||||||
|
var ErrNotWhitelisted = errors.New("not whitelisted")
|
||||||
|
|
||||||
|
// The error returned a key is checked that is banned.
|
||||||
|
var ErrBanned = errors.New("banned")
|
||||||
|
|
||||||
|
// AuthKey is the type that our lookups are keyed against.
|
||||||
|
type AuthKey string
|
||||||
|
|
||||||
|
// NewAuthKey returns an AuthKey from an ssh.PublicKey.
|
||||||
|
func NewAuthKey(key ssh.PublicKey) AuthKey {
|
||||||
|
// FIXME: Is there a way to index pubkeys without marshal'ing them into strings?
|
||||||
|
return AuthKey(string(key.Marshal()))
|
||||||
|
}
|
||||||
|
|
||||||
// Auth stores fingerprint lookups
|
// Auth stores fingerprint lookups
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
whitelist map[string]struct{}
|
|
||||||
banned map[string]struct{}
|
|
||||||
ops map[string]struct{}
|
|
||||||
|
|
||||||
sshd.Auth
|
sshd.Auth
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
whitelist map[AuthKey]struct{}
|
||||||
|
banned map[AuthKey]struct{}
|
||||||
|
ops map[AuthKey]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuth creates a new default Auth.
|
// NewAuth creates a new default Auth.
|
||||||
func NewAuth() Auth {
|
func NewAuth() *Auth {
|
||||||
return Auth{
|
return &Auth{
|
||||||
whitelist: make(map[string]struct{}),
|
whitelist: make(map[AuthKey]struct{}),
|
||||||
banned: make(map[string]struct{}),
|
banned: make(map[AuthKey]struct{}),
|
||||||
ops: make(map[string]struct{}),
|
ops: make(map[AuthKey]struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,43 +50,49 @@ func (a Auth) AllowAnonymous() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check determines if a pubkey fingerprint is permitted.
|
// Check determines if a pubkey fingerprint is permitted.
|
||||||
func (a Auth) Check(fingerprint string) (bool, error) {
|
func (a Auth) Check(key ssh.PublicKey) (bool, error) {
|
||||||
|
authkey := NewAuthKey(key)
|
||||||
|
|
||||||
a.RLock()
|
a.RLock()
|
||||||
defer a.RUnlock()
|
defer a.RUnlock()
|
||||||
|
|
||||||
if len(a.whitelist) > 0 {
|
if len(a.whitelist) > 0 {
|
||||||
// Only check whitelist if there is something in it, otherwise it's disabled.
|
// Only check whitelist if there is something in it, otherwise it's disabled.
|
||||||
_, whitelisted := a.whitelist[fingerprint]
|
|
||||||
|
_, whitelisted := a.whitelist[authkey]
|
||||||
if !whitelisted {
|
if !whitelisted {
|
||||||
return false, errors.New("not whitelisted")
|
return false, ErrNotWhitelisted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, banned := a.banned[fingerprint]
|
_, banned := a.banned[authkey]
|
||||||
if banned {
|
if banned {
|
||||||
return false, errors.New("banned")
|
return false, ErrBanned
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Op will set a fingerprint as a known operator.
|
// Op will set a fingerprint as a known operator.
|
||||||
func (a *Auth) Op(fingerprint string) {
|
func (a *Auth) Op(key ssh.PublicKey) {
|
||||||
|
authkey := NewAuthKey(key)
|
||||||
a.Lock()
|
a.Lock()
|
||||||
a.ops[fingerprint] = struct{}{}
|
a.ops[authkey] = struct{}{}
|
||||||
a.Unlock()
|
a.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whitelist will set a fingerprint as a whitelisted user.
|
// Whitelist will set a fingerprint as a whitelisted user.
|
||||||
func (a *Auth) Whitelist(fingerprint string) {
|
func (a *Auth) Whitelist(key ssh.PublicKey) {
|
||||||
|
authkey := NewAuthKey(key)
|
||||||
a.Lock()
|
a.Lock()
|
||||||
a.whitelist[fingerprint] = struct{}{}
|
a.whitelist[authkey] = struct{}{}
|
||||||
a.Unlock()
|
a.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ban will set a fingerprint as banned.
|
// Ban will set a fingerprint as banned.
|
||||||
func (a *Auth) Ban(fingerprint string) {
|
func (a *Auth) Ban(key ssh.PublicKey) {
|
||||||
|
authkey := NewAuthKey(key)
|
||||||
a.Lock()
|
a.Lock()
|
||||||
a.banned[fingerprint] = struct{}{}
|
a.banned[authkey] = struct{}{}
|
||||||
a.Unlock()
|
a.Unlock()
|
||||||
}
|
}
|
||||||
|
62
auth_test.go
Normal file
62
auth_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRandomPublicKey(bits int) (ssh.PublicKey, error) {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, bits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssh.NewPublicKey(key.Public())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClonePublicKey(key ssh.PublicKey) (ssh.PublicKey, error) {
|
||||||
|
return ssh.ParsePublicKey(key.Marshal())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthWhitelist(t *testing.T) {
|
||||||
|
key, err := NewRandomPublicKey(512)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := NewAuth()
|
||||||
|
ok, err := auth.Check(key)
|
||||||
|
if !ok || err != nil {
|
||||||
|
t.Error("Failed to permit in default state:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.Whitelist(key)
|
||||||
|
|
||||||
|
key_clone, err := ClonePublicKey(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(key_clone.Marshal()) != string(key.Marshal()) {
|
||||||
|
t.Error("Clone key does not match.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = auth.Check(key_clone)
|
||||||
|
if !ok || err != nil {
|
||||||
|
t.Error("Failed to permit whitelisted:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key2, err := NewRandomPublicKey(512)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = auth.Check(key2)
|
||||||
|
if ok || err == nil {
|
||||||
|
t.Error("Failed to restrict not whitelisted:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
101
cmd.go
101
cmd.go
@ -2,8 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -16,7 +14,6 @@ import (
|
|||||||
"github.com/alexcesaro/log/golog"
|
"github.com/alexcesaro/log/golog"
|
||||||
"github.com/jessevdk/go-flags"
|
"github.com/jessevdk/go-flags"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
|
||||||
|
|
||||||
"github.com/shazow/ssh-chat/chat"
|
"github.com/shazow/ssh-chat/chat"
|
||||||
"github.com/shazow/ssh-chat/sshd"
|
"github.com/shazow/ssh-chat/sshd"
|
||||||
@ -25,13 +22,13 @@ import _ "net/http/pprof"
|
|||||||
|
|
||||||
// Options contains the flag options
|
// Options contains the flag options
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."`
|
Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."`
|
||||||
Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
|
Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"`
|
||||||
Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
|
Bind string `long:"bind" description:"Host and port to listen on." default:"0.0.0.0:2022"`
|
||||||
Admin []string `long:"admin" description:"Fingerprint of pubkey to mark as admin."`
|
Admin string `long:"admin" description:"File of public keys who are admins."`
|
||||||
Whitelist string `long:"whitelist" description:"Optional file of pubkey fingerprints who are allowed to connect."`
|
Whitelist string `long:"whitelist" description:"Optional file of public keys who are allowed to connect."`
|
||||||
Motd string `long:"motd" description:"Optional Message of the Day file."`
|
Motd string `long:"motd" description:"Optional Message of the Day file."`
|
||||||
Pprof int `long:"pprof" description:"Enable pprof http server for profiling."`
|
Pprof int `long:"pprof" description:"Enable pprof http server for profiling."`
|
||||||
}
|
}
|
||||||
|
|
||||||
var logLevels = []log.Level{
|
var logLevels = []log.Level{
|
||||||
@ -83,7 +80,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey, err := readPrivateKey(privateKeyPath)
|
privateKey, err := ReadPrivateKey(privateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Couldn't read private key: %v", err)
|
logger.Errorf("Couldn't read private key: %v", err)
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
@ -109,25 +106,35 @@ func main() {
|
|||||||
fmt.Printf("Listening for connections on %v\n", s.Addr().String())
|
fmt.Printf("Listening for connections on %v\n", s.Addr().String())
|
||||||
|
|
||||||
host := NewHost(s)
|
host := NewHost(s)
|
||||||
host.auth = &auth
|
host.auth = auth
|
||||||
host.theme = &chat.Themes[0]
|
host.theme = &chat.Themes[0]
|
||||||
|
|
||||||
for _, fingerprint := range options.Admin {
|
err = fromFile(options.Admin, func(line []byte) error {
|
||||||
auth.Op(fingerprint)
|
key, _, _, _, err := ssh.ParseAuthorizedKey(line)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
auth.Op(key)
|
||||||
|
logger.Debugf("Added admin: %s", line)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to load admins: %v", err)
|
||||||
|
os.Exit(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Whitelist != "" {
|
err = fromFile(options.Whitelist, func(line []byte) error {
|
||||||
file, err := os.Open(options.Whitelist)
|
key, _, _, _, err := ssh.ParseAuthorizedKey(line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Could not open whitelist file")
|
return err
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
auth.Whitelist(scanner.Text())
|
|
||||||
}
|
}
|
||||||
|
auth.Whitelist(key)
|
||||||
|
logger.Debugf("Whitelisted: %s", line)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to load whitelist: %v", err)
|
||||||
|
os.Exit(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.Motd != "" {
|
if options.Motd != "" {
|
||||||
@ -154,42 +161,24 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// readPrivateKey attempts to read your private key and possibly decrypt it if it
|
func fromFile(path string, handler func(line []byte) error) error {
|
||||||
// requires a passphrase.
|
if path == "" {
|
||||||
// This function will prompt for a passphrase on STDIN if the environment variable (`IDENTITY_PASSPHRASE`),
|
// Skip
|
||||||
// is not set.
|
return nil
|
||||||
func readPrivateKey(privateKeyPath string) ([]byte, error) {
|
}
|
||||||
privateKey, err := ioutil.ReadFile(privateKeyPath)
|
|
||||||
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load identity: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
block, rest := pem.Decode(privateKey)
|
scanner := bufio.NewScanner(file)
|
||||||
if len(rest) > 0 {
|
for scanner.Scan() {
|
||||||
return nil, fmt.Errorf("extra data when decoding private key")
|
err := handler(scanner.Bytes())
|
||||||
}
|
|
||||||
if !x509.IsEncryptedPEMBlock(block) {
|
|
||||||
return privateKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
passphrase := []byte(os.Getenv("IDENTITY_PASSPHRASE"))
|
|
||||||
if len(passphrase) == 0 {
|
|
||||||
fmt.Printf("Enter passphrase: ")
|
|
||||||
passphrase, err = terminal.ReadPassword(int(os.Stdin.Fd()))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't read passphrase: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println()
|
|
||||||
}
|
}
|
||||||
der, err := x509.DecryptPEMBlock(block, passphrase)
|
return nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decrypt failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
privateKey = pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: block.Type,
|
|
||||||
Bytes: der,
|
|
||||||
})
|
|
||||||
|
|
||||||
return privateKey, nil
|
|
||||||
}
|
}
|
||||||
|
47
host_test.go
47
host_test.go
@ -2,12 +2,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/shazow/ssh-chat/chat"
|
"github.com/shazow/ssh-chat/chat"
|
||||||
"github.com/shazow/ssh-chat/sshd"
|
"github.com/shazow/ssh-chat/sshd"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func stripPrompt(s string) string {
|
func stripPrompt(s string) string {
|
||||||
@ -39,7 +42,7 @@ func TestHostGetPrompt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHostNameCollision(t *testing.T) {
|
func TestHostNameCollision(t *testing.T) {
|
||||||
key, err := sshd.NewRandomKey(512)
|
key, err := sshd.NewRandomSigner(512)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -50,6 +53,7 @@ func TestHostNameCollision(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
defer s.Close()
|
||||||
host := NewHost(s)
|
host := NewHost(s)
|
||||||
go host.Serve()
|
go host.Serve()
|
||||||
|
|
||||||
@ -110,5 +114,44 @@ func TestHostNameCollision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<-done
|
<-done
|
||||||
s.Close()
|
}
|
||||||
|
|
||||||
|
func TestHostWhitelist(t *testing.T) {
|
||||||
|
key, err := sshd.NewRandomSigner(512)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := NewAuth()
|
||||||
|
config := sshd.MakeAuth(auth)
|
||||||
|
config.AddHostKey(key)
|
||||||
|
|
||||||
|
s, err := sshd.ListenSSH(":0", config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
host := NewHost(s)
|
||||||
|
host.auth = auth
|
||||||
|
go host.Serve()
|
||||||
|
|
||||||
|
target := s.Addr().String()
|
||||||
|
|
||||||
|
err = sshd.NewClientSession(target, "foo", func(r io.Reader, w io.WriteCloser) {})
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientkey, err := rsa.GenerateKey(rand.Reader, 512)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientpubkey, _ := ssh.NewPublicKey(clientkey.Public())
|
||||||
|
auth.Whitelist(clientpubkey)
|
||||||
|
|
||||||
|
err = sshd.NewClientSession(target, "foo", func(r io.Reader, w io.WriteCloser) {})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Failed to block unwhitelisted connection.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
49
key.go
Normal file
49
key.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"code.google.com/p/gopass"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadPrivateKey attempts to read your private key and possibly decrypt it if it
|
||||||
|
// requires a passphrase.
|
||||||
|
// This function will prompt for a passphrase on STDIN if the environment variable (`IDENTITY_PASSPHRASE`),
|
||||||
|
// is not set.
|
||||||
|
func ReadPrivateKey(path string) ([]byte, error) {
|
||||||
|
privateKey, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load identity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, rest := pem.Decode(privateKey)
|
||||||
|
if len(rest) > 0 {
|
||||||
|
return nil, fmt.Errorf("extra data when decoding private key")
|
||||||
|
}
|
||||||
|
if !x509.IsEncryptedPEMBlock(block) {
|
||||||
|
return privateKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
passphrase := os.Getenv("IDENTITY_PASSPHRASE")
|
||||||
|
if passphrase == "" {
|
||||||
|
passphrase, err = gopass.GetPass("Enter passphrase: ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't read passphrase: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
der, err := x509.DecryptPEMBlock(block, []byte(passphrase))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decrypt failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey = pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: block.Type,
|
||||||
|
Bytes: der,
|
||||||
|
})
|
||||||
|
|
||||||
|
return privateKey, nil
|
||||||
|
}
|
31
sshd/auth.go
31
sshd/auth.go
@ -1,30 +1,34 @@
|
|||||||
package sshd
|
package sshd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Auth is used to authenticate connections based on public keys.
|
||||||
type Auth interface {
|
type Auth interface {
|
||||||
|
// Whether to allow connections without a public key.
|
||||||
AllowAnonymous() bool
|
AllowAnonymous() bool
|
||||||
Check(string) (bool, error)
|
// Given public key, return if the connection should be permitted.
|
||||||
|
Check(ssh.PublicKey) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeAuth makes an ssh.ServerConfig which performs authentication against an Auth implementation.
|
||||||
func MakeAuth(auth Auth) *ssh.ServerConfig {
|
func MakeAuth(auth Auth) *ssh.ServerConfig {
|
||||||
config := ssh.ServerConfig{
|
config := ssh.ServerConfig{
|
||||||
NoClientAuth: false,
|
NoClientAuth: false,
|
||||||
// Auth-related things should be constant-time to avoid timing attacks.
|
// Auth-related things should be constant-time to avoid timing attacks.
|
||||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
fingerprint := Fingerprint(key)
|
ok, err := auth.Check(key)
|
||||||
ok, err := auth.Check(fingerprint)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
perm := &ssh.Permissions{Extensions: map[string]string{"fingerprint": fingerprint}}
|
perm := &ssh.Permissions{Extensions: map[string]string{
|
||||||
|
"pubkey": string(ssh.MarshalAuthorizedKey(key)),
|
||||||
|
}}
|
||||||
return perm, nil
|
return perm, nil
|
||||||
},
|
},
|
||||||
KeyboardInteractiveCallback: func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
KeyboardInteractiveCallback: func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
||||||
@ -38,12 +42,16 @@ func MakeAuth(auth Auth) *ssh.ServerConfig {
|
|||||||
return &config
|
return &config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MakeNoAuth makes a simple ssh.ServerConfig which allows all connections.
|
||||||
|
// Primarily used for testing.
|
||||||
func MakeNoAuth() *ssh.ServerConfig {
|
func MakeNoAuth() *ssh.ServerConfig {
|
||||||
config := ssh.ServerConfig{
|
config := ssh.ServerConfig{
|
||||||
NoClientAuth: false,
|
NoClientAuth: false,
|
||||||
// Auth-related things should be constant-time to avoid timing attacks.
|
// Auth-related things should be constant-time to avoid timing attacks.
|
||||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
perm := &ssh.Permissions{Extensions: map[string]string{"fingerprint": Fingerprint(key)}}
|
perm := &ssh.Permissions{Extensions: map[string]string{
|
||||||
|
"pubkey": string(ssh.MarshalAuthorizedKey(key)),
|
||||||
|
}}
|
||||||
return perm, nil
|
return perm, nil
|
||||||
},
|
},
|
||||||
KeyboardInteractiveCallback: func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
KeyboardInteractiveCallback: func(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error) {
|
||||||
@ -54,8 +62,9 @@ func MakeNoAuth() *ssh.ServerConfig {
|
|||||||
return &config
|
return &config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fingerprint performs a SHA256 BASE64 fingerprint of the PublicKey, similar to OpenSSH.
|
||||||
|
// See: https://anongit.mindrot.org/openssh.git/commit/?id=56d1c83cdd1ac
|
||||||
func Fingerprint(k ssh.PublicKey) string {
|
func Fingerprint(k ssh.PublicKey) string {
|
||||||
hash := sha1.Sum(k.Marshal())
|
hash := sha256.Sum256(k.Marshal())
|
||||||
r := fmt.Sprintf("% x", hash)
|
return base64.StdEncoding.EncodeToString(hash[:])
|
||||||
return strings.Replace(r, " ", ":", -1)
|
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRandomKey generates a random key of a desired bit length.
|
// NewRandomSigner generates a random key of a desired bit length.
|
||||||
func NewRandomKey(bits int) (ssh.Signer, error) {
|
func NewRandomSigner(bits int) (ssh.Signer, error) {
|
||||||
key, err := rsa.GenerateKey(rand.Reader, bits)
|
key, err := rsa.GenerateKey(rand.Reader, bits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
44
sshd/client_test.go
Normal file
44
sshd/client_test.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package sshd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errRejectAuth = errors.New("not welcome here")
|
||||||
|
|
||||||
|
type RejectAuth struct{}
|
||||||
|
|
||||||
|
func (a RejectAuth) AllowAnonymous() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func (a RejectAuth) Check(ssh.PublicKey) (bool, error) {
|
||||||
|
return false, errRejectAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func consume(ch <-chan *Terminal) {
|
||||||
|
for range ch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientReject(t *testing.T) {
|
||||||
|
signer, err := NewRandomSigner(512)
|
||||||
|
config := MakeAuth(RejectAuth{})
|
||||||
|
config.AddHostKey(signer)
|
||||||
|
|
||||||
|
s, err := ListenSSH(":0", config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
go consume(s.ServeTerminal())
|
||||||
|
|
||||||
|
conn, err := ssh.Dial("tcp", s.Addr().String(), NewClientConfig("foo"))
|
||||||
|
if err == nil {
|
||||||
|
defer conn.Close()
|
||||||
|
t.Error("Failed to reject conncetion")
|
||||||
|
}
|
||||||
|
t.Log(err)
|
||||||
|
}
|
@ -29,6 +29,7 @@ func (l *SSHListener) handleConn(conn net.Conn) (*Terminal, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: Disconnect if too many faulty requests? (Avoid DoS.)
|
||||||
go ssh.DiscardRequests(requests)
|
go ssh.DiscardRequests(requests)
|
||||||
return NewSession(sshConn, channels)
|
return NewSession(sshConn, channels)
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Move some of these into their own package?
|
|
||||||
|
|
||||||
func TestServerInit(t *testing.T) {
|
func TestServerInit(t *testing.T) {
|
||||||
config := MakeNoAuth()
|
config := MakeNoAuth()
|
||||||
s, err := ListenSSH(":badport", config)
|
s, err := ListenSSH(":badport", config)
|
||||||
@ -27,7 +25,7 @@ func TestServerInit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServeTerminals(t *testing.T) {
|
func TestServeTerminals(t *testing.T) {
|
||||||
signer, err := NewRandomKey(512)
|
signer, err := NewRandomSigner(512)
|
||||||
config := MakeNoAuth()
|
config := MakeNoAuth()
|
||||||
config.AddHostKey(signer)
|
config.AddHostKey(signer)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user