From d15ec1f8cb7a7422c4f2387267909432670b49e0 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sun, 4 May 2025 16:26:04 +0200 Subject: [PATCH] feat: refactor REST API --- README.md | 87 ++---------------------- api/rcond.yaml | 157 +++++++++++++++++++++++++++++-------------- go.mod | 1 + go.sum | 2 + pkg/http/handlers.go | 138 +++++++++++++++++++++++++------------ pkg/http/server.go | 11 +-- pkg/network/nm.go | 111 ++++++++++++++++++++++-------- pkg/user/ssh.go | 58 +++++++++------- 8 files changed, 336 insertions(+), 229 deletions(-) diff --git a/README.md b/README.md index 2e52f43..fd0405e 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,14 @@ All endpoints except `/health` require authentication via an API token passed in | Method | Path | Description | |--------|------|-------------| | GET | `/health` | Health check endpoint that returns status | -| POST | `/network/up` | Create and activate a WiFi access point | -| POST | `/network/down` | Deactivate a WiFi interface | -| POST | `/network/remove` | Remove the stored connection profile | +| POST | `/network/ap` | Create and activate a WiFi access point | +| PUT | `/network/interface/{interface}` | Activate a connection | +| DELETE | `/network/interface/{interface}` | Deactivate a connection | +| DELETE | `/network/connection/{uuid}` | Remove a connection | | GET | `/hostname` | Get the hostname | | POST | `/hostname` | Set the hostname | -| POST | `/authorized-key` | Add an authorized SSH key | -| DELETE | `/authorized-key` | Remove an authorized SSH key | +| POST | `/users/{user}/keys` | Add an authorized SSH key | +| DELETE | `/users/{user}/keys/{fingerprint}` | Remove an authorized SSH key | ### Response Codes @@ -42,79 +43,3 @@ All endpoints except `/health` require authentication via an API token passed in ### Request/Response Format All endpoints use JSON for request and response payloads. - -### Bring a network up - -```bash -curl -v -X POST http://localhost:8080/network/up \ - -H "Content-Type: application/json" \ - -H "X-API-Token: 1234567890" \ - -d '{ - "interface": "wlan0", - "ssid": "MyNetworkSSID", - "password": "SuperSecretPassword" - }' -``` - -### Bring a network down - -```bash -curl -v -X POST http://localhost:8080/network/down \ - -H "Content-Type: application/json" \ - -H "X-API-Token: 1234567890" \ - -d '{ - "interface": "wlan0" - }' -``` - -### Remove the stored connection - -```bash -curl -v -X POST http://localhost:8080/network/remove \ - -H "X-API-Token: 1234567890" \ - -d '{ - "interface": "wlan0" - }' -``` - -### Get the hostname - -```bash -curl -v http://localhost:8080/hostname \ - -H "X-API-Token: 1234567890" -``` - -### Set the hostname - -```bash -curl -v -X POST http://localhost:8080/hostname \ - -H "Content-Type: application/json" \ - -H "X-API-Token: 1234567890" \ - -d '{ - "hostname": "MyHostname" - }' -``` - -### Add an authorized SSH key - -```bash -curl -v -X POST http://localhost:8080/authorized-key \ - -H "Content-Type: application/json" \ - -H "X-API-Token: 1234567890" \ - -d '{ - "user": "pi", - "pubkey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1234567890" - }' -``` - -### Remove an authorized SSH key - -```bash -curl -v -X DELETE http://localhost:8080/authorized-key \ - -H "Content-Type: application/json" \ - -H "X-API-Token: 1234567890" \ - -d '{ - "user": "pi", - "pubkey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1234567890" - }' -``` \ No newline at end of file diff --git a/api/rcond.yaml b/api/rcond.yaml index a3fe3bd..a81f2a9 100644 --- a/api/rcond.yaml +++ b/api/rcond.yaml @@ -24,6 +24,7 @@ paths: get: summary: Health check endpoint description: Returns the health status of the service + security: [] responses: '200': description: Service is healthy @@ -36,10 +37,10 @@ paths: type: string example: "healthy" - /network/up: + /network/ap: post: - summary: Create and activate WiFi access point - description: Creates and activates a WiFi access point on the specified interface + summary: Configure WiFi access point + description: Creates a WiFi access point configuration on the specified interface requestBody: required: true content: @@ -65,7 +66,16 @@ paths: example: "SuperSecretPassword" responses: '200': - description: Network interface brought up successfully + description: Access point configured successfully + content: + application/json: + schema: + type: object + properties: + uuid: + type: string + description: UUID of the created connection profile + example: "7d706027-727c-4d4c-a816-f0e1b99db8ab" '400': description: Invalid request payload '401': @@ -73,10 +83,18 @@ paths: '500': description: Internal server error - /network/down: - post: - summary: Deactivate network interface - description: Deactivates the specified network interface + /network/interface/{interface}: + put: + summary: Activate network connection + description: Activates an existing network connection on the specified interface + parameters: + - name: interface + in: path + required: true + schema: + type: string + description: Network interface name + example: "wlan0" requestBody: required: true content: @@ -84,12 +102,32 @@ paths: schema: type: object required: - - interface + - uuid properties: - interface: + uuid: type: string - description: Network interface name - example: "wlan0" + description: UUID of the connection profile + example: "7d706027-727c-4d4c-a816-f0e1b99db8ab" + responses: + '200': + description: Network interface brought up successfully + '400': + description: Invalid request payload + '401': + description: Unauthorized - invalid or missing API token + '500': + description: Internal server error + delete: + summary: Deactivate network connection + description: Deactivates the specified network connection + parameters: + - name: interface + in: path + required: true + schema: + type: string + description: Network interface name + example: "wlan0" responses: '200': description: Network interface brought down successfully @@ -100,13 +138,23 @@ paths: '500': description: Internal server error - /network/remove: - post: + /network/connection/{uuid}: + delete: summary: Remove stored connection profile description: Removes the stored NetworkManager connection profile + parameters: + - name: uuid + in: path + required: true + schema: + type: string + description: UUID of the connection profile to remove + example: "7d706027-727c-4d4c-a816-f0e1b99db8ab" responses: '200': description: Connection profile removed successfully + '400': + description: Invalid request payload '401': description: Unauthorized - invalid or missing API token '500': @@ -154,10 +202,19 @@ paths: description: Unauthorized - invalid or missing API token '500': description: Internal server error - /authorized-key: + + /users/{user}/keys: post: summary: Add SSH authorized key description: Adds an SSH public key to a user's authorized_keys file + parameters: + - name: user + in: path + required: true + schema: + type: string + description: Username to add key for + example: "pi" requestBody: required: true content: @@ -165,13 +222,8 @@ paths: schema: type: object required: - - user - pubkey properties: - user: - type: string - description: Username to add key for - example: "pi" pubkey: type: string description: SSH public key to add @@ -179,36 +231,12 @@ paths: responses: '200': description: Key added successfully - '400': - description: Invalid request payload or SSH key format - '401': - description: Unauthorized - invalid or missing API token - '500': - description: Internal server error - delete: - summary: Remove SSH authorized key - description: Removes an SSH public key from a user's authorized_keys file - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - user - - pubkey - properties: - user: - type: string - description: Username to remove key for - example: "pi" - pubkey: - type: string - description: SSH public key to remove - example: "ssh-rsa AAAAB3NzaC1yc2E... user@host" - responses: - '200': - description: Key removed successfully + content: + text/plain: + schema: + type: string + description: Fingerprint of the added key + example: "SHA256:abcdef1234567890..." '400': description: Invalid request payload or SSH key format '401': @@ -216,3 +244,32 @@ paths: '500': description: Internal server error + /users/{user}/keys/{fingerprint}: + delete: + summary: Remove SSH authorized key + description: Removes an SSH public key from a user's authorized_keys file + parameters: + - name: user + in: path + required: true + schema: + type: string + description: Username to remove key for + example: "pi" + - name: fingerprint + in: path + required: true + schema: + type: string + format: string + description: URL-safe Base64 encoded fingerprint of the key to remove + example: "U0hBMjU2OmFiY2RlZjEyMzQ1Njc4OTAuLi4=" + responses: + '200': + description: Key removed successfully + '400': + description: Invalid request payload or fingerprint + '401': + description: Unauthorized - invalid or missing API token + '500': + description: Internal server error diff --git a/go.mod b/go.mod index cd63a87..acbb638 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ replace github.com/0x1d/rcond/pkg => ./pkg require ( github.com/godbus/dbus/v5 v5.1.0 + github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 golang.org/x/crypto v0.37.0 ) diff --git a/go.sum b/go.sum index 311e408..bee5136 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= diff --git a/pkg/http/handlers.go b/pkg/http/handlers.go index bd5f50c..68b1eb3 100644 --- a/pkg/http/handlers.go +++ b/pkg/http/handlers.go @@ -1,62 +1,107 @@ package http import ( + "encoding/base64" "encoding/json" "log" "net/http" "github.com/0x1d/rcond/pkg/network" "github.com/0x1d/rcond/pkg/user" + "github.com/gorilla/mux" ) const ( NETWORK_CONNECTION_UUID = "7d706027-727c-4d4c-a816-f0e1b99db8ab" ) -func HandleNetworkUp(w http.ResponseWriter, r *http.Request) { +type configureAPRequest struct { + Interface string `json:"interface"` + SSID string `json:"ssid"` + Password string `json:"password"` +} + +type networkUpRequest struct { + UUID string `json:"uuid"` +} + +type setHostnameRequest struct { + Hostname string `json:"hostname"` +} + +type authorizedKeyRequest struct { + User string `json:"user"` + PubKey string `json:"pubkey"` +} + +func HandleConfigureAP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - var req struct { - Interface string `json:"interface"` - SSID string `json:"ssid"` - Password string `json:"password"` - } - + var req configureAPRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - log.Printf("Bringing up network interface %s with SSID %s", req.Interface, req.SSID) - if err := network.Up(req.Interface, req.SSID, req.Password, NETWORK_CONNECTION_UUID); err != nil { - log.Printf("Failed to bring up network interface %s: %v", req.Interface, err) + log.Printf("Configuring access point on interface %s", req.Interface) + uuid, err := network.ConfigureAP(req.Interface, req.SSID, req.Password) + if err != nil { + log.Printf("Failed to configure access point on interface %s: %v", req.Interface, err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - log.Printf("Successfully brought up network interface %s", req.Interface) + log.Printf("Successfully configured access point on interface %s with UUID %s", req.Interface, uuid) + + resp := struct { + UUID string `json:"uuid"` + }{ + UUID: uuid, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(resp); err != nil { + log.Printf("Failed to encode response: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} +func HandleNetworkUp(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req networkUpRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + vars := mux.Vars(r) + iface := vars["interface"] + + log.Printf("Bringing up network interface %s with UUID %s", iface, req.UUID) + if err := network.Up(iface, req.UUID); err != nil { + log.Printf("Failed to bring up network interface %s: %v", iface, err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + log.Printf("Successfully brought up network interface %s", iface) w.WriteHeader(http.StatusOK) } func HandleNetworkDown(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { + if r.Method != http.MethodDelete { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - var req struct { - Interface string `json:"interface"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - if err := network.Down(req.Interface); err != nil { + vars := mux.Vars(r) + iface := vars["interface"] + if err := network.Down(iface); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -65,12 +110,14 @@ func HandleNetworkDown(w http.ResponseWriter, r *http.Request) { } func HandleNetworkRemove(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { + if r.Method != http.MethodDelete { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - if err := network.Remove(NETWORK_CONNECTION_UUID); err != nil { + vars := mux.Vars(r) + uuid := vars["uuid"] + if err := network.Remove(uuid); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -93,10 +140,7 @@ func HandleSetHostname(w http.ResponseWriter, r *http.Request) { return } - var req struct { - Hostname string `json:"hostname"` - } - + var req setHostnameRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -116,22 +160,21 @@ func HandleAddAuthorizedKey(w http.ResponseWriter, r *http.Request) { return } - var req struct { - User string `json:"user"` - PubKey string `json:"pubkey"` - } - + var req authorizedKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } + vars := mux.Vars(r) + username := vars["user"] - if err := user.AddAuthorizedKey(req.User, req.PubKey); err != nil { + fingerprint, err := user.AddAuthorizedKey(username, req.PubKey) + if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.WriteHeader(http.StatusOK) + w.Write([]byte(fingerprint)) } func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) { @@ -140,17 +183,26 @@ func HandleRemoveAuthorizedKey(w http.ResponseWriter, r *http.Request) { return } - var req struct { - User string `json:"user"` - PubKey string `json:"pubkey"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + vars := mux.Vars(r) + fingerprint := vars["fingerprint"] + if fingerprint == "" { + http.Error(w, "fingerprint parameter is required", http.StatusBadRequest) return } - if err := user.RemoveAuthorizedKey(req.User, req.PubKey); err != nil { + username := vars["user"] + if username == "" { + http.Error(w, "user parameter is required", http.StatusBadRequest) + return + } + + fingerprintBytes, err := base64.RawURLEncoding.DecodeString(fingerprint) + if err != nil { + http.Error(w, "invalid fingerprint base64", http.StatusBadRequest) + return + } + + if err := user.RemoveAuthorizedKey(username, string(fingerprintBytes)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/pkg/http/server.go b/pkg/http/server.go index 40a0e71..200cd61 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -53,13 +53,14 @@ func (s *Server) verifyToken(next http.HandlerFunc) http.HandlerFunc { func (s *Server) RegisterRoutes() { s.router.HandleFunc("/health", s.healthHandler).Methods(http.MethodGet) - s.router.HandleFunc("/network/up", s.verifyToken(HandleNetworkUp)).Methods(http.MethodPost) - s.router.HandleFunc("/network/down", s.verifyToken(HandleNetworkDown)).Methods(http.MethodPost) - s.router.HandleFunc("/network/remove", s.verifyToken(HandleNetworkRemove)).Methods(http.MethodPost) + s.router.HandleFunc("/network/ap", s.verifyToken(HandleConfigureAP)).Methods(http.MethodPost) + s.router.HandleFunc("/network/interface/{interface}", s.verifyToken(HandleNetworkUp)).Methods(http.MethodPut) + s.router.HandleFunc("/network/interface/{interface}", s.verifyToken(HandleNetworkDown)).Methods(http.MethodDelete) + s.router.HandleFunc("/network/connection/{uuid}", s.verifyToken(HandleNetworkRemove)).Methods(http.MethodDelete) s.router.HandleFunc("/hostname", s.verifyToken(HandleGetHostname)).Methods(http.MethodGet) s.router.HandleFunc("/hostname", s.verifyToken(HandleSetHostname)).Methods(http.MethodPost) - s.router.HandleFunc("/authorized-key", s.verifyToken(HandleAddAuthorizedKey)).Methods(http.MethodPost) - s.router.HandleFunc("/authorized-key", s.verifyToken(HandleRemoveAuthorizedKey)).Methods(http.MethodDelete) + s.router.HandleFunc("/users/{user}/keys", s.verifyToken(HandleAddAuthorizedKey)).Methods(http.MethodPost) + s.router.HandleFunc("/users/{user}/keys/{fingerprint}", s.verifyToken(HandleRemoveAuthorizedKey)).Methods(http.MethodDelete) } func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/network/nm.go b/pkg/network/nm.go index 95d6e5e..a23b4ad 100644 --- a/pkg/network/nm.go +++ b/pkg/network/nm.go @@ -7,10 +7,7 @@ import ( "time" "github.com/godbus/dbus/v5" -) - -const ( - ourUUID = "7d706027-727c-4d4c-a816-f0e1b99db8ab" + "github.com/google/uuid" ) var ( @@ -18,6 +15,40 @@ var ( defaultPassword = "raspberry" ) +// ConnectionConfig holds the configuration for a NetworkManager connection +type ConnectionConfig struct { + Type string + UUID string + ID string + AutoConnect bool + SSID string + Mode string + Band string + Channel uint32 + KeyMgmt string + PSK string + IPv4Method string + IPv6Method string +} + +// DefaultAPConfig returns a default access point configuration +func DefaultAPConfig(uuid uuid.UUID, ssid string, password string) *ConnectionConfig { + return &ConnectionConfig{ + Type: "802-11-wireless", + UUID: uuid.String(), + ID: ssid, + AutoConnect: true, + SSID: ssid, + Mode: "ap", + Band: "bg", + Channel: 1, + KeyMgmt: "wpa-psk", + PSK: password, + IPv4Method: "shared", + IPv6Method: "ignore", + } +} + // withDbus executes the given function with a D-Bus system connection // and handles any connection errors func withDbus(fn func(*dbus.Conn) error) error { @@ -185,10 +216,18 @@ func GetConnectionPath(conn *dbus.Conn, connUUID string) (dbus.ObjectPath, error } // AddConnection creates a new NetworkManager connection profile for a WiFi access point. -// Takes a D-Bus connection, SSID string, and password string as arguments. +// Takes a D-Bus connection, UUID string, SSID string, and password string as arguments. // Returns the D-Bus object path of the new connection profile. // Returns an error if the connection creation fails. -func AddConnection(conn *dbus.Conn, ssid string, password string) (dbus.ObjectPath, error) { +func AddAccessPointConnection(conn *dbus.Conn, uuid uuid.UUID, ssid string, password string) (dbus.ObjectPath, error) { + return AddConnectionWithConfig(conn, DefaultAPConfig(uuid, ssid, password)) +} + +// AddConnectionWithConfig creates a new NetworkManager connection profile with the given configuration. +// Takes a D-Bus connection and ConnectionConfig struct as arguments. +// Returns the D-Bus object path of the new connection profile. +// Returns an error if the connection creation fails. +func AddConnectionWithConfig(conn *dbus.Conn, cfg *ConnectionConfig) (dbus.ObjectPath, error) { settingsObj := conn.Object( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings", @@ -196,26 +235,26 @@ func AddConnection(conn *dbus.Conn, ssid string, password string) (dbus.ObjectPa settingsMap := map[string]map[string]dbus.Variant{ "connection": { - "type": dbus.MakeVariant("802-11-wireless"), - "uuid": dbus.MakeVariant(ourUUID), - "id": dbus.MakeVariant(ssid), - "autoconnect": dbus.MakeVariant(true), + "type": dbus.MakeVariant(cfg.Type), + "uuid": dbus.MakeVariant(cfg.UUID), + "id": dbus.MakeVariant(cfg.ID), + "autoconnect": dbus.MakeVariant(cfg.AutoConnect), }, "802-11-wireless": { - "ssid": dbus.MakeVariant([]byte(ssid)), - "mode": dbus.MakeVariant("ap"), - "band": dbus.MakeVariant("bg"), - "channel": dbus.MakeVariant(uint32(1)), + "ssid": dbus.MakeVariant([]byte(cfg.SSID)), + "mode": dbus.MakeVariant(cfg.Mode), + "band": dbus.MakeVariant(cfg.Band), + "channel": dbus.MakeVariant(cfg.Channel), }, "802-11-wireless-security": { - "key-mgmt": dbus.MakeVariant("wpa-psk"), - "psk": dbus.MakeVariant(password), + "key-mgmt": dbus.MakeVariant(cfg.KeyMgmt), + "psk": dbus.MakeVariant(cfg.PSK), }, "ipv4": { - "method": dbus.MakeVariant("shared"), + "method": dbus.MakeVariant(cfg.IPv4Method), }, "ipv6": { - "method": dbus.MakeVariant("ignore"), + "method": dbus.MakeVariant(cfg.IPv6Method), }, } @@ -279,13 +318,34 @@ func SetHostname(newHost string) error { }) } -// Up creates and activates a WiFi access point connection. -// It takes the interface name, SSID, password and UUID as arguments. -// If a connection with the given UUID exists, it will be reused. -// Otherwise, a new connection will be created. +// ConfigureAP creates a WiFi access point connection with the specified settings. +// It takes the interface name, SSID and password as arguments. +// A new connection with a generated UUID will be created. +// Returns the UUID of the created connection and any error that occurred. +func ConfigureAP(iface string, ssid string, password string) (string, error) { + uuid := uuid.New() + + err := withDbus(func(conn *dbus.Conn) error { + _, err := AddAccessPointConnection(conn, uuid, ssid, password) + if err != nil { + return fmt.Errorf("failed to create access point connection: %v", err) + } + return nil + }) + + if err != nil { + return "", err + } + + return uuid.String(), nil +} + +// Up activates a connection. +// It takes the interface name and UUID as arguments. +// The connection with the given UUID must exist. // The connection will be activated on the specified interface. // Returns an error if any operation fails. -func Up(iface string, ssid string, password string, uuid string) error { +func Up(iface string, uuid string) error { return withDbus(func(conn *dbus.Conn) error { connPath, err := GetConnectionPath(conn, uuid) if err != nil { @@ -293,10 +353,7 @@ func Up(iface string, ssid string, password string, uuid string) error { } if connPath == "" { - connPath, err = AddConnection(conn, ssid, password) - if err != nil { - return err - } + return fmt.Errorf("connection with UUID %s not found", uuid) } log.Printf("Getting device path for interface %s", iface) diff --git a/pkg/user/ssh.go b/pkg/user/ssh.go index 1e765b1..0d360a3 100644 --- a/pkg/user/ssh.go +++ b/pkg/user/ssh.go @@ -9,18 +9,19 @@ import ( ) // AddAuthorizedKey verifies and adds an SSH public key to /home//.ssh/authorized_keys -// if it doesn't already exist -func AddAuthorizedKey(user string, pubKey string) error { - // Verify the public key format - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) +// if it doesn't already exist. Returns the key's fingerprint. +func AddAuthorizedKey(user string, pubKey string) (string, error) { + // Verify the public key format and get fingerprint + parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) if err != nil { - return fmt.Errorf("invalid SSH public key: %v", err) + return "", fmt.Errorf("invalid SSH public key: %v", err) } + fingerprint := ssh.FingerprintSHA256(parsed) // Ensure .ssh directory exists sshDir := fmt.Sprintf("/home/%s/.ssh", user) if err := os.MkdirAll(sshDir, 0700); err != nil { - return fmt.Errorf("failed to create .ssh directory: %v", err) + return "", fmt.Errorf("failed to create .ssh directory: %v", err) } // Check if key already exists @@ -28,43 +29,45 @@ func AddAuthorizedKey(user string, pubKey string) error { if _, err := os.Stat(keyFile); err == nil { existingKeys, err := os.ReadFile(keyFile) if err != nil { - return fmt.Errorf("failed to read authorized_keys: %v", err) + return "", fmt.Errorf("failed to read authorized_keys: %v", err) } if string(existingKeys) != "" { for _, line := range strings.Split(string(existingKeys), "\n") { - if line == pubKey { - // Key already exists, nothing to do - return nil + if line == "" { + continue + } + parsed, err := ssh.ParsePublicKey([]byte(line)) + if err != nil { + continue + } + if ssh.FingerprintSHA256(parsed) == fingerprint { + // Key already exists, return fingerprint + return fingerprint, nil } } } } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to check authorized_keys: %v", err) + return "", fmt.Errorf("failed to check authorized_keys: %v", err) } // Open authorized_keys file in append mode f, err := os.OpenFile(keyFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { - return fmt.Errorf("failed to open authorized_keys: %v", err) + return "", fmt.Errorf("failed to open authorized_keys: %v", err) } defer f.Close() // Write the public key if _, err := f.WriteString(pubKey + "\n"); err != nil { - return fmt.Errorf("failed to write public key: %v", err) + return "", fmt.Errorf("failed to write public key: %v", err) } - return nil + return fingerprint, nil } // RemoveAuthorizedKey removes an authorized SSH key from /home//.ssh/authorized_keys -func RemoveAuthorizedKey(user string, pubKey string) error { - // Verify the public key format - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey)) - if err != nil { - return fmt.Errorf("invalid SSH public key: %v", err) - } - +// using the key's fingerprint to identify which key to remove +func RemoveAuthorizedKey(user string, fingerprint string) error { // Check if authorized_keys file exists keyFile := fmt.Sprintf("/home/%s/.ssh/authorized_keys", user) if _, err := os.Stat(keyFile); err != nil { @@ -80,10 +83,19 @@ func RemoveAuthorizedKey(user string, pubKey string) error { return fmt.Errorf("failed to read authorized_keys: %v", err) } - // Filter out the key to remove + // Filter out the key with matching fingerprint var newLines []string for _, line := range strings.Split(string(existingKeys), "\n") { - if line != "" && line != pubKey { + if line == "" { + continue + } + parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(line)) + if err != nil { + // Keep lines we can't parse + newLines = append(newLines, line) + continue + } + if ssh.FingerprintSHA256(parsed) != fingerprint { newLines = append(newLines, line) } }