Add SSH Key Management

This commit is contained in:
Tom Wiesing 2023-01-15 13:41:56 +01:00
parent ef76844922
commit bcd1805001
No known key found for this signature in database
62 changed files with 1004 additions and 188 deletions

View file

@ -0,0 +1,51 @@
package sshkeys
import (
"context"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component/sql"
"github.com/gliderlabs/ssh"
)
type SSHKeys struct {
component.Base
Dependencies struct {
SQL *sql.SQL
Auth *auth.Auth
}
}
var (
_ component.Table = (*SSHKeys)(nil)
_ component.UserDeleteHook = (*SSHKeys)(nil)
)
// Admin returns the set of administrative ssh keys.
// These are ssh keys associated to distillery admin users.
func (k *SSHKeys) Admin(ctx context.Context) (keys []ssh.PublicKey, err error) {
users, err := k.Dependencies.Auth.Users(ctx)
if err != nil {
return nil, err
}
// iterate over enabled distillery admin users
for _, user := range users {
if !user.IsEnabled() || !user.IsAdmin() {
continue
}
ukeys, err := k.Keys(ctx, user.User.User)
if err != nil {
return nil, err
}
for _, ukey := range ukeys {
if pk := ukey.PublicKey(); pk != nil {
keys = append(keys, pk)
}
}
}
// and return the keys!
return keys, nil
}

View file

@ -0,0 +1,55 @@
package sshkeys
import (
"crypto/rand"
"math/big"
"time"
"github.com/gliderlabs/ssh"
)
// KeyOneOf checks if keys is one of the given set of keys.
func KeyOneOf(keys []ssh.PublicKey, key ssh.PublicKey) bool {
return len(KeyIndexes(keys, key)) > 0
}
// KeyIndexes returns a slice of ints that contain the indexes of the given key.
func KeyIndexes(keys []ssh.PublicKey, key ssh.PublicKey) []int {
indexes := make([]int, 0, len(keys))
for i, cey := range keys {
if ssh.KeysEqual(key, cey) {
indexes = append(indexes, i)
}
}
return indexes
}
const (
slowdownMinDelay = time.Second / 10
slowdownJitter = time.Second / 10
)
// slowdown invokes f immediatly, but introduces a random delay to prevent timing attacks.
// the delay is also introduced if f() panics.
func Slowdown[T any](f func() T) T {
start := time.Now()
defer func() {
// sleep the minimum remaining time
remain := time.Since(start) - slowdownMinDelay
if remain > 0 {
time.Sleep(remain)
}
// find a second random delay
delay, err := rand.Int(rand.Reader, big.NewInt(int64(slowdownJitter)))
if err != nil {
return
}
// and wait that long
time.Sleep(time.Duration(delay.Int64()))
}()
return f()
}

View file

@ -0,0 +1,128 @@
package sshkeys
import (
"context"
"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
"github.com/FAU-CDI/wisski-distillery/internal/models"
"github.com/gliderlabs/ssh"
"github.com/tkw1536/goprogram/lib/reflectx"
)
func (ssh2 *SSHKeys) TableInfo() component.TableInfo {
return component.TableInfo{
Model: reflectx.TypeOf[models.Keys](),
Name: models.KeysTable,
}
}
// Keys returns a list of keys for the given user
func (ssh2 *SSHKeys) Keys(ctx context.Context, user string) ([]models.Keys, error) {
// the empty user has no key
if user == "" {
return nil, nil
}
// get the table
table, err := ssh2.Dependencies.SQL.QueryTable(ctx, ssh2)
if err != nil {
return nil, err
}
var keys []models.Keys
// make a query to find all keys (in the underlying model)
query := table.Find(&keys, &models.Keys{User: user})
if query.Error != nil {
return nil, query.Error
}
return keys, nil
}
// Add adds a new key to the given user, unless it already exists
func (ssh2 *SSHKeys) Add(ctx context.Context, user string, comment string, key ssh.PublicKey) error {
// check that the given user exists
{
_, err := ssh2.Dependencies.Auth.User(ctx, user)
if err != nil {
return err
}
}
// fetch all the keys
keys, err := ssh2.Keys(ctx, user)
if err != nil {
return err
}
pks := make([]ssh.PublicKey, 0, len(keys))
for _, key := range keys {
if pk := key.PublicKey(); pk != nil {
pks = append(pks, pk)
}
}
// key already exists
if KeyOneOf(pks, key) {
return nil
}
// create a new key with the given comment
mk := models.Keys{
User: user,
Comment: comment,
}
mk.SetPublicKey(key)
// get the table
table, err := ssh2.Dependencies.SQL.QueryTable(ctx, ssh2)
if err != nil {
return err
}
// create the key instance
return table.Create(&mk).Error
}
// Remove removes a given publuc key from a user.
func (ssh2 *SSHKeys) Remove(ctx context.Context, user string, key ssh.PublicKey) error {
// find all the keys for the given user
keys, err := ssh2.Keys(ctx, user)
if err != nil {
return err
}
// iterate and find all the public keys
var pks []uint
for _, candidate := range keys {
if ssh.KeysEqual(candidate.PublicKey(), key) {
pks = append(pks, candidate.Pk)
}
}
// nothing to delete
if len(pks) == 0 {
return nil
}
// query the table again
table, err := ssh2.Dependencies.SQL.QueryTable(ctx, ssh2)
if err != nil {
return nil
}
// and do the delete
return table.Where("pk in ?", pks).Delete(&models.Keys{}).Error
}
func (ssh2 *SSHKeys) OnUserDelete(ctx context.Context, user *models.User) error {
// get the table
table, err := ssh2.Dependencies.SQL.QueryTable(ctx, ssh2)
if err != nil {
return err
}
// delete all keys for the user
return table.Delete(&models.Keys{}, &models.Keys{User: user.User}).Error
}