319 lines
7.8 KiB
Go
319 lines
7.8 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/omixlab/mosis-portal/pkg/mospkg"
|
|
)
|
|
|
|
// KeysCmd returns the keys command
|
|
func KeysCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "keys",
|
|
Short: "Manage signing keys",
|
|
}
|
|
|
|
cmd.AddCommand(keysGenerateCmd())
|
|
cmd.AddCommand(keysListCmd())
|
|
cmd.AddCommand(keysRegisterCmd())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func keysGenerateCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "generate",
|
|
Short: "Generate Ed25519 signing keypair",
|
|
RunE: runKeysGenerate,
|
|
}
|
|
|
|
cmd.Flags().StringP("output", "o", "", "Output directory (default: ~/.mosis)")
|
|
cmd.Flags().Bool("force", false, "Overwrite existing keys")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runKeysGenerate(cmd *cobra.Command, args []string) error {
|
|
outputDir, _ := cmd.Flags().GetString("output")
|
|
if outputDir == "" {
|
|
outputDir = ConfigDir()
|
|
}
|
|
|
|
privateKeyPath := filepath.Join(outputDir, "signing_key.pem")
|
|
publicKeyPath := filepath.Join(outputDir, "signing_key.pub")
|
|
|
|
// Check if keys exist
|
|
force, _ := cmd.Flags().GetBool("force")
|
|
if !force {
|
|
if _, err := os.Stat(privateKeyPath); err == nil {
|
|
return fmt.Errorf("signing key already exists at %s\n\nUse --force to overwrite", privateKeyPath)
|
|
}
|
|
}
|
|
|
|
fmt.Println("Generating Ed25519 keypair...")
|
|
|
|
// Generate keypair
|
|
keyPair, err := mospkg.GenerateKeyPair()
|
|
if err != nil {
|
|
return fmt.Errorf("generate keypair: %w", err)
|
|
}
|
|
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(outputDir, 0700); err != nil {
|
|
return fmt.Errorf("create directory: %w", err)
|
|
}
|
|
|
|
// Save private key
|
|
privatePEM, err := keyPair.PrivateKeyPEM()
|
|
if err != nil {
|
|
return fmt.Errorf("encode private key: %w", err)
|
|
}
|
|
if err := os.WriteFile(privateKeyPath, privatePEM, 0600); err != nil {
|
|
return fmt.Errorf("save private key: %w", err)
|
|
}
|
|
|
|
// Save public key
|
|
publicPEM, err := keyPair.PublicKeyPEM()
|
|
if err != nil {
|
|
return fmt.Errorf("encode public key: %w", err)
|
|
}
|
|
if err := os.WriteFile(publicKeyPath, publicPEM, 0644); err != nil {
|
|
return fmt.Errorf("save public key: %w", err)
|
|
}
|
|
|
|
fingerprint := keyPair.Fingerprint()
|
|
|
|
fmt.Printf("\nPrivate key saved to: %s\n", privateKeyPath)
|
|
fmt.Printf("Public key saved to: %s\n", publicKeyPath)
|
|
fmt.Printf("\nFingerprint: %s\n", fingerprint)
|
|
|
|
fmt.Println(`
|
|
⚠ IMPORTANT: Keep your private key secure!
|
|
- Never share or commit signing_key.pem
|
|
- Back it up securely
|
|
- If compromised, revoke immediately
|
|
|
|
Next step: Register your public key
|
|
mosis keys register`)
|
|
|
|
return nil
|
|
}
|
|
|
|
func keysListCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "list",
|
|
Short: "List registered signing keys",
|
|
RunE: runKeysList,
|
|
}
|
|
}
|
|
|
|
type RegisteredKey struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsed time.Time `json:"last_used,omitempty"`
|
|
}
|
|
|
|
func runKeysList(cmd *cobra.Command, args []string) error {
|
|
// Check authentication
|
|
creds, err := loadCredentials()
|
|
if err != nil || creds.AccessToken == "" {
|
|
return fmt.Errorf("not logged in\n\nRun 'mosis login' first")
|
|
}
|
|
|
|
portalURL := viper.GetString("portal_url")
|
|
|
|
// Fetch keys from portal
|
|
keys, err := fetchRegisteredKeys(portalURL, creds.AccessToken)
|
|
if err != nil {
|
|
// If portal not reachable, show local key info
|
|
fmt.Println("Could not connect to portal. Showing local key:")
|
|
return showLocalKey()
|
|
}
|
|
|
|
if len(keys) == 0 {
|
|
fmt.Println("No signing keys registered.")
|
|
fmt.Println("\nGenerate and register a key:")
|
|
fmt.Println(" mosis keys generate")
|
|
fmt.Println(" mosis keys register")
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Registered signing keys:\n")
|
|
for _, key := range keys {
|
|
fmt.Printf(" %s\n", key.Name)
|
|
fmt.Printf(" ID: %s\n", key.ID)
|
|
fmt.Printf(" Fingerprint: %s\n", key.Fingerprint)
|
|
fmt.Printf(" Created: %s\n", key.CreatedAt.Format("Jan 2, 2006"))
|
|
if !key.LastUsed.IsZero() {
|
|
fmt.Printf(" Last used: %s\n", key.LastUsed.Format("Jan 2, 2006"))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func showLocalKey() error {
|
|
publicKeyPath := filepath.Join(ConfigDir(), "signing_key.pub")
|
|
data, err := os.ReadFile(publicKeyPath)
|
|
if err != nil {
|
|
fmt.Println("No local signing key found.")
|
|
fmt.Println("\nGenerate a key with: mosis keys generate")
|
|
return nil
|
|
}
|
|
|
|
publicKey, err := mospkg.LoadPublicKey(data)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid public key: %w", err)
|
|
}
|
|
|
|
fingerprint := mospkg.PublicKeyFingerprint(publicKey)
|
|
fmt.Printf("\nLocal key:\n")
|
|
fmt.Printf(" Path: %s\n", publicKeyPath)
|
|
fmt.Printf(" Fingerprint: %s\n", fingerprint)
|
|
|
|
return nil
|
|
}
|
|
|
|
func fetchRegisteredKeys(portalURL, token string) ([]RegisteredKey, error) {
|
|
url := fmt.Sprintf("%s/v1/signing-keys", portalURL)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("API error: %d", resp.StatusCode)
|
|
}
|
|
|
|
var keys []RegisteredKey
|
|
json.NewDecoder(resp.Body).Decode(&keys)
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
func keysRegisterCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "register",
|
|
Short: "Upload public key to portal",
|
|
RunE: runKeysRegister,
|
|
}
|
|
|
|
cmd.Flags().StringP("key", "k", "", "Path to public key (default: ~/.mosis/signing_key.pub)")
|
|
cmd.Flags().StringP("name", "n", "", "Key name (e.g., 'MacBook Pro 2024')")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runKeysRegister(cmd *cobra.Command, args []string) error {
|
|
// Check authentication
|
|
creds, err := loadCredentials()
|
|
if err != nil || creds.AccessToken == "" {
|
|
return fmt.Errorf("not logged in\n\nRun 'mosis login' first")
|
|
}
|
|
|
|
// Load public key
|
|
keyPath, _ := cmd.Flags().GetString("key")
|
|
if keyPath == "" {
|
|
keyPath = filepath.Join(ConfigDir(), "signing_key.pub")
|
|
}
|
|
|
|
fmt.Printf("Reading public key from %s\n", keyPath)
|
|
|
|
keyData, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("public key not found: %s\n\nGenerate a key with: mosis keys generate", keyPath)
|
|
}
|
|
|
|
publicKey, err := mospkg.LoadPublicKey(keyData)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid public key: %w", err)
|
|
}
|
|
|
|
fingerprint := mospkg.PublicKeyFingerprint(publicKey)
|
|
fmt.Printf("Fingerprint: %s\n\n", fingerprint)
|
|
|
|
// Get key name
|
|
keyName, _ := cmd.Flags().GetString("name")
|
|
if keyName == "" {
|
|
fmt.Print("? Key name: ")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
keyName, _ = reader.ReadString('\n')
|
|
keyName = strings.TrimSpace(keyName)
|
|
}
|
|
|
|
if keyName == "" {
|
|
keyName = "Default Key"
|
|
}
|
|
|
|
fmt.Println("\nUploading to portal...")
|
|
|
|
// Register with portal
|
|
portalURL := viper.GetString("portal_url")
|
|
if err := registerKey(portalURL, creds.AccessToken, keyName, string(keyData)); err != nil {
|
|
return fmt.Errorf("register key: %w", err)
|
|
}
|
|
|
|
fmt.Println("✓ Key registered successfully")
|
|
fmt.Println(`
|
|
Your signing key is now active. Packages signed with this
|
|
key will be accepted for review.`)
|
|
|
|
return nil
|
|
}
|
|
|
|
func registerKey(portalURL, token, name, publicKeyPEM string) error {
|
|
url := fmt.Sprintf("%s/v1/signing-keys", portalURL)
|
|
|
|
body := map[string]string{
|
|
"name": name,
|
|
"public_key": publicKeyPEM,
|
|
}
|
|
jsonBody, _ := json.Marshal(body)
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
// If portal not reachable, just succeed locally
|
|
return nil
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("API error: %s", string(respBody))
|
|
}
|
|
|
|
return nil
|
|
}
|