add developer CLI tool with Cobra for app workflow
This commit is contained in:
318
portal/cmd/mosis/cmd/keys.go
Normal file
318
portal/cmd/mosis/cmd/keys.go
Normal file
@@ -0,0 +1,318 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user