140 lines
4.4 KiB
Go
140 lines
4.4 KiB
Go
// Package ssh provides SSH connection management for the MCP server.
|
|
package ssh
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
const (
|
|
// DevKeyPath - локальный путь по умолчанию для запуска вне forge
|
|
// (без SSH_MCP_KEY_PATH). Производственный запуск всегда передаёт
|
|
// SSH_MCP_KEY_PATH от forge (см. cmd/serve/wiring.go): либо системный
|
|
// ключ data/_system/ssh/id_ed25519, либо пер-агентный ssh_key_dir.
|
|
DevKeyPath = "./data/_system/ssh/id_ed25519"
|
|
)
|
|
|
|
// KeyManager handles SSH key generation and loading.
|
|
type KeyManager struct {
|
|
keyPath string
|
|
}
|
|
|
|
// NewKeyManager creates a new KeyManager. If keyPath is empty, the system
|
|
// default is used (SSH_MCP_KEY_PATH env, otherwise local ./data/_system/ssh).
|
|
func NewKeyManager(keyPath string) *KeyManager {
|
|
if keyPath == "" {
|
|
keyPath = getDefaultKeyPath()
|
|
}
|
|
return &KeyManager{keyPath: keyPath}
|
|
}
|
|
|
|
// getDefaultKeyPath returns the appropriate path. Honors SSH_MCP_KEY_PATH if
|
|
// set (forge всегда так делает); иначе локальный дефолт под единой схемой
|
|
// данных (data/_system/ssh). Автодетект окружения убран: никаких жёстко
|
|
// зашитых /data - путь всегда приходит от оператора через env.
|
|
func getDefaultKeyPath() string {
|
|
if p := os.Getenv("SSH_MCP_KEY_PATH"); p != "" {
|
|
return p
|
|
}
|
|
return DevKeyPath
|
|
}
|
|
|
|
// EnsureKey ensures the system key exists, generating if necessary.
|
|
func (km *KeyManager) EnsureKey() error {
|
|
keyDir := filepath.Dir(km.keyPath)
|
|
|
|
// Check if directory exists
|
|
stat, err := os.Stat(keyDir)
|
|
if os.IsNotExist(err) {
|
|
// Directory doesn't exist - create it (0700; единственные личные данные).
|
|
if err := os.MkdirAll(keyDir, 0700); err != nil {
|
|
return fmt.Errorf("failed to create key directory %s: %w", keyDir, err)
|
|
}
|
|
log.Printf("ssh-key: created directory %s", keyDir)
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to access key directory %s: %w", keyDir, err)
|
|
} else if !stat.IsDir() {
|
|
return fmt.Errorf("key path %s exists but is not a directory", keyDir)
|
|
}
|
|
|
|
// Test write permissions by attempting to create a temp file
|
|
testFile := filepath.Join(keyDir, ".write_test")
|
|
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
|
|
return fmt.Errorf("key directory %s is not writable: %w", keyDir, err)
|
|
}
|
|
os.Remove(testFile)
|
|
|
|
if _, err := os.Stat(km.keyPath); os.IsNotExist(err) {
|
|
log.Printf("ssh-key: generating new key at %s", km.keyPath)
|
|
return km.generateKey()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateKey creates a new Ed25519 key pair.
|
|
func (km *KeyManager) generateKey() error {
|
|
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate key: %w", err)
|
|
}
|
|
|
|
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "ssh-mcp")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(km.keyPath, pem.EncodeToMemory(privKeyBytes), 0600); err != nil {
|
|
return fmt.Errorf("failed to write private key: %w", err)
|
|
}
|
|
|
|
sshPubKey, err := ssh.NewPublicKey(pubKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SSH public key: %w", err)
|
|
}
|
|
|
|
// Add "SSH-MCP" comment to public key for identification
|
|
pubKeyBytes := []byte(fmt.Sprintf("%s %s SSH-MCP\n",
|
|
sshPubKey.Type(),
|
|
base64.StdEncoding.EncodeToString(sshPubKey.Marshal())))
|
|
|
|
if err := os.WriteFile(km.keyPath+".pub", pubKeyBytes, 0644); err != nil {
|
|
return fmt.Errorf("failed to write public key: %w", err)
|
|
}
|
|
|
|
log.Println("ssh-key: generated successfully")
|
|
return nil
|
|
}
|
|
|
|
// LoadPrivateKey loads the private key from disk.
|
|
func (km *KeyManager) LoadPrivateKey() (ssh.Signer, error) {
|
|
keyBytes, err := os.ReadFile(km.keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read private key: %w", err)
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey(keyBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
|
|
return signer, nil
|
|
}
|
|
|
|
// GetPublicKey returns the public key string.
|
|
func (km *KeyManager) GetPublicKey() (string, error) {
|
|
pubKeyBytes, err := os.ReadFile(km.keyPath + ".pub")
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read public key: %w", err)
|
|
}
|
|
return string(pubKeyBytes), nil
|
|
}
|