Skip to content

Commit

Permalink
Move SSH keys handling to the kernel
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinguidee committed Sep 28, 2023
1 parent 33c3a10 commit 5059605
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 119 deletions.
2 changes: 1 addition & 1 deletion router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func (r *Router) initServices(about types.About) {
dependenciesService = services.NewDependenciesService(about.Version)
settingsService = services.NewSettingsService(settingsFSAdapter)
hardwareService = services.NewHardwareService()
sshService = services.NewSSHService(nil)
sshService = services.NewSSHService()
}

func (r *Router) initAPIRoutes(about types.About) {
Expand Down
3 changes: 3 additions & 0 deletions router/router_kernel.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
dockerCliAdapter types.DockerAdapterPort

dockerKernelService services.DockerKernelService
sshKernelService services.SSHKernelService
)

type KernelRouter struct {
Expand Down Expand Up @@ -81,10 +82,12 @@ func (r *KernelRouter) initAdapters() {

func (r *KernelRouter) initServices() {
dockerKernelService = services.NewDockerKernelService(dockerCliAdapter)
sshKernelService = services.NewSSHKernelService(nil)
}

func (r *KernelRouter) initAPIRoutes() {
api := r.engine.Group("/api")

addDockerKernelRoutes(api.Group("/docker"))
addSecurityKernelRoutes(api.Group("/security"))
}
4 changes: 2 additions & 2 deletions router/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func handleGetSSHKey(c *gin.Context) {
c.JSON(http.StatusOK, keys)
}

type addSSHKeyBody struct {
type AddSSHKeyBody struct {
AuthorizedKey string `json:"authorized_key"`
}

Expand All @@ -41,7 +41,7 @@ type addSSHKeyBody struct {
// - failed_to_parse_body: failed to parse the request body.
// - failed_to_add_ssh_key: failed to add the SSH key.
func handleAddSSHKey(c *gin.Context) {
var body addSSHKeyBody
var body AddSSHKeyBody
err := c.BindJSON(&body)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, types.APIError{
Expand Down
94 changes: 94 additions & 0 deletions router/security_kernel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package router

import (
"bytes"
"errors"
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/vertex-center/vertex/services"
"github.com/vertex-center/vertex/types"
)

func addSecurityKernelRoutes(r *gin.RouterGroup) {
r.GET("/ssh", handleGetSSHKeyKernel)
r.POST("/ssh", handleAddSSHKeyKernel)
r.DELETE("/ssh/:fingerprint", handleDeleteSSHKeyKernel)
}

// handleGetSSHKey handles the retrieval of the SSH key.
// Errors can be:
// - failed_to_get_ssh_keys: failed to get the SSH keys.
func handleGetSSHKeyKernel(c *gin.Context) {
keys, err := sshKernelService.GetAll()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, types.APIError{
Code: "failed_to_get_ssh_keys",
Message: fmt.Sprintf("failed to get SSH keys: %v", err),
})
return
}

c.JSON(http.StatusOK, keys)
}

// handleAddSSHKey handles the addition of an SSH key.
// Errors can be:
// - failed_to_parse_body: failed to parse the request body.
// - failed_to_add_ssh_key: failed to add the SSH key.
func handleAddSSHKeyKernel(c *gin.Context) {
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(c.Request.Body)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, types.APIError{
Code: "failed_to_parse_body",
Message: fmt.Sprintf("failed to parse request body: %v", err),
})
return
}
key := buf.String()

err = sshKernelService.Add(key)
if err != nil && errors.Is(err, services.ErrInvalidPublicKey) {
_ = c.AbortWithError(http.StatusBadRequest, types.APIError{
Code: "invalid_public_key",
Message: fmt.Sprintf("error while parsing the public key: %v", err),
})
return
} else if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, types.APIError{
Code: "failed_to_add_ssh_key",
Message: fmt.Sprintf("failed to add SSH key: %v", err),
})
return
}

c.Status(http.StatusCreated)
}

// handleDeleteSSHKey handles the deletion of an SSH key.
// Errors can be:
// - failed_to_parse_body: failed to parse the request body.
// - failed_to_delete_ssh_key: failed to delete the SSH key.
func handleDeleteSSHKeyKernel(c *gin.Context) {
fingerprint := c.Param("fingerprint")
if fingerprint == "" {
_ = c.AbortWithError(http.StatusBadRequest, types.APIError{
Code: "invalid_fingerprint",
Message: "invalid fingerprint",
})
return
}

err := sshKernelService.Delete(fingerprint)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, types.APIError{
Code: "failed_to_delete_ssh_key",
Message: fmt.Sprintf("failed to delete SSH key: %v", err),
})
return
}

c.Status(http.StatusNoContent)
}
119 changes: 17 additions & 102 deletions services/ssh.go
Original file line number Diff line number Diff line change
@@ -1,124 +1,39 @@
package services

import (
"errors"
"os"
"path"
"strings"
"context"

"github.com/vertex-center/vertex/pkg/log"
"github.com/carlmjohnson/requests"
"github.com/vertex-center/vertex/types"
"golang.org/x/crypto/ssh"
)

var (
ErrInvalidPublicKey = errors.New("invalid key")
)

type SSHService struct {
authorizedKeysPath string
}

type SSHServiceParams struct {
AuthorizedKeysPath string
}

func NewSSHService(params *SSHServiceParams) SSHService {
func NewSSHService() SSHService {
s := SSHService{}

if params == nil {
params = &SSHServiceParams{}
}

s.authorizedKeysPath = params.AuthorizedKeysPath
if s.authorizedKeysPath == "" {
var err error
s.authorizedKeysPath, err = getAuthorizedKeysPath()
if err != nil {
log.Error(err)
}
}

return s
}

func (s *SSHService) GetAll() ([]types.PublicKey, error) {
bytes, err := os.ReadFile(s.authorizedKeysPath)
if err != nil && errors.Is(err, os.ErrNotExist) {
log.Info("authorized_keys file does not exist")
return []types.PublicKey{}, nil
} else if err != nil {
return nil, err
}

var publicKeys []ssh.PublicKey
for len(bytes) > 0 {
pubKey, _, _, rest, _ := ssh.ParseAuthorizedKey(bytes)
if pubKey != nil {
publicKeys = append(publicKeys, pubKey)
}
bytes = rest
}

keys := []types.PublicKey{}
for _, key := range publicKeys {
keys = append(keys, types.PublicKey{
Type: key.Type(),
FingerprintSHA256: ssh.FingerprintSHA256(key),
})
}

return keys, nil
var keys []types.PublicKey
err := requests.URL("http://localhost:6131/api/security/ssh").
ToJSON(&keys).
Fetch(context.Background())
return keys, err
}

// Add adds an SSH key to the authorized keys file. The key must
// be a valid SSH public key, otherwise ErrInvalidPublicKey is returned.
func (s *SSHService) Add(authorizedKey string) error {
// Check if the key is valid.
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authorizedKey))
if err != nil {
return ErrInvalidPublicKey
}

file, err := os.OpenFile(s.authorizedKeysPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()

_, err = file.WriteString(authorizedKey + "\n")
return err
func (s *SSHService) Add(key string) error {
return requests.URL("http://localhost:6131/api/security/ssh").
Post().
BodyBytes([]byte(key)).
Fetch(context.Background())
}

// Delete deletes an SSH key from the authorized keys file.
func (s *SSHService) Delete(fingerprint string) error {
content, err := os.ReadFile(s.authorizedKeysPath)
if err != nil {
return err
}

lines := strings.Split(string(content), "\n")
for i, line := range lines {
key, _, _, _, _ := ssh.ParseAuthorizedKey([]byte(line))
if key == nil {
continue
}

fingerprintLine := ssh.FingerprintSHA256(key)

if fingerprintLine == fingerprint {
lines = append(lines[:i], lines[i+1:]...)
break
}
}

return os.WriteFile(s.authorizedKeysPath, []byte(strings.Join(lines, "\n")), 0644)
}

func getAuthorizedKeysPath() (string, error) {
dir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return path.Join(dir, ".ssh", "authorized_keys"), nil
return requests.URL("http://localhost:6131/").
Pathf("/api/security/ssh/%s", fingerprint).
Delete().
Fetch(context.Background())
}
28 changes: 14 additions & 14 deletions services/ssh_test.go → services/ssh_kernel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
"golang.org/x/crypto/ssh"
)

type SSHServiceTestSuite struct {
type SSHKernelServiceTestSuite struct {
suite.Suite

service SSHService
service SSHKernelService
authorizedKeysFile *os.File

// Test data
Expand All @@ -23,11 +23,11 @@ type SSHServiceTestSuite struct {
fingerprints []string
}

func TestSSHServiceTestSuite(t *testing.T) {
suite.Run(t, new(SSHServiceTestSuite))
func TestSSHKernelServiceTestSuite(t *testing.T) {
suite.Run(t, new(SSHKernelServiceTestSuite))
}

func (suite *SSHServiceTestSuite) SetupSuite() {
func (suite *SSHKernelServiceTestSuite) SetupSuite() {
suite.keys = []string{
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6IPH4bqdhPVQUfdmuisPdQJO6Tv2+a0OZ9qLs6W0W2flxn6/yQmYut02cl0UtNcDtmb4RqNj2ms2v2TeDVSWVZkUR/q4jjZSSljQEpTd3r1YhYrO/GPDNiIUMm5HvZ8qIfBQA6gn9uMT1g6FO53O64ACNr+ItU4gNdr+S44MNJRMxMy6+s/LsFlQjyO2MbPQHQ6HSOgTLrCNiH8NTLA/evekrZ/rmIZrrES2vQvw5pbCDgEOkLZruRSMMFJFStb6tlGoiN/jQpfX51jebDVLZ1/U3SU5+7LNN6DxZYE9w1eCA2G8L8q1PUYju+b4F6IhGA1AYXPaAaR12qRJ4lLeN",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtkVmRevgiIRc7QHahcd01d+0qjtZj1KcY5u25TQW7GomgVuJukdKupnUP2Q1DGo1JjI0OMaIVcEAs4rQgHDAIYovHSeQpkhb3QzJKpS9YUxq/ZWtBQd7cdyRrwAJuT0uR0m52NopEVaaETSIFH6byScRoOAdKgRPwWv5EiHleklOuZCG2/BKq2FtHIb5xb7eAEeMy/5ebu1f4C211/q/Y0AIy/Gp7rJGTDSutTi2UXMQxo3kVDykIIg/xqH2h5IUvYOR8Y+t6f9rbKPcglc+9ygmYHeqrIVmkFzru1sbOOCHlIfv1N53RVp5A9734cHm9u3FzfIPkV+j0tOJ8dhdP",
Expand All @@ -43,7 +43,7 @@ func (suite *SSHServiceTestSuite) SetupSuite() {
}
}

func (suite *SSHServiceTestSuite) SetupTest() {
func (suite *SSHKernelServiceTestSuite) SetupTest() {
var err error

suite.authorizedKeysFile, err = os.CreateTemp("", "*_authorized_keys")
Expand All @@ -58,19 +58,19 @@ func (suite *SSHServiceTestSuite) SetupTest() {
}
}

suite.service = NewSSHService(&SSHServiceParams{
suite.service = NewSSHKernelService(&SSHKernelServiceParams{
AuthorizedKeysPath: suite.authorizedKeysFile.Name(),
})
}

func (suite *SSHServiceTestSuite) TearDownTest() {
func (suite *SSHKernelServiceTestSuite) TearDownTest() {
err := os.Remove(suite.authorizedKeysFile.Name())
if err != nil && !errors.Is(err, os.ErrNotExist) {
suite.NoError(err)
}
}

func (suite *SSHServiceTestSuite) TestGetAll() {
func (suite *SSHKernelServiceTestSuite) TestGetAll() {
keys, err := suite.service.GetAll()
suite.NoError(err)
suite.Equal(2, len(keys))
Expand All @@ -80,7 +80,7 @@ func (suite *SSHServiceTestSuite) TestGetAll() {
}
}

func (suite *SSHServiceTestSuite) TestGetAllInvalidKey() {
func (suite *SSHKernelServiceTestSuite) TestGetAllInvalidKey() {
_, err := suite.authorizedKeysFile.Write([]byte("invalid"))
suite.NoError(err)

Expand All @@ -89,7 +89,7 @@ func (suite *SSHServiceTestSuite) TestGetAllInvalidKey() {
suite.Equal(2, len(keys))
}

func (suite *SSHServiceTestSuite) TestGetAllNoSuchFile() {
func (suite *SSHKernelServiceTestSuite) TestGetAllNoSuchFile() {
suite.authorizedKeysFile.Close()
err := os.Remove(suite.service.authorizedKeysPath)
suite.NoError(err)
Expand All @@ -99,7 +99,7 @@ func (suite *SSHServiceTestSuite) TestGetAllNoSuchFile() {
suite.Equal(0, len(keys))
}

func (suite *SSHServiceTestSuite) TestAdd() {
func (suite *SSHKernelServiceTestSuite) TestAdd() {
publicKey, err := generatePublicKey()
if err != nil {
suite.FailNow(err.Error())
Expand All @@ -113,7 +113,7 @@ func (suite *SSHServiceTestSuite) TestAdd() {
suite.Equal(3, len(keys))
}

func (suite *SSHServiceTestSuite) TestAddInvalidKey() {
func (suite *SSHKernelServiceTestSuite) TestAddInvalidKey() {
err := suite.service.Add("invalid")
suite.Error(err)

Expand All @@ -122,7 +122,7 @@ func (suite *SSHServiceTestSuite) TestAddInvalidKey() {
suite.Equal(2, len(keys))
}

func (suite *SSHServiceTestSuite) TestDelete() {
func (suite *SSHKernelServiceTestSuite) TestDelete() {
err := suite.service.Delete(ssh.FingerprintSHA256(suite.authorizedKeys[0]))
suite.NoError(err)

Expand Down
Loading

0 comments on commit 5059605

Please sign in to comment.