From de920570339c078ddfa977b4313b634873ab685b Mon Sep 17 00:00:00 2001 From: nato Date: Mon, 23 Jan 2023 17:41:22 -0800 Subject: [PATCH] Auth.go: parse and record ssh key comments at startup --- auth.go | 61 ++++++++++++++++++++++++++++++++------------- auth_test.go | 2 +- cmd/ssh-chat/cmd.go | 12 +++++---- host.go | 8 +++--- host_test.go | 4 +-- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/auth.go b/auth.go index 0cd5e0d..bac0760 100644 --- a/auth.go +++ b/auth.go @@ -18,7 +18,7 @@ import ( // KeyLoader loads public keys, e.g. from an authorized_keys file. // It must return a nil slice on error. -type KeyLoader func() ([]ssh.PublicKey, error) +type KeyLoader func() ([]ssh.PublicKey, []string, error) // ErrNotAllowed Is the error returned when a key is checked that is not allowlisted, // when allowlisting is enabled. @@ -55,12 +55,14 @@ func newAuthAddr(addr net.Addr) string { // Auth stores lookups for bans, allowlists, and ops. It implements the sshd.Auth interface. // If the contained passphrase is not empty, it complements a allowlist. type Auth struct { - passphraseHash []byte - bannedAddr *set.Set - bannedClient *set.Set - banned *set.Set - allowlist *set.Set - ops *set.Set + passphraseHash []byte + bannedAddr *set.Set + bannedClient *set.Set + banned *set.Set + allowlist *set.Set + ops *set.Set + allowlistComments map[string]string + adminComments map[string]string settingsMu sync.RWMutex allowlistMode bool @@ -71,11 +73,13 @@ type Auth struct { // NewAuth creates a new empty Auth. func NewAuth() *Auth { return &Auth{ - bannedAddr: set.New(), - bannedClient: set.New(), - banned: set.New(), - allowlist: set.New(), - ops: set.New(), + bannedAddr: set.New(), + bannedClient: set.New(), + banned: set.New(), + allowlist: set.New(), + ops: set.New(), + allowlistComments: make(map[string]string), + adminComments: make(map[string]string), } } @@ -158,7 +162,8 @@ func (a *Auth) CheckPassphrase(passphrase string) error { } // Op sets a public key as a known operator. -func (a *Auth) Op(key ssh.PublicKey, d time.Duration) { +// comment is the string that follows the public key in openssh authorized_keys format +func (a *Auth) Op(key ssh.PublicKey, comment string, d time.Duration) { if key == nil { return } @@ -168,6 +173,12 @@ func (a *Auth) Op(key ssh.PublicKey, d time.Duration) { } else { a.ops.Set(authItem) } + + fingerprint := sshd.Fingerprint(key) + if len(a.adminComments[fingerprint]) == 0 { + a.adminComments[fingerprint] = comment + } + logger.Debugf("Added to ops: %q (for %s)", authItem.Key(), d) } @@ -189,11 +200,13 @@ func (a *Auth) LoadOps(loader KeyLoader) error { func (a *Auth) ReloadOps() error { a.settingsMu.RLock() defer a.settingsMu.RUnlock() + a.adminComments = make(map[string]string) return addFromLoader(a.opLoader, a.Op) } // Allowlist will set a public key as a allowlisted user. -func (a *Auth) Allowlist(key ssh.PublicKey, d time.Duration) { +// comment is the string that follows the public key in openssh authorized_keys format +func (a *Auth) Allowlist(key ssh.PublicKey, comment string, d time.Duration) { if key == nil { return } @@ -204,6 +217,12 @@ func (a *Auth) Allowlist(key ssh.PublicKey, d time.Duration) { } else { err = a.allowlist.Set(authItem) } + + fingerprint := sshd.Fingerprint(key) + if len(a.allowlistComments[fingerprint]) == 0 { + a.allowlistComments[fingerprint] = comment + } + if err == nil { logger.Debugf("Added to allowlist: %q (for %s)", authItem.Key(), d) } else { @@ -223,16 +242,22 @@ func (a *Auth) LoadAllowlist(loader KeyLoader) error { func (a *Auth) ReloadAllowlist() error { a.settingsMu.RLock() defer a.settingsMu.RUnlock() + a.allowlistComments = make(map[string]string) return addFromLoader(a.allowlistLoader, a.Allowlist) } -func addFromLoader(loader KeyLoader, adder func(ssh.PublicKey, time.Duration)) error { +func addFromLoader(loader KeyLoader, adder func(ssh.PublicKey, string, time.Duration)) error { if loader == nil { return nil } - keys, err := loader() - for _, key := range keys { - adder(key, 0) + keys, comments, err := loader() + var comment string + for index, key := range keys { + comment = "" + if len(comments) > index { + comment = comments[index] + } + adder(key, comment, 0) } return err } diff --git a/auth_test.go b/auth_test.go index b7c55e3..16d9467 100644 --- a/auth_test.go +++ b/auth_test.go @@ -33,7 +33,7 @@ func TestAuthAllowlist(t *testing.T) { t.Error("Failed to permit in default state:", err) } - auth.Allowlist(key, 0) + auth.Allowlist(key, "", 0) auth.SetAllowlistMode(true) keyClone, err := ClonePublicKey(key) diff --git a/cmd/ssh-chat/cmd.go b/cmd/ssh-chat/cmd.go index 96e5a5a..1566a16 100644 --- a/cmd/ssh-chat/cmd.go +++ b/cmd/ssh-chat/cmd.go @@ -200,28 +200,30 @@ func loaderFromFile(path string, logger *golog.Logger) sshchat.KeyLoader { if path == "" { return nil } - return func() ([]ssh.PublicKey, error) { + return func() ([]ssh.PublicKey, []string, error) { file, err := os.Open(path) if err != nil { - return nil, err + return nil, nil, err } defer file.Close() var keys []ssh.PublicKey + var comments []string scanner := bufio.NewScanner(file) for scanner.Scan() { - key, _, _, _, err := ssh.ParseAuthorizedKey(scanner.Bytes()) + key, comment, _, _, err := ssh.ParseAuthorizedKey(scanner.Bytes()) if err != nil { if err.Error() == "ssh: no key found" { continue // Skip line } - return nil, err + return nil, nil, err } keys = append(keys, key) + comments = append(comments, comment) } if keys == nil { logger.Warning("file", path, "contained no keys") } - return keys, nil + return keys, comments, nil } } diff --git a/host.go b/host.go index be5eeab..62276ab 100644 --- a/host.go +++ b/host.go @@ -626,7 +626,7 @@ func (h *Host) InitCommands(c *chat.Commands) { member.IsOp = opValue id := member.Identifier.(*Identity) - h.auth.Op(id.PublicKey(), until) + h.auth.Op(id.PublicKey(), "", until) var body string if opValue { @@ -780,7 +780,7 @@ func (h *Host) InitCommands(c *chat.Commands) { if pk == nil { noKeyUsers = append(noKeyUsers, user.Identifier.Name()) } else { - h.auth.Allowlist(pk, 0) + h.auth.Allowlist(pk, "", 0) } } return nil @@ -876,9 +876,9 @@ func (h *Host) InitCommands(c *chat.Commands) { case "off": h.auth.SetAllowlistMode(false) case "add": - replyLines = forPubkeyUser(args[1:], func(pk ssh.PublicKey) { h.auth.Allowlist(pk, 0) }) + replyLines = forPubkeyUser(args[1:], func(pk ssh.PublicKey) { h.auth.Allowlist(pk, "", 0) }) case "remove": - replyLines = forPubkeyUser(args[1:], func(pk ssh.PublicKey) { h.auth.Allowlist(pk, 1) }) + replyLines = forPubkeyUser(args[1:], func(pk ssh.PublicKey) { h.auth.Allowlist(pk, "", 1) }) case "import": replyLines, err = allowlistImport(args[1:]) case "reload": diff --git a/host_test.go b/host_test.go index 4075717..fa6e490 100644 --- a/host_test.go +++ b/host_test.go @@ -164,12 +164,12 @@ func TestHostAllowlist(t *testing.T) { } clientKey := clientPrivateKey.PublicKey() loadCount := -1 - loader := func() ([]ssh.PublicKey, error) { + loader := func() ([]ssh.PublicKey, []string, error) { loadCount++ return [][]ssh.PublicKey{ {}, {clientKey}, - }[loadCount], nil + }[loadCount], []string{}, nil } auth.LoadAllowlist(loader)