From c1dbcefc0640bf73a6466ce8d21be452acbd572a Mon Sep 17 00:00:00 2001 From: "N. Goncalves" Date: Fri, 19 Jul 2024 15:53:22 +0100 Subject: [PATCH 1/6] Add build stage before release job --- .github/workflows/ci.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c18c24e..540f154 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,8 +54,30 @@ jobs: go mod tidy make test + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21.9' + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Lint + run: | + go mod tidy + make build + release: - needs: [lint, test ] + needs: [lint, test, build ] if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest steps: From cdd05b84dd091b5617d7b96f82962e7377a12b49 Mon Sep 17 00:00:00 2001 From: "N. Goncalves" Date: Tue, 23 Jul 2024 18:20:16 +0100 Subject: [PATCH 2/6] Move to hcl config system. Context command improvements. Add vault operator functionality with yubikeys. --- .github/workflows/ci.yml | 6 +- Makefile | 13 +- auth/auth.go | 69 ----- cmd/generate_root.go | 73 +++++ cmd/get.go | 40 +++ cmd/list.go | 66 +++++ cmd/login.go | 40 +++ cmd/root.go | 150 +++++++++++ cmd/show.go | 77 ++++++ cmd/unseal.go | 69 +++++ cmd/vault.go | 20 -- config/config.go | 61 ----- contexts/config.go | 49 ---- contexts/context.go | 108 -------- go.mod | 47 +++- go.sum | 127 +++++++-- main.go | 11 +- pkg/crypto/crypto.go | 117 ++++++++ pkg/secrets/kv.go | 140 ++++++++++ pkg/utils/utils.go | 164 +++++++++++ pkg/vault/auth.go | 82 ++++++ pkg/vault/operator.go | 198 ++++++++++++++ pkg/vault/vault.go | 61 +++++ pkg/yubikey/yubikey.go | 125 +++++++++ pkg/yubikeypgp/yubikeypgp.go | 223 +++++++++++++++ pkg/yubikeyscard/apdu.go | 125 +++++++++ pkg/yubikeyscard/data_object.go | 180 +++++++++++++ pkg/yubikeyscard/iso7816.go | 141 ++++++++++ pkg/yubikeyscard/yubikeyscard.go | 448 +++++++++++++++++++++++++++++++ 29 files changed, 2667 insertions(+), 363 deletions(-) delete mode 100644 auth/auth.go create mode 100644 cmd/generate_root.go create mode 100644 cmd/get.go create mode 100644 cmd/list.go create mode 100644 cmd/login.go create mode 100644 cmd/root.go create mode 100644 cmd/show.go create mode 100644 cmd/unseal.go delete mode 100644 cmd/vault.go delete mode 100644 config/config.go delete mode 100644 contexts/config.go delete mode 100644 contexts/context.go create mode 100644 pkg/crypto/crypto.go create mode 100644 pkg/secrets/kv.go create mode 100644 pkg/utils/utils.go create mode 100644 pkg/vault/auth.go create mode 100644 pkg/vault/operator.go create mode 100644 pkg/vault/vault.go create mode 100644 pkg/yubikey/yubikey.go create mode 100644 pkg/yubikeypgp/yubikeypgp.go create mode 100644 pkg/yubikeyscard/apdu.go create mode 100644 pkg/yubikeyscard/data_object.go create mode 100644 pkg/yubikeyscard/iso7816.go create mode 100644 pkg/yubikeyscard/yubikeyscard.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 540f154..fd813f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./bin/zeusctl-linux-amd64 - asset_name: zeusctl-linux-amd64 + asset_name: zeusctl-${{ github.ref }}-linux-amd64 asset_content_type: application/octet-stream - name: Upload Darwin AMD64 Asset uses: actions/upload-release-asset@v1 @@ -116,7 +116,7 @@ jobs: with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./bin/zeusctl-darwin-amd64 - asset_name: zeusctl-darwin-amd64 + asset_name: zeusctl-${{ github.ref }}-darwin-amd64 asset_content_type: application/octet-stream - name: Upload Darwin ARM64 Asset uses: actions/upload-release-asset@v1 @@ -124,6 +124,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./bin/zeusctl-darwin-arm64 + asset_path: ./bin/zeusctl-${{ github.ref }}-darwin-arm64 asset_name: zeusctl-darwin-arm64 asset_content_type: application/octet-stream diff --git a/Makefile b/Makefile index 6986f83..d7baab8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: fmt lint test build -TARGETS := linux darwin-amd64 darwin-arm64 +TARGETS := linux-amd64 linux-arm64 darwin-amd64 darwin-arm64 default: all @@ -15,13 +15,16 @@ test: build: $(TARGETS) -linux: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o bin/zeusctl-linux-amd64 ./main.go +linux-amd64: + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o bin/zeusctl-linux-amd64 ./main.go + +linux-arm64: + CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -o bin/zeusctl-linux-arm64 ./main.go darwin-amd64: - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -a -installsuffix cgo -o bin/zeusctl-darwin-amd64 ./main.go + CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/zeusctl-darwin-amd64 ./main.go darwin-arm64: - CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -a -installsuffix cgo -o bin/zeusctl-darwin-arm64 ./main.go + CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o bin/zeusctl-darwin-arm64 ./main.go all: fmt lint test build diff --git a/auth/auth.go b/auth/auth.go deleted file mode 100644 index 4e46ae3..0000000 --- a/auth/auth.go +++ /dev/null @@ -1,69 +0,0 @@ -package auth - -import ( - "fmt" - "os" - - "github.com/hashicorp/vault/api" - "github.com/spf13/cobra" - - "github.com/CorefluxCommunity/zeusctl/config" -) - -var ( - host string - caPath string - user string - password string -) - -func NewAuthCommand() *cobra.Command { - authCmd := &cobra.Command{ - Use: "auth", - Short: "Authenticate with Vault", - Run: authenticate, - } - - authCmd.Flags().StringVar(&host, "host", "", "Vault host URL") - authCmd.Flags().StringVar(&caPath, "ca-path", "", "Path to CA certificate") - authCmd.Flags().StringVar(&user, "user", "", "Username for Vault login") - authCmd.Flags().StringVar(&password, "password", "", "Password for Vault login") - - return authCmd -} - -func authenticate(cmd *cobra.Command, args []string) { - apiConfig := &api.Config{ - Address: host, - } - - if caPath != "" { - apiConfig.ConfigureTLS(&api.TLSConfig{CAPath: caPath}) - } - - client, err := api.NewClient(apiConfig) - if err != nil { - fmt.Fprintf(os.Stderr, "Error creating Vault client: %s\n", err) - os.Exit(1) - } - - options := map[string]interface{}{ - "password": password, - } - path := fmt.Sprintf("auth/userpass/login/%s", user) - - secret, err := client.Logical().Write(path, options) - if err != nil { - fmt.Fprintf(os.Stderr, "Error logging in to Vault: %s\n", err) - os.Exit(1) - } - - // Save the configuration to a file - err = config.SaveConfig(secret.Auth.ClientToken, host, caPath) - if err != nil { - fmt.Fprintf(os.Stderr, "Error saving configuration: %s\n", err) - os.Exit(1) - } - - fmt.Println("Logged in successfully. Configuration saved.") -} diff --git a/cmd/generate_root.go b/cmd/generate_root.go new file mode 100644 index 0000000..3e8ab4f --- /dev/null +++ b/cmd/generate_root.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/zeusctl/pkg/vault" +) + +func init() { + generateRootServerSubCmd.Flags().StringVarP(&vaultGenerateRootNonce, "nonce", "n", "", "nonce for root token generation") + generateRootClusterSubCmd.Flags().StringVarP(&vaultGenerateRootNonce, "nonce", "n", "", "nonce for root token generation") + + generateRootCmd.AddCommand(generateRootServerSubCmd) + generateRootCmd.AddCommand(generateRootClusterSubCmd) + + rootCmd.AddCommand(generateRootCmd) +} + +var generateRootCmd = &cobra.Command{ + Use: "generate-root", + Short: "Generate Vault root token", + Long: `Decrypt the unseal key and generate root token for Vault cluster.`, +} + +var generateRootServerSubCmd = &cobra.Command{ + Use: "server -n ", + Short: "Generate root token for Vault cluster", + Long: `Decrypt the unseal key and generate Vault root token.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + vaultAddr := args[0] + keyPath := args[1] + + keys, err := utils.ReadKeyFile(keyPath) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + if err := vault.GenerateRoot(vaultAddr, keys); err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} + +var generateRootClusterSubCmd = &cobra.Command{ + Use: "cluster -n ", + Short: "Generate root token for Vault cluster", + Long: `Decrypt the unseal key and generate Vault root token.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + clusterName := args[0] + + vaultAddr, err := getVaultAddress(clusterName) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + cluster, err := getVaultClusterConfig(clusterName) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + keys, err := cluster.keyring() + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + if err := vault.GenerateRoot(vaultAddr, utils.Unique(keys)); err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} diff --git a/cmd/get.go b/cmd/get.go new file mode 100644 index 0000000..1127164 --- /dev/null +++ b/cmd/get.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "github.com/CorefluxCommunity/zeusctl/pkg/secrets" + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/spf13/cobra" +) + +func init() { + getSecretsSubCmd.Flags().StringVarP(&contextFile, "context", "c", "", "secrets context file") + getSecretsSubCmd.Flags().BoolVarP(&exportSecrets, "export", "e", false, "export secrets as environment variables") + + getCmd.AddCommand(getSecretsSubCmd) + + rootCmd.AddCommand(getCmd) +} + +var getCmd = &cobra.Command{ + Use: "get", + Short: "Get Vault secrets", + Long: `Get Vault secrets from a secrets context file.`, +} + +var getSecretsSubCmd = &cobra.Command{ + Use: "secrets ", + Short: "Get Vault secrets", + Long: `Get Vault secrets from a secrets context file.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + vaultAddr, err := getVaultAddress(args[0]) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + contextName := args[1] + + if err := secrets.GetSecrets(contextName, contextFile, exportSecrets, vaultAddr); err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..d86b207 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "fmt" + + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/zeusctl/pkg/yubikey" + "github.com/spf13/cobra" +) + +func init() { + listCmd.AddCommand(listClustersSubCmd) + listCmd.AddCommand(listYubiKeysSubCmd) + + rootCmd.AddCommand(listCmd) +} + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List connected YubiKeys and configured Vault clusters", + Long: `List connected YubiKeys and configured Vault clusters.`, +} + +var listClustersSubCmd = &cobra.Command{ + Use: "clusters", + Short: "List Vault clusters", + Long: `List Vault clusters in Zeus configuration file.`, + Run: func(cmd *cobra.Command, args []string) { + i := 0 + for name, cluster := range config.Clusters { + keys, err := cluster[0].keyring() + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + utils.PrintHeader(name) + utils.PrintKVSlice("Server(s)", cluster[0].Servers) + + uniqKeys := utils.Unique(keys) + if len(keys) != len(uniqKeys) { + dupCount := len(keys) - len(uniqKeys) + utils.PrintKV("Key(s)", fmt.Sprintf("%d (%d duplicates)", len(keys), dupCount)) + } else { + utils.PrintKV("Key(s)", fmt.Sprintf("%d", len(keys))) + } + + if i < len(config.Clusters)-1 { + fmt.Println() + } + + i++ + } + }, +} + +var listYubiKeysSubCmd = &cobra.Command{ + Use: "yubikeys", + Short: "List connected YubiKeys", + Long: `List overview of connected YubiKeys.`, + Run: func(cmd *cobra.Command, args []string) { + err := yubikey.ListYubiKeys() + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..1c77e68 --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/zeusctl/pkg/vault" + "github.com/spf13/cobra" +) + +func init() { + loginClusterSubCmd.Flags().StringVarP(&method, "method", "m", "", "Authentication method for Vault") + loginClusterSubCmd.Flags().StringVarP(&user, "user", "u", "", "Username for Vault login") + loginClusterSubCmd.Flags().StringVarP(&password, "password", "p", "", "Password for Vault login") + + loginCmd.AddCommand(loginClusterSubCmd) + + rootCmd.AddCommand(loginCmd) +} + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Login to Vault", + Long: `Login to Vault using the userpass auth method.`, +} + +var loginClusterSubCmd = &cobra.Command{ + Use: "cluster ", + Short: "Login to Vault", + Long: `Login to Vault using the userpass auth method.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + vaultAddr, err := getVaultAddress(args[0]) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + if err := vault.Authenticate(vaultAddr, method, user, password); err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..4f72043 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,150 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/CorefluxCommunity/zeusctl/pkg/utils" +) + +var ( + // CLI config + config ZeusConfig + configFile string + + // Commands + // generate-root + vaultGenerateRootNonce string + + // login + method string // TODO: Create enum for auth methods + user string + password string + + // get + contextFile string + exportSecrets bool + + rootCmd = &cobra.Command{ + Use: "zeusctl", + } +) + +type ZeusConfig struct { + Clusters map[string][]*VaultClusterConfig `hcl:"cluster" mapstructure:"cluster"` +} + +type VaultClusterConfig struct { + Address string `hcl:"address" mapstructure:"address"` + Servers []string `hcl:"servers" mapstructure:"servers"` + Keys []string `hcl:"keys" mapstructure:"keys"` + KeyFile string `hcl:"key_file" mapstructure:"key_file"` +} + +// Execute executes the root command. +func Execute() error { + return rootCmd.Execute() +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.zeusctl/zeusctl.hcl)") +} + +func initConfig() { + if configFile != "" { + // Use config file from the flag. + viper.SetConfigFile(configFile) + } else { + // Find home directory. + home, err := homedir.Dir() + cobra.CheckErr(err) + + // Search config in $HOME/zeusctl directory with name "zeusctl" (without extension). + // TODO: Get cli config directory from shell env + viper.AddConfigPath(home + "/.zeusctl") + viper.SetConfigName("zeusctl") + viper.SetConfigType("hcl") + } + + viper.AutomaticEnv() + viper.ReadInConfig() + + err := viper.Unmarshal(&config) + if err != nil { + utils.PrintFatal(fmt.Sprintf("unable to decode into struct, %v", err), 1) + } + + // get zeusctl config direction and set as cwd + configDir, err := getConfigDir() + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + os.Chdir(configDir) +} + +func getVaultClusterConfig(clusterName string) (*VaultClusterConfig, error) { + for name, cluster := range config.Clusters { + if name == clusterName { + return cluster[0], nil + } + } + + return nil, fmt.Errorf("config for Vault cluster '%s' not found", clusterName) +} + +func getVaultAddress(clusterName string) (string, error) { + // Check if the cluster exists in the config + cluster, ok := config.Clusters[clusterName] + if !ok { + return "", fmt.Errorf("cluster '%s' not found in configuration", clusterName) + } + + // Use the first config in the slice (assuming there's only one per cluster name) + clusterConfig := cluster[0] + + // If address is provided, use it + if clusterConfig.Address != "" { + return clusterConfig.Address, nil + } + + // If address is not provided, but servers are, use the first server + if len(clusterConfig.Servers) > 0 { + return clusterConfig.Servers[0], nil + } + + // If neither address nor servers are provided, return an error + return "", fmt.Errorf("no address or servers found for cluster '%s'", clusterName) +} + +func (vc *VaultClusterConfig) keyring() ([]string, error) { + keys := vc.Keys + if vc.KeyFile != "" { + kf, err := utils.ReadKeyFile(vc.KeyFile) + if err != nil { + return nil, err + } + + keys = append(keys, kf...) + } + + return keys, nil +} + +func getConfigDir() (string, error) { + home, err := homedir.Dir() + if err != nil { + return "", err + } + + // TODO: Get cli config directory from shell env + path := home + "/.zeusctl" + + return path, nil +} diff --git a/cmd/show.go b/cmd/show.go new file mode 100644 index 0000000..a26b2ff --- /dev/null +++ b/cmd/show.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "fmt" + + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/zeusctl/pkg/vault" + "github.com/CorefluxCommunity/zeusctl/pkg/yubikey" + "github.com/spf13/cobra" +) + +func init() { + showCmd.AddCommand(showClusterSubCmd) + showCmd.AddCommand(showYubiKeySubCmd) + + rootCmd.AddCommand(showCmd) +} + +var showCmd = &cobra.Command{ + Use: "show", + Short: "Show details of YubiKeys and Vault clusters", + Long: `Show details of YubiKeys and Vault clusters.`, +} + +var showClusterSubCmd = &cobra.Command{ + Use: "cluster ", + Short: "Show Vault cluster status", + Long: `Show overview and unseal status of the specified Vault cluster.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + clusterName := args[0] + + cluster, err := getVaultClusterConfig(clusterName) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + keys, err := cluster.keyring() + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + if len(cluster.Servers) == 0 { + utils.PrintFatal("no Vault servers in configuration", 1) + } + + utils.PrintHeader("Vault Cluster Status") + utils.PrintKVSlice("Server(s)", cluster.Servers) + + uniqKeys := utils.Unique(keys) + if len(keys) != len(uniqKeys) { + dupCount := len(keys) - len(uniqKeys) + utils.PrintKV("Key(s)", fmt.Sprintf("%d (%d duplicates)", len(keys), dupCount)) + } else { + utils.PrintKV("Key(s)", fmt.Sprintf("%d", len(keys))) + } + + if err := vault.ListVaultStatus(cluster.Servers[0]); err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} + +var showYubiKeySubCmd = &cobra.Command{ + Use: "yubikey ", + Short: "Show YubiKey details", + Long: `Show YubiKey details returned from OpenPGP application data objects.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + sn := args[0] + + err := yubikey.ShowYubiKey(sn) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} diff --git a/cmd/unseal.go b/cmd/unseal.go new file mode 100644 index 0000000..a8a4dfa --- /dev/null +++ b/cmd/unseal.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/zeusctl/pkg/vault" +) + +func init() { + unsealCmd.AddCommand(unsealServerSubCmd) + unsealCmd.AddCommand(unsealClusterSubCmd) + + rootCmd.AddCommand(unsealCmd) +} + +var unsealCmd = &cobra.Command{ + Use: "unseal", + Short: "Unseal Vault by server or cluster", + Long: `Decrypt PGP-encrypted unseal key and unseal Vault.`, +} + +var unsealServerSubCmd = &cobra.Command{ + Use: "server ", + Short: "Unseal Vault server", + Long: `Decrypt unseal key and unseal single Vault server.`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + vaultAddr := args[0] + keyPath := args[1] + + keys, err := utils.ReadKeyFile(keyPath) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + if err := vault.Unseal([]string{vaultAddr}, keys); err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} + +var unsealClusterSubCmd = &cobra.Command{ + Use: "cluster ", + Short: "Unseal Vault cluster", + Long: `Decrypt unseal key and unseal Vault cluster.`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + clusterName := args[0] + + cluster, err := getVaultClusterConfig(clusterName) + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + keys, err := cluster.keyring() + if err != nil { + utils.PrintFatal(err.Error(), 1) + } + + if len(cluster.Servers) == 0 { + utils.PrintFatal("no Vault servers in configuration", 1) + } + + if err := vault.Unseal(cluster.Servers, utils.Unique(keys)); err != nil { + utils.PrintFatal(err.Error(), 1) + } + }, +} diff --git a/cmd/vault.go b/cmd/vault.go deleted file mode 100644 index f08774b..0000000 --- a/cmd/vault.go +++ /dev/null @@ -1,20 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - - "github.com/CorefluxCommunity/zeusctl/auth" - "github.com/CorefluxCommunity/zeusctl/contexts" -) - -func InitVaultCommands(rootCmd *cobra.Command) { - vaultCmd := &cobra.Command{ - Use: "vault", - Short: "Commands related to Vault", - } - - vaultCmd.AddCommand(auth.NewAuthCommand()) - vaultCmd.AddCommand(contexts.NewContextCommand()) - - rootCmd.AddCommand(vaultCmd) -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index ee4de5f..0000000 --- a/config/config.go +++ /dev/null @@ -1,61 +0,0 @@ -package config - -import ( - "encoding/json" - "os" - "path/filepath" -) - -type VaultConfig struct { - Token string `json:"token"` - Host string `json:"host"` - CAPath string `json:"caPath"` -} - -func SaveConfig(token, host, caPath string) error { - homeDir, err := os.UserHomeDir() - if err != nil { - return err - } - - configFile := filepath.Join(homeDir, ".zeusctl", "vault.json") - - err = os.MkdirAll(filepath.Dir(configFile), 0700) - if err != nil { - return err - } - - config := VaultConfig{ - Token: token, - Host: host, - CAPath: caPath, - } - - data, err := json.Marshal(config) - if err != nil { - return err - } - - return os.WriteFile(configFile, data, 0600) -} - -func LoadConfig() (*VaultConfig, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, err - } - - configFile := filepath.Join(homeDir, ".zeusctl", "vault.json") - - data, err := os.ReadFile(configFile) - if err != nil { - return nil, err - } - - var config VaultConfig - if err = json.Unmarshal(data, &config); err != nil { - return nil, err - } - - return &config, nil -} diff --git a/contexts/config.go b/contexts/config.go deleted file mode 100644 index 6ef3558..0000000 --- a/contexts/config.go +++ /dev/null @@ -1,49 +0,0 @@ -package contexts - -import ( - "encoding/json" - "os" - "path/filepath" -) - -type SecretInfo struct { - Path string `json:"path"` - Key string `json:"key"` - DecodeBase64 bool `json:"decodeBase64"` - EncodeBase64 bool `json:"encodeBase64"` -} - -type Context struct { - Name string `json:"name"` - Secrets map[string]SecretInfo `json:"secrets"` -} - -type Config struct { - Contexts []Context `json:"contexts"` -} - -func LoadContexts(path string) (*Config, error) { - if path == "" { - // If no path provided, use the default location - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, err - } - path = filepath.Join(homeDir, ".zeusctl", "contexts.json") - } - - file, err := os.Open(path) - if err != nil { - return nil, err - } - defer file.Close() - - var config Config - - decoder := json.NewDecoder(file) - if err := decoder.Decode(&config); err != nil { - return nil, err - } - - return &config, nil -} diff --git a/contexts/context.go b/contexts/context.go deleted file mode 100644 index a01087f..0000000 --- a/contexts/context.go +++ /dev/null @@ -1,108 +0,0 @@ -package contexts - -import ( - "encoding/base64" - "fmt" - "os" - - "github.com/hashicorp/vault/api" - "github.com/spf13/cobra" - - "github.com/CorefluxCommunity/zeusctl/config" -) - -var configFilePath string - -func NewContextCommand() *cobra.Command { - contextCmd := &cobra.Command{ - Use: "context [name]", - Short: "Load a named context and export its secrets as environment variables", - Args: cobra.ExactArgs(1), - Run: loadContext, - } - - contextCmd.Flags().StringVarP(&configFilePath, "config", "c", "", "Path to the contexts configuration file") - - return contextCmd -} - -func loadContext(cmd *cobra.Command, args []string) { - vaultConfig, err := config.LoadConfig() - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading configuration: %s\n", err) - os.Exit(1) - } - - clientConfig := &api.Config{ - Address: vaultConfig.Host, - } - if vaultConfig.CAPath != "" { - clientConfig.ConfigureTLS(&api.TLSConfig{CAPath: vaultConfig.CAPath}) - } - - client, err := api.NewClient(clientConfig) - if err != nil { - fmt.Fprintf(os.Stderr, "Error creating Vault client: %s\n", err) - os.Exit(1) - } - client.SetToken(vaultConfig.Token) - - contextName := args[0] - contexts, err := LoadContexts(configFilePath) - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading contexts: %s\n", err) - os.Exit(1) - } - - found := false - for _, ctx := range contexts.Contexts { - if ctx.Name == contextName { - found = true - for envName, secretInfo := range ctx.Secrets { - secret, err := client.Logical().Read(secretInfo.Path) - if err != nil { - fmt.Printf("Error reading secret from %s: %s\n", secretInfo.Path, err) - continue - - } - - if secret == nil || secret.Data == nil { - fmt.Printf("No data found at path: %s\n", secretInfo.Path) - continue - } - - value, ok := secret.Data["data"].(map[string]interface{})[secretInfo.Key] - if !ok { - - fmt.Printf("Secret key %s not found at path: %s\n", secretInfo.Key, secretInfo.Path) - - continue - } - - var finalValue string - if secretInfo.DecodeBase64 { - decodedBytes, decodeErr := base64.StdEncoding.DecodeString(fmt.Sprintf("%v", value)) - if decodeErr != nil { - fmt.Printf("Error decoding base64 value for %s: %s\n", envName, decodeErr) - continue - } - finalValue = string(decodedBytes) - } else { - finalValue = fmt.Sprintf("%v", value) - } - if secretInfo.EncodeBase64 { - finalValue = base64.StdEncoding.EncodeToString([]byte(finalValue)) - } - - // Output export command instead of setting directly - fmt.Printf("export %s='%s'\n", envName, finalValue) - } - - break - } - } - - if !found { - fmt.Fprintf(os.Stderr, "Context '%s' not found\n", contextName) - } -} diff --git a/go.mod b/go.mod index 6e4336f..253c3ca 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,60 @@ module github.com/CorefluxCommunity/zeusctl -go 1.21.9 +go 1.22 require ( - github.com/hashicorp/vault/api v1.13.0 - github.com/spf13/cobra v1.8.0 + github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 + github.com/hashicorp/hcl/v2 v2.21.0 + github.com/hashicorp/vault/api v1.14.0 + github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/mitchellh/go-homedir v1.1.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + golang.org/x/crypto v0.25.0 + golang.org/x/term v0.22.0 ) require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-retryablehttp v0.7.6 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.19.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index daf3d6b..bf1d575 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,45 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 h1:HYAhfGa9dEemCZgGZWL5AvVsctBCsHxl2CI0HUXzHQE= +github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7/go.mod h1:BkYEeWL6FbT4Ek+TcOBnPzEKnL7kOq2g19tTQXkorHY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= -github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM= +github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= @@ -40,10 +51,20 @@ github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0S github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.13.0 h1:RTCGpE2Rgkn9jyPcFlc7YmNocomda44k5ck8FKMH41Y= -github.com/hashicorp/vault/api v1.13.0/go.mod h1:0cb/uZUv1w2cVu9DIvuW1SMlXXC6qtATJt+LXJRx+kg= +github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= +github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/vault/api v1.14.0 h1:Ah3CFLixD5jmjusOgm8grfN9M0d+Y8fVR2SW0K6pJLU= +github.com/hashicorp/vault/api v1.14.0/go.mod h1:pV9YLxBGSz+cItFDd8Ii4G17waWOQ32zVjMWHe/cOqk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -53,38 +74,86 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index b430cd2..689bc26 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,9 @@ package main import ( - "github.com/spf13/cobra" - "github.com/CorefluxCommunity/zeusctl/cmd" ) func main() { - var rootCmd = &cobra.Command{ - Use: "zeusctl", - Short: "zeusctl is a CLI tool for operating Hashicorp Vault", - } - - cmd.InitVaultCommands(rootCmd) - - rootCmd.Execute() + cmd.Execute() } diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go new file mode 100644 index 0000000..ba7a7df --- /dev/null +++ b/pkg/crypto/crypto.go @@ -0,0 +1,117 @@ +package crypto + +import ( + "encoding/base64" + "errors" + "fmt" + "syscall" + + "golang.org/x/term" + + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/zeusctl/pkg/yubikeypgp" + "github.com/CorefluxCommunity/zeusctl/pkg/yubikeyscard" +) + +const ( + unsealKeyLengthMin int = 16 + unsealKeyLengthMax int = 33 +) + +// decryptUnsealKeys wraps decryptUnsealKey to decrypt a slice of unseal keys +// and provide console messages. +func DecryptUnsealKeys(encryptedKeys []string) ([]string, error) { + yks := new(yubikeyscard.YubiKeys) + if err := yks.Connect(); err != nil { + return nil, err + } + + defer yks.Disconnect() + + var keys []string + for _, ek := range encryptedKeys { + key, err := decryptUnsealKey(yks, ek) + if err != nil { + utils.PrintError(err.Error()) + } else { + keys = append(keys, key) + } + } + + if len(keys) == 0 { + return nil, errors.New("no Vault unseal keys found, cannot proceed with unseal operation") + } + + msg := fmt.Sprintf("decrypted %d Vault unseal key(s)", len(keys)) + utils.PrintSuccess(msg) + + return keys, nil +} + +// decryptUnsealKey performs a base64 decode, then decrypts a PGP-encrypted +// Vault unseal key. +func decryptUnsealKey(yks *yubikeyscard.YubiKeys, cipherTxtB64 string) (unsealKey string, err error) { + encryptedKey, err := base64.StdEncoding.DecodeString(cipherTxtB64) + if err != nil { + err = errors.New("encrypted unseal key is not base64 encoded") + return + } + + retries := 1 + for retries > 0 { + md, retries, err := yubikeypgp.ReadMessage(yks, encryptedKey, promptPIN) + if err != nil { + switch { + case retries == 0: + utils.PrintFatal("PIN bank locked, no retries remaining", 1) + case retries < 0: + return "", err + default: + utils.PrintWarning(err.Error()) + continue + } + } + + serial := md.YubiKey.AppRelatedData.AID.Serial + utils.PrintInfo(fmt.Sprintf("decrypted unseal key with key ID %X found on YubiKey %x", md.DecryptedWith, serial)) + + unsealKey = string(md.Body) + break + } + + // unsealKey is a byte slice of unicode characters, divide length by 2 to get raw byte length + n := len(unsealKey) / 2 + if n < unsealKeyLengthMin { + err = fmt.Errorf("unseal key length is shorter than minimum %d bytes", unsealKeyLengthMin) + return + } + if n > unsealKeyLengthMax { + err = fmt.Errorf("unseal key length is longer than maximum %d bytes", unsealKeyLengthMax) + return + } + + return +} + +// promptPin will read a PIN from an interactive terminal. +func promptPIN() ([]byte, error) { + fmt.Print("\U0001F513 Enter YubiKey OpenPGP PIN: ") + p, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return []byte{}, err + } + + fmt.Println() + + if len(p) < 6 || len(p) > 127 { + return []byte{}, errors.New("expected PIN length of 6-127 characters") + } + + for i := range p { + if p[i] < 0x30 || p[i] > 0x39 { + return []byte{}, errors.New("only digits 0-9 are valid PIN characters") + } + } + + return p, nil +} diff --git a/pkg/secrets/kv.go b/pkg/secrets/kv.go new file mode 100644 index 0000000..e95239e --- /dev/null +++ b/pkg/secrets/kv.go @@ -0,0 +1,140 @@ +package secrets + +import ( + "encoding/base64" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/hcl/v2/hclsimple" + + "github.com/CorefluxCommunity/zeusctl/pkg/vault" +) + +type ContextConfig struct { + Contexts []*Context `hcl:"context,block"` +} + +type Context struct { + Name string `hcl:"name,label"` + Secrets []*Secret `hcl:"secret,block"` +} + +type Secret struct { + Name string `hcl:"name,label"` + Path string `hcl:"path"` + Keys []*Key `hcl:"key,block"` +} + +type Key struct { + Name string `hcl:"name,label"` + ExportName string `hcl:"export_name,optional"` + Base64Decode bool `hcl:"base64_decode,optional"` +} + +func GetSecrets(contextName string, contextFile string, exportSecrets bool, vaultAddr string) error { + if contextFile == "" { + currentDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting current directory: %w", err) + } + contextFile = filepath.Join(currentDir, "contexts.hcl") + } + + if _, err := os.Stat(contextFile); os.IsNotExist(err) { + return fmt.Errorf("contexts file not found: %s", contextFile) + } + + content, err := os.ReadFile(contextFile) + if err != nil { + return fmt.Errorf("error reading contexts file: %w", err) + } + + var contexts ContextConfig + err = hclsimple.Decode(contextFile, content, nil, &contexts) + if err != nil { + return fmt.Errorf("error decoding contexts file: %w", err) + } + + var targetContext *Context + for _, context := range contexts.Contexts { + if context.Name == contextName { + targetContext = context + break + } + } + + if targetContext == nil { + return fmt.Errorf("context '%s' not found in contexts file", contextName) + } + + // Check for saved token + token, err := getSavedToken() + if err != nil || token == "" { + return fmt.Errorf("no valid token found, please authenticate first") + } + + // Create Vault client + vaultClient, err := vault.NewVaultClient(vaultAddr) + if err != nil { + return fmt.Errorf("failed to create Vault client: %w", err) + } + vaultClient.ApiClient.SetToken(token) + + for _, secret := range targetContext.Secrets { + secretData, err := vaultClient.FetchSecret(secret.Path) + if err != nil { + if strings.Contains(err.Error(), "permission denied") { + fmt.Printf("Warning: Not authorized to read secret: %s\n", secret.Path) + continue + } + return fmt.Errorf("failed to fetch secret %s: %w", secret.Path, err) + } + + for _, key := range secret.Keys { + value, ok := secretData[key.Name].(string) + if !ok { + fmt.Printf("Warning: Key %s not found in secret %s\n", key.Name, secret.Path) + continue + } + + if key.Base64Decode { + decodedValue, err := base64.StdEncoding.DecodeString(value) + if err != nil { + fmt.Printf("Warning: Failed to base64 decode %s: %v\n", key.Name, err) + } else { + value = string(decodedValue) + } + } + + exportName := key.Name + if key.ExportName != "" { + exportName = key.ExportName + } + + if exportSecrets { + fmt.Printf("export %s='%s'\n", exportName, value) + } else { + fmt.Printf("%s=%s\n", exportName, value) + } + } + } + + return nil +} + +func getSavedToken() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + + tokenFile := filepath.Join(homeDir, ".zeusctl", "token") + tokenBytes, err := os.ReadFile(tokenFile) + if err != nil { + return "", err + } + + return strings.TrimSpace(string(tokenBytes)), nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..6807228 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,164 @@ +package utils + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/logrusorgru/aurora" +) + +const keyFileSizeMax int64 = 8192 + +const ( + printKVPadWidth int = 30 + printHeaderPadWidth int = 10 +) + +// ReadFile will read an unseal key file from the provided path and return a +// slice of strings containing base64-encoded PGP-encrypted Vault unseal keys. +func ReadKeyFile(path string) ([]string, error) { + buf, err := readFile(path, keyFileSizeMax) + if err != nil { + return nil, err + } + + return strings.Split(strings.TrimSpace(string(buf)), "\n"), nil +} + +// readFile will read a file from the provided path up to the byte length +// limit provided. +func readFile(path string, maxBytes int64) ([]byte, error) { + var buf []byte + + file, err := os.Open(path) + if err != nil { + return nil, err + } + + defer file.Close() + + fileStat, err := file.Stat() + if err != nil { + return nil, err + } + + if fileStat.Size() > maxBytes { + return nil, fmt.Errorf("unseal key is larger that the maximum file size of %d bytes", maxBytes) + } + + buf, err = io.ReadAll(file) + if err != nil { + return nil, err + } + + return buf, nil +} + +// PrintKV will bold print the key followed by padding to the specified +// total width, then the value. +func PrintKV(key string, value string) { + pad := strings.Repeat(".", printKVPadWidth-len(key)-2) + label := aurora.Bold(fmt.Sprintf("%s %s:", key, pad)) + fmt.Println(label, value) +} + +// PrintKV will bold print the key followed by padding to the specified +// total width, then the first value in the slice. If additional values +// are present in the slice they will be displayed on new line indented +// to match the previous value. +func PrintKVSlice(key string, values []string) { + for i, value := range values { + if i == 0 { + pad := strings.Repeat(".", printKVPadWidth-len(key)-2) + label := aurora.Bold(fmt.Sprintf("%s %s:", key, pad)) + fmt.Println(label, value) + } else { + pad := strings.Repeat(" ", printKVPadWidth) + fmt.Println(pad, value) + } + } +} + +// PrintHeader will print a bolded header label. +func PrintHeader(label string) { + pad := strings.Repeat("=", printHeaderPadWidth) + fmt.Println(aurora.Bold(fmt.Sprintf("%s %s %s", pad, label, pad))) +} + +// PrintInfo will print a formatted info message to stdout. +func PrintInfo(msg string) { + fmt.Println(aurora.Blue(aurora.Bold("[info] ")), msg) +} + +// PrintSuccess will print a formatted success message to stdout. +func PrintSuccess(msg string) { + fmt.Println(aurora.Green(aurora.Bold("[success]")), msg) +} + +// PrintWarning will print a formatted warning message to stdout. +func PrintWarning(msg string) { + fmt.Println(aurora.Yellow(aurora.Bold("[warning]")), msg) +} + +// PrintError will print a formatted error message to stdout. +func PrintError(msg string) { + fmt.Println(aurora.Red(aurora.Bold("[error] ")), msg) +} + +// PrintFatal will print a formatted error message to stdout and exit with +// the provided status. +func PrintFatal(msg string, code int) { + fmt.Println(aurora.Red(aurora.Bold("[fatal] ")), msg) + os.Exit(code) +} + +// Unique is a function that removes duplicate strings from a slice of strings +// and returns the deduplicated slice. +func Unique(orig []string) []string { + var dedup []string + + for _, s := range orig { + present := false + + for _, d := range dedup { + if s == d { + present = true + break + } + } + + if !present { + dedup = append(dedup, s) + } + } + + return dedup +} + +// fmtFingerprint accepts a byte array containing a PGP fingerprint and +// returns a formatted string that displays the fingerprint in 2-byte +// hexadecimal blocks. +func fmtFingerprint(fp [20]byte) string { + var fpString string + + for i := 0; i < len(fp); i += 2 { + fpString = strings.ToUpper(fmt.Sprintf(fpString+"%x ", fp[i:i+2])) + } + + return strings.TrimSpace(fpString[:24] + " " + fpString[24:]) +} + +// fmtFingerprintTerse accepts a byte array containing a PGP fingerprint and +// returns a short form formatted string that displays the last 8 bytes of the +// fingerprint in 2-byte hexadecimal blocks. +func FmtFingerprintTerse(fp [20]byte) string { + var fpString string + + for i := 12; i < len(fp); i += 2 { + fpString = strings.ToUpper(fmt.Sprintf(fpString+"%x", fp[i:i+2])) + } + + return fpString +} diff --git a/pkg/vault/auth.go b/pkg/vault/auth.go new file mode 100644 index 0000000..7490a21 --- /dev/null +++ b/pkg/vault/auth.go @@ -0,0 +1,82 @@ +package vault + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/vault/api" +) + +type AuthMethod interface { + Authenticate(client *VaultClient) (*api.Secret, error) +} + +type UserPassAuth struct { + Username string + Password string +} + +func (u *UserPassAuth) Authenticate(client *VaultClient) (*api.Secret, error) { + return client.ApiClient.Logical().Write("auth/userpass/login/"+u.Username, map[string]interface{}{ + "password": u.Password, + }) +} + +func Authenticate(vaultAddr, method string, user string, password string) error { + vault, err := NewVaultClient(vaultAddr) + if err != nil { + return fmt.Errorf("failed to create Vault client: %w", err) + } + + var authMethod AuthMethod + + switch method { + case "userpass": + authMethod = &UserPassAuth{ + Username: user, + Password: password, + } + // Add more cases here for future auth methods + default: + return fmt.Errorf("unsupported auth method: %s", method) + } + + secret, err := authMethod.Authenticate(vault) + if err != nil { + return fmt.Errorf("authentication failed: %w", err) + } + + if secret == nil || secret.Auth == nil { + return fmt.Errorf("no auth info returned") + } + + // Store the token in the CLI's home config directory + err = storeToken(secret.Auth.ClientToken) + if err != nil { + return fmt.Errorf("failed to store token: %w", err) + } + + return nil +} + +func storeToken(token string) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get user home directory: %w", err) + } + + configDir := filepath.Join(homeDir, ".zeusctl") + err = os.MkdirAll(configDir, 0700) + if err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + tokenFile := filepath.Join(configDir, "token") + err = os.WriteFile(tokenFile, []byte(token), 0600) + if err != nil { + return fmt.Errorf("failed to write token file: %w", err) + } + + return nil +} diff --git a/pkg/vault/operator.go b/pkg/vault/operator.go new file mode 100644 index 0000000..373e463 --- /dev/null +++ b/pkg/vault/operator.go @@ -0,0 +1,198 @@ +package vault + +import ( + "fmt" + + "github.com/CorefluxCommunity/zeusctl/pkg/crypto" + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/hashicorp/vault/api" +) + +// connect to Vault server and execute unseal operation +func (vault *VaultClient) unseal(keys []string) (*api.SealStatusResponse, error) { + resp, err := vault.ApiClient.Sys().SealStatus() + if err != nil { + return nil, err + } + + if !resp.Initialized { + return resp, fmt.Errorf("%s - Vault server is not initialized", vault.url.Host) + } + + // if node is already unsealed, skip it + if !resp.Sealed { + utils.PrintSuccess(vault.url.Host + " - already unsealed, skipping unseal operation") + return resp, nil + } + + for _, key := range keys { + resp, err = vault.ApiClient.Sys().Unseal(key) + if err != nil { + return nil, err + } + + if !resp.Sealed { + break + } + } + + utils.PrintInfo(fmt.Sprintf("%s - provided %d unseal key share(s) toward unseal progress", vault.url.Host, len(keys))) + + resp, err = vault.ApiClient.Sys().SealStatus() + if err != nil { + return nil, err + } + + if !resp.Sealed { + utils.PrintSuccess(fmt.Sprintf("%s - Vault unsealed", vault.url.Host)) + } + + return resp, nil +} + +// connect to Vault server and execute unseal operation +func (vault *VaultClient) generateRoot(keys []string) (*api.GenerateRootStatusResponse, error) { + resp, err := vault.ApiClient.Sys().GenerateRootStatus() + if err != nil { + return nil, err + } + + // if node is already unsealed, skip it + if !resp.Started { + utils.PrintWarning(vault.url.Host + " - root token generation process has not been started") + return resp, nil + } + + nonce := resp.Nonce + for _, key := range keys { + resp, err = vault.ApiClient.Sys().GenerateRootUpdate(key, nonce) + if err != nil { + return nil, err + } + + msg := fmt.Sprintf("%s - provided unseal key share, root token generation progress: %d of %d key shares", + vault.url.Host, resp.Progress, resp.Required) + utils.PrintInfo(msg) + + if resp.Complete { + msg = fmt.Sprintf("%s - root token generation complete", vault.url.Host) + utils.PrintSuccess(msg) + + return resp, nil + } + } + + return resp, nil +} + +func printSealStatus(resp *api.SealStatusResponse) { + status := "unsealed" + if resp.Sealed { + status = "sealed" + } else { + utils.PrintKV("Cluster name", resp.ClusterName) + utils.PrintKV("Cluster ID", resp.ClusterID) + } + + utils.PrintKV("Seal status", status) + utils.PrintKV("Key threshold/shares", fmt.Sprintf("%d/%d", resp.T, resp.N)) + utils.PrintKV("Progress", fmt.Sprintf("%d/%d", resp.Progress, resp.T)) + utils.PrintKV("Version", resp.Version) +} + +func printGenRootStatus(resp *api.GenerateRootStatusResponse) { + status := "not started" + if resp.Started { + status = "started" + + if resp.Complete { + status = "complete" + } + } + + utils.PrintKV("Root generation", status) + + if resp.Started { + utils.PrintKV("Nonce", resp.Nonce) + utils.PrintKV("Progress", fmt.Sprintf("%d/%d", resp.Progress, resp.Required)) + + if resp.PGPFingerprint != "" { + utils.PrintKV("PGP fingerprint", resp.PGPFingerprint) + } + } + + if resp.EncodedRootToken != "" { + utils.PrintKV("Encoded root token", resp.EncodedRootToken) + } +} + +// Unseal will decrypt the provided unseal key(s) and unseal each of the +// provided Vault cluster nodes. +func Unseal(vaultAddrs []string, encryptedKeys []string) error { + keys, err := crypto.DecryptUnsealKeys(encryptedKeys) + if err != nil { + return err + } + + for i, addr := range vaultAddrs { + vault, err := NewVaultClient(addr) + if err != nil { + return err + } + + resp, err := vault.unseal(keys) + if err != nil { + return err + } + + if i == len(vaultAddrs)-1 { + fmt.Println() + utils.PrintHeader("Vault Cluster Status") + printSealStatus(resp) + } + } + + return nil +} + +// GenerateRoot will decrypt the provided unseal key and enter the key share +// to progress the root generation attempt. +func GenerateRoot(vaultAddr string, encryptedKeys []string) error { + keys, err := crypto.DecryptUnsealKeys(encryptedKeys) + if err != nil { + return err + } + + vault, err := NewVaultClient(vaultAddr) + if err != nil { + return err + } + + resp, err := vault.generateRoot(keys) + if err != nil { + return err + } + + fmt.Println() + utils.PrintHeader("Root Token Generation Status") + printGenRootStatus(resp) + + return nil +} + +// ListVaultStatus will output of the status the provided Vault address. +func ListVaultStatus(vaultAddr string) error { + vault, err := NewVaultClient(vaultAddr) + if err != nil { + return err + } + + resp, err := vault.ApiClient.Sys().SealStatus() + if err != nil { + return err + } + + printSealStatus(resp) + + return nil +} diff --git a/pkg/vault/vault.go b/pkg/vault/vault.go new file mode 100644 index 0000000..21ac311 --- /dev/null +++ b/pkg/vault/vault.go @@ -0,0 +1,61 @@ +package vault + +import ( + "fmt" + "net/url" + + "github.com/hashicorp/vault/api" +) + +type VaultClient struct { + ApiClient *api.Client + url *url.URL +} + +func NewVaultClient(addr string) (*VaultClient, error) { + vault := new(VaultClient) + + config := &api.Config{ + Address: addr, + } + + api, err := api.NewClient(config) + if err != nil { + return vault, err + } + + // Disable namespace usage + api.SetNamespace("") + + url, err := url.Parse(addr) + if err != nil { + return vault, err + } + + vault.ApiClient = api + vault.url = url + + return vault, nil +} + +func (v *VaultClient) FetchSecret(path string) (map[string]interface{}, error) { + secret, err := v.ApiClient.Logical().Read(path) + if err != nil { + return nil, err + } + if secret == nil { + return nil, fmt.Errorf("secret not found") + } + + // Check if this is a KV2 secret + if data, ok := secret.Data["data"].(map[string]interface{}); ok { + return data, nil + } + + // If it's not a KV2 secret, return the data as is + if secret.Data != nil { + return secret.Data, nil + } + + return nil, fmt.Errorf("unexpected secret data format") +} diff --git a/pkg/yubikey/yubikey.go b/pkg/yubikey/yubikey.go new file mode 100644 index 0000000..e6e84ff --- /dev/null +++ b/pkg/yubikey/yubikey.go @@ -0,0 +1,125 @@ +package yubikey + +import ( + "encoding/binary" + "fmt" + "strings" + "time" + + "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/zeusctl/pkg/yubikeyscard" +) + +// ListYubiKeys will output the basic details of connected YubiKeys. +func ListYubiKeys() error { + // connect YubiKey smart card interface, disconnect on return + yks := new(yubikeyscard.YubiKeys) + if err := yks.Connect(); err != nil { + return err + } + + defer yks.Disconnect() + + for i, yk := range yks.YubiKeys { + ard := yk.AppRelatedData + crd := yk.CardRelatedData + + utils.PrintHeader(fmt.Sprint(i+1, ": ", yk.ReaderLabel)) + utils.PrintKV("Manufacturer", "Yubico") + utils.PrintKV("Serial number", fmt.Sprintf("%x", ard.AID.Serial)) + + if crd.Name != nil { + utils.PrintKV("Name of cardholder", strings.Replace(fmt.Sprintf("%s", crd.Name), "<<", " ", -1)) + } + + utils.PrintKV("Signature key", fmt.Sprintf("rsa%d/%s", + binary.BigEndian.Uint16(ard.AlgoAttrSign.RSAModLen[:]), + utils.FmtFingerprintTerse(ard.Fingerprints.Sign))) + utils.PrintKV("Encryption key", fmt.Sprintf("rsa%d/%s", + binary.BigEndian.Uint16(ard.AlgoAttrEnc.RSAModLen[:]), + utils.FmtFingerprintTerse(ard.Fingerprints.Enc))) + utils.PrintKV("Authentication key", fmt.Sprintf("rsa%d/%s", + binary.BigEndian.Uint16(ard.AlgoAttrAuth.RSAModLen[:]), + utils.FmtFingerprintTerse(ard.Fingerprints.Auth))) + + if i < len(yks.YubiKeys)-1 { + fmt.Println() + } + } + + return nil +} + +// ShowYubiKey will search the connected YubiKeys for the specified serial +// number and output the details including smart card and application-related +// data. +func ShowYubiKey(sn string) error { + // connect YubiKey smart card interface, disconnect on return + yks := new(yubikeyscard.YubiKeys) + if err := yks.Connect(); err != nil { + return err + } + + defer yks.Disconnect() + + yk := yks.FindBySN(sn) + if yk == nil { + return fmt.Errorf("could not locate YubiKey that supports OpenPGP with serial number '%s'", sn) + } + + ard := yk.AppRelatedData + crd := yk.CardRelatedData + + utils.PrintHeader("YubiKey Status") + + utils.PrintKV("Reader", yk.ReaderLabel) + utils.PrintKV("Application ID", fmt.Sprintf("%x%x%x%x%x%x", + ard.AID.RID, ard.AID.App, ard.AID.Version, + ard.AID.Manufacturer, ard.AID.Serial, ard.AID.RFU)) + utils.PrintKV("Application type", "OpenPGP") + utils.PrintKV("Version", fmt.Sprintf("%d.%d", ard.AID.Version[0], ard.AID.Version[1])) + utils.PrintKV("Manufacturer", "Yubico") + utils.PrintKV("Serial number", fmt.Sprintf("%x", ard.AID.Serial)) + utils.PrintKV("Name of cardholder", strings.Replace(fmt.Sprintf("%s", crd.Name), "<<", " ", -1)) + utils.PrintKV("Language prefs", string(crd.LanguagePrefs)) + + switch crd.Salutation { + case 0x30: + utils.PrintKV("Pronoun", "unspecified") + case 0x31: + utils.PrintKV("Pronoun", "he") + case 0x32: + utils.PrintKV("Pronoun", "she") + case 0x39: + utils.PrintKV("Pronoun", "they") + } + + utils.PrintKV("Max. PIN lengths", fmt.Sprintf("%d %d %d", + ard.PWStatus.PW1MaxLenFmt, + ard.PWStatus.PW1MaxLenRC, + ard.PWStatus.PW3MaxLenFmt)) + utils.PrintKV("PIN retry counter", fmt.Sprintf("%d %d %d", + ard.PWStatus.PW1RetryCtr, + ard.PWStatus.PW1RCRetryCtr, + ard.PWStatus.PW3RetryCtr)) + + utils.PrintKV("Signature key", utils.FmtFingerprintTerse(ard.Fingerprints.Sign)) + utils.PrintKV(" algorithm", fmt.Sprintf("rsa%d", + binary.BigEndian.Uint16(ard.AlgoAttrSign.RSAModLen[:]))) + signGenDate := int64(binary.BigEndian.Uint32(ard.KeyGenDates.Sign[:])) + utils.PrintKV(" created", time.Unix(signGenDate, 0).String()) + + utils.PrintKV("Encryption key", utils.FmtFingerprintTerse(ard.Fingerprints.Enc)) + utils.PrintKV(" algorithm", fmt.Sprintf("rsa%d", + binary.BigEndian.Uint16(ard.AlgoAttrEnc.RSAModLen[:]))) + encGenDate := int64(binary.BigEndian.Uint32(ard.KeyGenDates.Enc[:])) + utils.PrintKV(" created", time.Unix(encGenDate, 0).String()) + + utils.PrintKV("Authentication key", utils.FmtFingerprintTerse(ard.Fingerprints.Auth)) + utils.PrintKV(" algorithm", fmt.Sprintf("rsa%d", + binary.BigEndian.Uint16(ard.AlgoAttrAuth.RSAModLen[:]))) + authGenDate := int64(binary.BigEndian.Uint32(ard.KeyGenDates.Auth[:])) + utils.PrintKV(" created", time.Unix(authGenDate, 0).String()) + + return nil +} diff --git a/pkg/yubikeypgp/yubikeypgp.go b/pkg/yubikeypgp/yubikeypgp.go new file mode 100644 index 0000000..6a45462 --- /dev/null +++ b/pkg/yubikeypgp/yubikeypgp.go @@ -0,0 +1,223 @@ +package yubikeypgp + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + "golang.org/x/crypto/openpgp/packet" + + "github.com/CorefluxCommunity/zeusctl/pkg/yubikeyscard" +) + +const ( + sessionKeyLength = 16 + encryptedKeyPacketHeaderLength = 3 + encryptedKeyPacketKeyInfoLength = 12 + symmetricallyEncryptedVersion = 1 +) + +type PinPromptFunction func() ([]byte, error) + +// MessageDetails contains the result of parsing an OpenPGP encrypted and/or +// signed message. +type MessageDetails struct { + IsEncrypted bool // true if the message was encrypted. + DecryptedWith uint64 // key ID of decryption key used to decrypt session key + YubiKey *yubikeyscard.YubiKey // YubiKey containing private key used to decrypt session key + Body []byte // the contents of the message. +} + +type encryptedKeyPacket struct { + tag uint8 + length int + version uint8 + keyID uint64 + keyAlgo uint8 + keySize uint16 + encryptedBytes []byte +} + +// ReadMessage will decrypt a PGP-encrypted message by using a YubiKey to first +// obtain the session key (DEK). Decrypt will then decrypt the symmetrically +// encrypted portion of the message and return the resultant plain text. +// In the event of an incorrect PIN, Decrypt will return an empty byte array +// and the number of remaining PIN retries. +func ReadMessage(yks *yubikeyscard.YubiKeys, msg []byte, prompt PinPromptFunction) (md *MessageDetails, retries int, err error) { + md = new(MessageDetails) + retries = -1 + + // read encrypted key packet fields and deserialize to struct + ek, err := readEncKeyPacket(bytes.NewReader(msg)) + if err != nil { + return + } + + md.IsEncrypted = true + + // locate YubiKey with matching decryption key + yk := yks.FindByKeyID(ek.keyID) + if yk == nil { + err = fmt.Errorf("decryption key %X could not be found on any YubiKeys", ek.keyID) + return + } + + // check if PIN is cached, if not retrieve PIN input from user, then validate format + pin := yk.CachedPIN(2) + + if pin == nil { + pin, err = prompt() + if err != nil { + return + } + } + + // verify the PIN (bank 2) with the OpenPGP smart card applet + retries, err = yubikeyscard.Verify(yk.Card, 2, pin) + if err != nil { + return + } else { + // add verified PIN to the cache + yk.SetCachedPIN(2, pin) + } + + // decipher the session key + sk, err := yubikeyscard.Decipher(yk.Card, ek.encryptedBytes) + if err != nil { + return + } + + if len(sk) != (sessionKeyLength + 3) { + err = errors.New("unable to decipher PGP session key") + return + } + + // get cipher function from first octect + c := packet.CipherFunction(sk[0]) + if c != packet.CipherAES128 { + err = errors.New("unsupported cipher function, only AES-128-CFB supported") + return + } + + // after cipher function, the next 16 bytes contain the session key + sessionKey := sk[1 : sessionKeyLength+1] + + // read the message from the symmetrically encrypted packet using session key + md.Body, err = readSymEncPacket(bytes.NewReader(msg[ek.length:]), sessionKey, c) + if err != nil { + return + } + + md.DecryptedWith = ek.keyID + md.YubiKey = yk + + return +} + +func readHeader(r io.Reader) (tag uint8, length int, contents io.Reader, err error) { + var buf [3]byte + + _, err = io.ReadFull(r, buf[:]) + if err != nil { + return + } + + if buf[0]&0xc0 != 0xc0 { + err = errors.New("invalid PGP packet header, only new format supported") + return + } + + if buf[1] < 192 && buf[1] > 223 { + err = errors.New("invalid PGP packet length, expected two-octect length format") + return + } + + tag = buf[0] & 0x1f + length = int(binary.BigEndian.Uint16([]byte{buf[1] - 192, buf[2]})+192) + 3 + contents = r + return tag, length, contents, nil +} + +func readEncKeyPacket(r io.Reader) (ek encryptedKeyPacket, err error) { + var buf [encryptedKeyPacketKeyInfoLength]byte + + tag, length, contents, err := readHeader(r) + if err != nil { + return + } + + if tag != 1 { + err = errors.New("invalid PGP packet type, only encrypted key and symmetrically encrypted packets supported") + return + } + + n, err := io.ReadFull(contents, buf[:]) + if err != nil { + return + } + + if n != encryptedKeyPacketKeyInfoLength { + err = errors.New("invalid PGP packet, body too short") + return + } + + if buf[0] != 3 { + err = errors.New("invalid PGP encrypted key packet, only version 3 supported") + return + } + + if buf[9] != uint8(packet.PubKeyAlgoRSA) { + err = errors.New("invalid PGP encrypted key packet, only RSA supported") + return + } + + ek.tag = tag + ek.length = length + ek.version = buf[0] + ek.keyID = binary.BigEndian.Uint64(buf[1:9]) + ek.keyAlgo = buf[9] + ek.keySize = binary.BigEndian.Uint16(buf[10:12]) + + ek.encryptedBytes = make([]byte, length-(encryptedKeyPacketHeaderLength+encryptedKeyPacketKeyInfoLength)) + if _, err = io.ReadFull(contents, ek.encryptedBytes); err != nil { + return + } + + return ek, nil +} + +// readSymEncPacket decrypts the symmetrically encypted portion of the message +// with the provided session key and cipher function. +func readSymEncPacket(r io.Reader, key []byte, cipherFunc packet.CipherFunction) ([]byte, error) { + packets := packet.NewReader(r) + + for { + p, err := packets.Next() + if err != nil { + return nil, err + } + + switch p := p.(type) { + case *packet.SymmetricallyEncrypted: + decrypted, err := p.Decrypt(cipherFunc, key) + if err != nil { + return nil, err + } + + if err := packets.Push(decrypted); err != nil { + return nil, err + } + case *packet.LiteralData: + body, err := io.ReadAll(p.Body) + if err != nil { + return nil, err + } + + return body, nil + default: + return nil, errors.New("unexpected PGP packet type encountered") + } + } +} diff --git a/pkg/yubikeyscard/apdu.go b/pkg/yubikeyscard/apdu.go new file mode 100644 index 0000000..eeba0c8 --- /dev/null +++ b/pkg/yubikeyscard/apdu.go @@ -0,0 +1,125 @@ +package yubikeyscard + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/ebfe/scard" +) + +var appID = []byte{0xd2, 0x76, 0x00, 0x01, 0x24, 0x01} // OpenPGP applet ID + +// commandAPDU represents an application data unit sent to a smartcard. +type commandAPDU struct { + cla, ins, p1, p2 uint8 // Class, Instruction, Parameter 1, Parameter 2 + data []byte // Command data + le uint8 // Command data length + pib bool // Padding indicator byte present + elf bool // Use extended length fields +} + +// responseAPDU represents an application data unit received from a smart card +type responseAPDU struct { + data []byte // response data + sw1, sw2 uint8 // status words 1 and 2 +} + +// serialize serializes a command APDU. +func (ca commandAPDU) serialize() ([]byte, error) { + buf := new(bytes.Buffer) + + // write 4 header bytes to buffer + if _, err := buf.Write([]byte{ca.cla, ca.ins, ca.p1, ca.p2}); err != nil { + return nil, err + } + + // if a payload exists, calculate the length, prepend it to the payload, and write to buffer + if len(ca.data) > 0 { + lc := len(ca.data) + + // subtract one byte from length if padding indicator byte present + if ca.pib { + lc-- + } + + // check if extended length fields (3 bytes) should be used + if ca.elf { + lcElf := make([]byte, 2) + binary.BigEndian.PutUint16(lcElf, uint16(lc)) + + if _, err := buf.Write(append([]byte{0}, lcElf...)); err != nil { + return nil, err + } + } else { + if _, err := buf.Write([]byte{uint8(lc)}); err != nil { + return nil, err + } + } + + if _, err := buf.Write(ca.data); err != nil { + return nil, err + } + } + + if _, err := buf.Write([]byte{ca.le}); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// transmit will send the serialized APDU command to the applet. +func (ca commandAPDU) transmit(card *scard.Card) (responseAPDU, error) { + ra := new(responseAPDU) + + cmd, err := ca.serialize() + if err != nil { + return *ra, err + } + + rsp, err := card.Transmit(cmd) + if err != nil { + return *ra, err + } + + if err = ra.deserialize(rsp); err != nil { + return *ra, err + } + + return *ra, nil +} + +// deserialize deserializes a response APDU. +func (ra *responseAPDU) deserialize(data []byte) error { + if len(data) < 2 { + return fmt.Errorf("can not deserialize data: payload too short (%d < 2)", len(data)) + } + + r := bytes.NewReader(data) + + ra.data = make([]byte, len(data)-2) + _, err := io.ReadFull(r, ra.data) + if err != nil { + return err + } + + sw := make([]byte, 2) + _, err = io.ReadFull(r, sw) + if err != nil { + return err + } + + ra.sw1 = sw[0] + ra.sw2 = sw[1] + + return nil +} + +func (ra *responseAPDU) success() bool { + success := []byte{0x90, 0x00} + status := []byte{ra.sw1, ra.sw2} + + return bytes.Equal(status, success) +} diff --git a/pkg/yubikeyscard/data_object.go b/pkg/yubikeyscard/data_object.go new file mode 100644 index 0000000..2516b89 --- /dev/null +++ b/pkg/yubikeyscard/data_object.go @@ -0,0 +1,180 @@ +package yubikeyscard + +import ( + "bytes" + "encoding/binary" +) + +const ( + MaxResponseLength uint16 = 256 +) + +type DataObject struct { + tag uint16 + constructed bool + parent uint16 + binary bool + extLen uint8 + desc string +} + +var doURL = DataObject{tag: 0x5F50, constructed: false, parent: 0, binary: false, extLen: 2, desc: "URL"} +var doHistBytes = DataObject{tag: 0x5F52, constructed: false, parent: 0, binary: true, extLen: 0, desc: "Historical Bytes"} +var doCardRelData = DataObject{tag: 0x0065, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Cardholder Related Data"} +var doName = DataObject{tag: 0x005B, constructed: false, parent: 0x65, binary: false, extLen: 0, desc: "Name"} +var doLangPrefs = DataObject{tag: 0x5F2D, constructed: false, parent: 0x65, binary: false, extLen: 0, desc: "Language preferences"} +var doSalutation = DataObject{tag: 0x5F35, constructed: false, parent: 0x65, binary: false, extLen: 0, desc: "Salutation"} +var doAppRelData = DataObject{tag: 0x006E, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Application Related Data"} +var doLoginData = DataObject{tag: 0x005E, constructed: false, parent: 0, binary: true, extLen: 2, desc: "Login Data"} +var doAID = DataObject{tag: 0x004F, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Application Idenfifier (AID)"} +var doDiscrDOs = DataObject{tag: 0x0073, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Discretionary Data Objects"} +var doCardCaps = DataObject{tag: 0x0047, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Card Capabilities"} +var doExtLenCaps = DataObject{tag: 0x00C0, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Extended Card Capabilities"} +var doAlgoAttrSign = DataObject{tag: 0x00C1, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Algorithm Attributes Signature"} +var doAlgoAttrEnc = DataObject{tag: 0x00C2, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Algorithm Attributes Encryption"} +var doAlgoAttrAuth = DataObject{tag: 0x00C3, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Algorithm Attributes Authentication"} +var doPWStatus = DataObject{tag: 0x00C4, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Password Status Bytes"} +var doFingerprints = DataObject{tag: 0x00C5, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Fingerprints"} +var doCAFingerprints = DataObject{tag: 0x00C6, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "CA Fingerprints"} +var doKeyGenDate = DataObject{tag: 0x00CD, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Generation times of key pairs"} +var doSecSuppTmpl = DataObject{tag: 0x007A, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Security Support Template"} +var doDigSigCtr = DataObject{tag: 0x0093, constructed: false, parent: 0x7A, binary: true, extLen: 0, desc: "Digital Signature Counter"} +var doPrivateDO1 = DataObject{tag: 0x0101, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 1"} +var doPrivateDO2 = DataObject{tag: 0x0102, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 2"} +var doPrivateDO3 = DataObject{tag: 0x0103, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 3"} +var doPrivateDO4 = DataObject{tag: 0x0104, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 4"} +var doCardholderCrt = DataObject{tag: 0x7F21, constructed: true, parent: 0, binary: true, extLen: 1, desc: "Cardholder certificate"} + +// V3.0 +var doGenFeatMgmt = DataObject{tag: 0x7F74, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "General Feature Management"} +var doAESKeyData = DataObject{tag: 0x00D5, constructed: false, parent: 0, binary: true, extLen: 0, desc: "AES key data"} +var doUIFSig = DataObject{tag: 0x00D6, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Signature"} +var doUIFDec = DataObject{tag: 0x00D7, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Decryption"} +var doUIFAut = DataObject{tag: 0x00D8, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Authentication"} +var doUIFAtt = DataObject{tag: 0x00D8, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Yubico Attestation key"} +var doKDFDO = DataObject{tag: 0x00F9, constructed: false, parent: 0, binary: true, extLen: 0, desc: "KDF data object"} +var doAlgoInfo = DataObject{tag: 0x00FA, constructed: false, parent: 0, binary: true, extLen: 2, desc: "Algorithm Information"} + +var DataObjects = []DataObject{ + doURL, doHistBytes, doCardRelData, doName, doLangPrefs, doSalutation, + doAppRelData, doLoginData, doAID, doDiscrDOs, doCardCaps, doExtLenCaps, + doAlgoAttrSign, doAlgoAttrEnc, doAlgoAttrAuth, doPWStatus, doFingerprints, + doCAFingerprints, doKeyGenDate, doSecSuppTmpl, doDigSigCtr, doPrivateDO1, + doPrivateDO2, doPrivateDO3, doPrivateDO4, doCardholderCrt, doGenFeatMgmt, + doAESKeyData, doUIFSig, doUIFDec, doUIFAut, doUIFAtt, doKDFDO, doAlgoInfo, +} + +func (do *DataObject) tagBytes() []byte { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, uint16(do.tag)) + return b +} + +func (do *DataObject) tagP1() byte { + return do.tagBytes()[0] +} + +func (do *DataObject) tagP2() byte { + return do.tagBytes()[1] +} + +func (do *DataObject) parentBytes() []byte { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, uint16(do.parent)) + return b +} + +func (do *DataObject) children() []DataObject { + var c []DataObject + + for _, d := range DataObjects { + if bytes.Equal(d.parentBytes(), do.tagBytes()) { + c = append(c, d) + } + } + + return c +} + +// doFindTLV function is based on GnuPG implementation of do_find_tlv in common/tlv.c +func doFindTLV(data []byte, tag uint16, nestLevel int) []byte { + var o int = 0 + var n int = len(data) + var tagLen uint16 + var thisTag uint16 + var tagFound bool + var composite bool + + for !tagFound { + if n < 2 { // Buffer definitely too short for tag and length. + return nil + } + + if data[o] == 0 || data[o] == 0xff { // Skip optional filler between TLV objects. + o++ + n-- + } + + composite = (data[o] & 0x20) != 0 + + if (data[o] & 0x1f) == 0x1f { // more tag bytes to follow + o++ + n-- + + if n < 2 { // Buffer definitely too short for tag and length. + return nil + } + if (data[o] & 0x1f) == 0x1f { // We support only up to 2 bytes. + return nil + } + + thisTag = binary.BigEndian.Uint16([]byte{data[o-1], data[o] & 0x7f}) + } else { + thisTag = binary.BigEndian.Uint16([]byte{0, data[o]}) + } + + tagLen = binary.BigEndian.Uint16([]byte{0, data[o+1]}) + o += 2 + n -= 2 + if tagLen < 0x80 { + // do nothing + } else if tagLen == 0x81 { // One byte length follows. + if n != 0 { // we expected 1 more bytes with the length + return nil + } + + tagLen = binary.BigEndian.Uint16([]byte{0, data[o]}) + o++ + n-- + } else if tagLen == 0x82 { // Two byte length follows + if n < 2 { // We expected 2 more bytes with the length. + return nil + } + + tagLen = binary.BigEndian.Uint16([]byte{data[o], data[o+1]}) + o += 2 + n -= 2 + } else { // APDU limit is 65535, thus it does not make sense to assume longer length fields. */ + return nil + } + + if composite && nestLevel < 100 { // Dive into this composite DO after checking for a too deep nesting + tmpData := doFindTLV(data[o:], tag, nestLevel+1) + + if len(tmpData) > 0 { + return tmpData + } + } + + if thisTag == tag { + tagFound = true + } else if int(tagLen) > n { // Buffer too short to skip to the next tag. + return nil + } else { + o += int(tagLen) + n -= int(tagLen) + } + } + + return data[o : o+int(tagLen)] +} diff --git a/pkg/yubikeyscard/iso7816.go b/pkg/yubikeyscard/iso7816.go new file mode 100644 index 0000000..09e9dd6 --- /dev/null +++ b/pkg/yubikeyscard/iso7816.go @@ -0,0 +1,141 @@ +package yubikeyscard + +import ( + "errors" + "fmt" + + "github.com/ebfe/scard" +) + +// Decipher data with private key on smart card +func Decipher(card *scard.Card, data []byte) ([]byte, error) { + ca := commandAPDU{ + cla: 0, + ins: 0x2a, + p1: 0x80, + p2: 0x86, + data: append(append([]byte{0}, data...), 1), // prepend RSA padding indicator byte and append footer + le: 0, + pib: true, + elf: true, + } + + if len(data)%16 != 0 { + return nil, errors.New("decipher input blocks should be in multiples of 16 bytes") + } + + ra, err := ca.transmit(card) + if err != nil { + return nil, err + } + + if !ra.success() { + return nil, errors.New("decipher operation unsuccessful") + } + + return ra.data, nil +} + +func GetData(card *scard.Card, do DataObject) ([]byte, error) { + data := []byte{} + + ca := commandAPDU{ + cla: 0, + ins: 0xca, + p1: do.tagP1(), + p2: do.tagP2(), + le: 0, + } + + ra, err := ca.transmit(card) + if err != nil { + return nil, err + } + + data = append(data, ra.data...) + + for !ra.success() { + if ra.sw1 == 0x61 { + ca = commandAPDU{ + cla: 0, + ins: 0xc0, + p1: 0, + p2: 0, + le: 0, + } + + ra, err = ca.transmit(card) + if err != nil { + return nil, err + } + + data = append(data, ra.data...) + } else { + return nil, errors.New("error occurred, could not get data segment") + } + } + + return data, nil +} + +func SelectApp(card *scard.Card) error { + ca := commandAPDU{ + cla: 0, + ins: 0xa4, + p1: 0x04, + p2: 0, + data: appID, + le: 0, + } + + ra, err := ca.transmit(card) + if err != nil { + return err + } + + if !ra.success() { + return errors.New("this YubiKey does not support OpenPGP") + } + + return nil +} + +// Verify is used to check the PIN for the provided bank and set appropriate +// access. Verify will return the number of tries remaining. If an error other +// than an invalid PIN occurs, -1 will be returned for the number of remaining +// retries. +func Verify(card *scard.Card, bank uint8, pin []byte) (int, error) { + ca := commandAPDU{ + cla: 0, + ins: 0x20, + p1: 0, + p2: 0x80 + bank, + data: pin, + le: 0, + } + + if bank < 1 || bank > 3 { + return -1, errors.New("invalid PIN bank, use banks 1-3") + } + + ra, err := ca.transmit(card) + if err != nil { + return -1, err + } + + if !ra.success() { + retries, err := pw1PINRetries(card) + if err != nil { + return -1, err + } + + verb := "retry" + if retries > 1 { + verb = "retries" + } + + return retries, fmt.Errorf("invalid PIN, %d %s remaining", retries, verb) + } + + return 3, nil +} diff --git a/pkg/yubikeyscard/yubikeyscard.go b/pkg/yubikeyscard/yubikeyscard.go new file mode 100644 index 0000000..bae647b --- /dev/null +++ b/pkg/yubikeyscard/yubikeyscard.go @@ -0,0 +1,448 @@ +package yubikeyscard + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "regexp" + "time" + + "github.com/ebfe/scard" +) + +const ( + AlgoIdRSA uint8 = 1 + AlgoIdECDH uint8 = 12 + AlgoIdECDSA uint8 = 13 +) + +const ( + scardPresentTimeout int = 1 + scardGetStatusChangeTimeout int = 5 +) + +var yubikeyManufacturerID = [2]byte{0, 6} + +type YubiKeys struct { + YubiKeys []*YubiKey + Context *scard.Context +} + +type YubiKey struct { + Card *scard.Card + ReaderLabel string + CardRelatedData CardRelatedData + AppRelatedData AppRelatedData + PINCache [3][]byte +} + +type CardRelatedData struct { + Name []byte + LanguagePrefs []byte + Salutation byte +} + +type AppRelatedData struct { + AID AID + AlgoAttrSign AlgoAttr + AlgoAttrEnc AlgoAttr + AlgoAttrAuth AlgoAttr + PWStatus PWStatus + Fingerprints Fingerprints + KeyGenDates KeyGenDates +} + +type AID struct { + RID [5]byte + App byte + Version [2]byte + Manufacturer [2]byte + Serial [4]byte + RFU [2]byte +} + +type AlgoAttr struct { + ID byte + RSAModLen [2]byte + RSAPubKeyExpLen [2]byte + ECurveOID []byte + PrivKeyImpFmt byte +} + +type PWStatus struct { + PW1Validity byte + PW1MaxLenFmt byte + PW1MaxLenRC byte + PW3MaxLenFmt byte + PW1RetryCtr byte + PW1RCRetryCtr byte + PW3RetryCtr byte +} + +type Fingerprints struct { + Sign [20]byte + Enc [20]byte + Auth [20]byte +} + +type KeyGenDates struct { + Sign [4]byte + Enc [4]byte + Auth [4]byte +} + +// Connect establishes the system context and opens sessions with all available +// YubiKeys. +func (yks *YubiKeys) Connect() error { + // establish system context + ctx, err := scard.EstablishContext() + if err != nil { + return err + } + + yks.Context = ctx + + // list available smart card readers + readers, err := ctx.ListReaders() + if err != nil { + return err + } + + // wait for all smards card to reach present state + presentReaders, err := waitUntilCardsPresent(ctx, readers) + if err != nil { + return err + } + + // ignore other smart cards + for _, r := range presentReaders { + yk := new(YubiKey) + + // connect to card + card, err := ctx.Connect(r, scard.ShareExclusive, scard.ProtocolAny) + if err != nil { + return err + } + + // skip smart cards that do not support the OpenPGP applet + if err = SelectApp(card); err != nil { + continue + } + + // build YubiKey struct + re := regexp.MustCompile("^(.*?) [0-9]{2}$") + yk.ReaderLabel = re.ReplaceAllString(r, "$1") + yk.Card = card + + if err = yk.refreshCardRelatedData(); err != nil { + return err + } + + if err = yk.refreshAppRelatedData(); err != nil { + return err + } + + // skip smart cards not manufactured by YubiCo + if yk.AppRelatedData.AID.Manufacturer != yubikeyManufacturerID { + continue + } + + yks.YubiKeys = append(yks.YubiKeys, yk) + } + + // if no YubiKeys are found, release context, and throw error + if len(yks.YubiKeys) == 0 { + // Release reader context + err = ctx.Release() + if err != nil { + return err + } + + return errors.New("no YubiKeys found") + } + + return nil +} + +// Disconnect will reset all open sessions smart cards and release the system +// context. +func (yks *YubiKeys) Disconnect() error { + for _, yk := range yks.YubiKeys { + // Disconnect card by sending reset command + err := yk.Card.Disconnect(scard.ResetCard) + if err != nil { + return err + } + } + + // Release reader context + err := yks.Context.Release() + if err != nil { + return err + } + + return nil +} + +// FindBySN will search the connected YubiKeys for matching serial numbers and +// if found, will return a pointer to that YubiKey. +func (yks *YubiKeys) FindBySN(sn string) *YubiKey { + for _, yk := range yks.YubiKeys { + if sn == fmt.Sprintf("%x", yk.AppRelatedData.AID.Serial) { + return yk + } + } + + return nil +} + +// FindByKeyID will search the connected YubiKeys for a matching PGP key ID and +// if found, will return a pointer to that YubiKey. +func (yks *YubiKeys) FindByKeyID(keyID uint64) *YubiKey { + for _, yk := range yks.YubiKeys { + fps := yk.AppRelatedData.Fingerprints + + for _, fp := range [][20]byte{fps.Sign, fps.Enc, fps.Auth} { + if binary.BigEndian.Uint64(fp[12:20]) == keyID { + return yk + } + } + + } + + return nil +} + +// CachedPIN returns the cached PIN for the provided bank if available. If PIN is not +// cached, CachedPIN will return nil. +func (yk *YubiKey) CachedPIN(bank uint8) []byte { + if bank < 1 || bank > 3 { + return nil + } + + return yk.PINCache[bank-1] +} + +// SetCachedPIN adds a verified PIN to the cache. +func (yk *YubiKey) SetCachedPIN(bank uint8, pin []byte) error { + if bank < 1 || bank > 3 { + return errors.New("invalid PIN bank, use banks 1-3") + } + + yk.PINCache[bank-1] = pin + return nil +} + +func waitUntilCardsPresent(ctx *scard.Context, readers []string) ([]string, error) { + start := time.Now() + var presentReaders []string + rs := make([]scard.ReaderState, len(readers)) + + for i := range rs { + rs[i].Reader = readers[i] + rs[i].CurrentState = scard.StateUnaware + } + + for { + ready := 0 + for i := range rs { + rs[i].CurrentState = rs[i].EventState + if rs[i].EventState&scard.StatePresent != 0 { + ready++ + + for _, pr := range presentReaders { + if pr == readers[i] { + continue + } + } + + presentReaders = append(presentReaders, readers[i]) + } + + } + + if ready == len(readers) { + return presentReaders, nil + } + + err := ctx.GetStatusChange(rs, time.Duration(scardPresentTimeout)*time.Second) + if err != nil { + return nil, err + } + + if time.Since(start) > time.Duration(scardGetStatusChangeTimeout)*time.Second { + return presentReaders, nil + } + } +} + +func pw1PINRetries(card *scard.Card) (int, error) { + data, err := GetData(card, doPWStatus) + if err != nil { + return 0, err + } + + return int(data[4]), nil +} + +func (yk *YubiKey) refreshCardRelatedData() error { + crd := &yk.CardRelatedData + + data, err := GetData(yk.Card, doCardRelData) + if err != nil { + return err + } + + for _, c := range doCardRelData.children() { + d := doFindTLV(data, c.tag, 1) + r := bytes.NewReader(d) + + switch c.tag { + case doName.tag: + crd.Name = make([]byte, r.Len()) + if _, err := io.ReadFull(r, crd.Name); err != nil { + return err + } + case doLangPrefs.tag: + crd.LanguagePrefs = make([]byte, r.Len()) + if _, err := io.ReadFull(r, crd.LanguagePrefs); err != nil { + return err + } + case doSalutation.tag: + if crd.Salutation, err = r.ReadByte(); err != nil { + return err + } + } + if err != nil { + return err + } + } + + return nil +} + +func (yk *YubiKey) refreshAppRelatedData() error { + ard := &yk.AppRelatedData + + data, err := GetData(yk.Card, doAppRelData) + if err != nil { + return err + } + + for _, c := range doAppRelData.children() { + cData := doFindTLV(data, c.tag, 1) + buf := bytes.NewReader(cData) + + switch c.tag { + case doAID.tag: + err = ard.AID.deserialize(buf) + case doAlgoAttrSign.tag: + err = ard.AlgoAttrSign.deserialize(buf) + case doAlgoAttrEnc.tag: + err = ard.AlgoAttrEnc.deserialize(buf) + case doAlgoAttrAuth.tag: + err = ard.AlgoAttrAuth.deserialize(buf) + case doPWStatus.tag: + err = ard.PWStatus.deserialize(buf) + case doFingerprints.tag: + err = ard.Fingerprints.deserialize(buf) + case doKeyGenDate.tag: + err = ard.KeyGenDates.deserialize(buf) + } + + if err != nil { + return err + } + } + + return nil +} + +func (aid *AID) deserialize(r *bytes.Reader) (err error) { + if _, err = io.ReadFull(r, aid.RID[:]); err != nil { + return + } + + if aid.App, err = r.ReadByte(); err != nil { + return err + } + + for _, a := range []*[2]byte{&aid.Version, &aid.Manufacturer} { + if _, err := io.ReadFull(r, a[:]); err != nil { + return err + } + } + + if _, err := io.ReadFull(r, aid.Serial[:]); err != nil { + return err + } + + if _, err := io.ReadFull(r, aid.RFU[:]); err != nil { + return err + } + + return nil +} + +func (aa *AlgoAttr) deserialize(r *bytes.Reader) (err error) { + if aa.ID, err = r.ReadByte(); err != nil { + return err + } + + switch aa.ID { + case AlgoIdRSA: + for _, a := range []*[2]byte{&aa.RSAModLen, &aa.RSAPubKeyExpLen} { + if _, err := io.ReadFull(r, a[:]); err != nil { + return err + } + } + case AlgoIdECDH, AlgoIdECDSA: + aa.ECurveOID = make([]byte, r.Len()-1) + if _, err := io.ReadFull(r, aa.ECurveOID); err != nil { + return err + } + + } + + if aa.PrivKeyImpFmt, err = r.ReadByte(); err != nil { + return err + } + + return nil +} + +func (pws *PWStatus) deserialize(r *bytes.Reader) (err error) { + pwb := []*byte{&pws.PW1Validity, &pws.PW1MaxLenFmt, &pws.PW1MaxLenRC, + &pws.PW3MaxLenFmt, &pws.PW1RetryCtr, &pws.PW1RCRetryCtr, &pws.PW3RetryCtr} + + for _, p := range pwb { + *p, err = r.ReadByte() + if err != nil { + return + } + } + + return +} + +func (fps *Fingerprints) deserialize(r *bytes.Reader) error { + for _, fp := range []*[20]byte{&fps.Sign, &fps.Enc, &fps.Auth} { + if _, err := io.ReadFull(r, fp[:]); err != nil { + return err + } + } + + return nil +} + +func (kgds *KeyGenDates) deserialize(r *bytes.Reader) error { + for _, kgd := range []*[4]byte{&kgds.Sign, &kgds.Enc, &kgds.Auth} { + if _, err := io.ReadFull(r, kgd[:]); err != nil { + return err + } + } + + return nil +} From 9be10012a71c7a57b2e8ae26a0db87fd52775a17 Mon Sep 17 00:00:00 2001 From: "N. Goncalves" Date: Tue, 23 Jul 2024 18:24:40 +0100 Subject: [PATCH 3/6] Update go mod version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 253c3ca..f7055b3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/CorefluxCommunity/zeusctl -go 1.22 +go 1.21.9 require ( github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 From 8152993c5f605d5c50ac425a83fbf9e038d078e3 Mon Sep 17 00:00:00 2001 From: "N. Goncalves" Date: Wed, 24 Jul 2024 10:38:02 +0100 Subject: [PATCH 4/6] Attempt to fix missing C headers for piv-go pkg --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd813f3..e6ade89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,10 @@ jobs: uses: actions/setup-go@v4 with: go-version: '1.21.9' + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpcsclite-dev - name: Cache Go modules uses: actions/cache@v3 with: @@ -40,6 +44,10 @@ jobs: uses: actions/setup-go@v4 with: go-version: '1.21.9' + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpcsclite-dev - name: Cache Go modules uses: actions/cache@v3 with: @@ -62,6 +70,10 @@ jobs: uses: actions/setup-go@v4 with: go-version: '1.21.9' + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpcsclite-dev - name: Cache Go modules uses: actions/cache@v3 with: @@ -86,6 +98,10 @@ jobs: uses: actions/setup-go@v4 with: go-version: '1.21.9' + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libpcsclite-dev - name: Build for multiple platforms run: | go mod tidy From c8ae954a0198a70c0a60e37d4a3abe1f618bb739 Mon Sep 17 00:00:00 2001 From: "N. Goncalves" Date: Thu, 26 Sep 2024 15:24:05 +0100 Subject: [PATCH 5/6] Clean up and Nix Temporarily remove smart card powered operator commands to create a more GPG agnostic solution. Nix ready. Documentation --- .envrc | 1 - .github/workflows/ci.yml | 145 ---------- .github/workflows/issues.yml | 20 -- .gitignore | 3 - Makefile | 30 --- README.md | 150 ++++++++++- cmd/generate_root.go | 73 ----- cmd/get.go | 4 +- cmd/list.go | 35 +-- cmd/login.go | 4 +- cmd/root.go | 40 +-- cmd/show.go | 40 +-- cmd/unseal.go | 69 ----- flake.lock | 79 +++++- flake.nix | 82 ++++-- go.mod | 22 +- go.sum | 36 +-- gomod2nix.toml | 156 +++++++++++ main.go | 2 +- pkg/crypto/crypto.go | 117 -------- pkg/secrets/kv.go | 4 +- pkg/utils/utils.go | 92 ------- pkg/vault/auth.go | 2 +- pkg/vault/operator.go | 160 +---------- pkg/yubikey/yubikey.go | 125 --------- pkg/yubikeypgp/yubikeypgp.go | 223 --------------- pkg/yubikeyscard/apdu.go | 125 --------- pkg/yubikeyscard/data_object.go | 180 ------------- pkg/yubikeyscard/iso7816.go | 141 ---------- pkg/yubikeyscard/yubikeyscard.go | 448 ------------------------------- 30 files changed, 489 insertions(+), 2119 deletions(-) delete mode 100644 .envrc delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/issues.yml delete mode 100644 .gitignore delete mode 100644 Makefile delete mode 100644 cmd/generate_root.go delete mode 100644 cmd/unseal.go create mode 100644 gomod2nix.toml delete mode 100644 pkg/crypto/crypto.go delete mode 100644 pkg/yubikey/yubikey.go delete mode 100644 pkg/yubikeypgp/yubikeypgp.go delete mode 100644 pkg/yubikeyscard/apdu.go delete mode 100644 pkg/yubikeyscard/data_object.go delete mode 100644 pkg/yubikeyscard/iso7816.go delete mode 100644 pkg/yubikeyscard/yubikeyscard.go diff --git a/.envrc b/.envrc deleted file mode 100644 index 3550a30..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index e6ade89..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: CI - -on: - push: - branches: [ main ] - tags: - - 'v*.*.*' - pull_request: - branches: [ main ] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21.9' - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libpcsclite-dev - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Lint - run: | - go mod tidy - make fmt - make lint - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21.9' - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libpcsclite-dev - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Test - run: | - go mod tidy - make test - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21.9' - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libpcsclite-dev - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Lint - run: | - go mod tidy - make build - - release: - needs: [lint, test, build ] - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21.9' - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libpcsclite-dev - - name: Build for multiple platforms - run: | - go mod tidy - make build - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: false - - name: Upload Linux AMD64 Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./bin/zeusctl-linux-amd64 - asset_name: zeusctl-${{ github.ref }}-linux-amd64 - asset_content_type: application/octet-stream - - name: Upload Darwin AMD64 Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./bin/zeusctl-darwin-amd64 - asset_name: zeusctl-${{ github.ref }}-darwin-amd64 - asset_content_type: application/octet-stream - - name: Upload Darwin ARM64 Asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./bin/zeusctl-${{ github.ref }}-darwin-arm64 - asset_name: zeusctl-darwin-arm64 - asset_content_type: application/octet-stream diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml deleted file mode 100644 index f2ab6ab..0000000 --- a/.github/workflows/issues.yml +++ /dev/null @@ -1,20 +0,0 @@ -on: - issues: - types: - [opened, closed, reopened] - -jobs: - alert: - runs-on: ubuntu-latest - steps: - - uses: danhellem/github-actions-issue-to-work-item@v2.3 - env: - ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN_COREFLUXPROD }}" - github_token: "${{ secrets.GITHUB_TOKEN }}" - ado_organization: "CorefluxProd" - ado_project: "CorefluxTeam" - ado_wit: "Issue" - ado_new_state: "New" - ado_active_state: "Active" - ado_close_state: "Closed" - ado_bypassrules: true diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 78f0afe..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Builds -bin/* - diff --git a/Makefile b/Makefile deleted file mode 100644 index d7baab8..0000000 --- a/Makefile +++ /dev/null @@ -1,30 +0,0 @@ -.PHONY: fmt lint test build - -TARGETS := linux-amd64 linux-arm64 darwin-amd64 darwin-arm64 - -default: all - -fmt: - go fmt ./... - -lint: - go vet ./... - -test: - go test ./... - -build: $(TARGETS) - -linux-amd64: - CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o bin/zeusctl-linux-amd64 ./main.go - -linux-arm64: - CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -o bin/zeusctl-linux-arm64 ./main.go - -darwin-amd64: - CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/zeusctl-darwin-amd64 ./main.go - -darwin-arm64: - CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o bin/zeusctl-darwin-arm64 ./main.go - -all: fmt lint test build diff --git a/README.md b/README.md index 4a22b9c..2b2adad 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,154 @@ -# Zeus CLI +# vaultctl -- Simple CLI to facilitate Vault operations, like auth and fetching secrets using context configs +``vaultctl`` is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault by allowing users to fetch and export secrets based on predefined configurations. This guide will cover how to use the vaultctl commands, install it as a Nix package, and understand its features. -## Usage +## Installation -### Auth +``vaultctl`` is packaged as a Nix Flake. To install and use ``vaultctl``, you need to have Nix installed. Follow the [Nix installation guide](https://nix.dev/manual/nix/2.18/installation/multi-user) if you haven't set up Nix yet. +### Using vaultctl with Nix Flakes + +Once Nix is installed, you can enter a shell with ``vaultctl`` ready to use by running the following command: + +```bash +nix develop github:CorefluxCommunity/vaultctl ``` -zeusctl vault auth --host HOST:PORT --ca-path CA_CERT_PATH --user USER --password PASSWORD + +Alternatively, you can add ``vaultctl`` to your own Nix Flake as an input. Here's an example of how to include it in your flake: + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + vaultctl.url = "github:CorefluxCommunity/vaultctl"; + }; + + outputs = { self, nixpkgs, vaultctl }: + let + pkgs = import nixpkgs { system = "x86_64-linux"; }; + in { + devShell = pkgs.mkShell { + buildInputs = [ vaultctl.packages."x86_64-linux".default ]; + }; + }; +} ``` -### Load and Export Context +This configuration sets up a development shell with ``vaultctl`` available. + +## Configuration Files +``vaultctl`` uses two main configuration files: + +### Vault Configuration File (config.hcl): Defines the Vault clusters and their servers. + +**Example configuration (~/.vaultctl/config.hcl):** + +```hcl +cluster "prod" { + address = "https://vault.example.com" + servers = ["https://vault1.example.com", "https://vault2.example.com"] +} + +cluster "dev" { + address = "https://vault-dev.example.com" + servers = ["https://vault-dev1.example.com"] +} ``` -eval $(zeusctl vault context CONTEXT_NAME) + +### Secrets Context File (contexts.hcl): Defines the secrets to be fetched and the keys to be exported. + +**Example context file (./contexts.hcl):** + +```hcl +context "app-secrets" { + secret "database" { + path = "secret/data/prod/database" + key { + name = "username" + } + key { + name = "password" + export_name = "DB_PASSWORD" + } + } + + secret "api" { + path = "secret/data/prod/api" + key { + name = "api_key" + base64_decode = true + } + } +} ``` +## Command Overview + +``vaultctl`` provides the following main commands: + +### ``login`` + +Authenticate to Vault using the userpass authentication method. The token generated after authentication is stored locally and used for subsequent requests. + +**Usage:** + +```bash +vaultctl login cluster --method userpass --user --password +``` + +``cluster-name``: The name of the Vault cluster (from the configuration). +``method``: The authentication method (currently only supports userpass. WIP: client certs). +``user``: The username for Vault login. +``password``: The password for Vault login. + +### ``get secrets`` + +Fetch secrets from Vault based on a predefined context from a secrets context file. The secrets can be exported as environment variables using the --export flag. + +**Usage:** + +```bash +vaultctl get secrets --context [--export] +``` + +``cluster-name``: The Vault cluster name. +``context-name``: The context name defined in the context file. +``context``: (Optional) Path to the context file. Defaults to ./contexts.hcl. +``export``: (Optional) Export secrets as environment variables. + +```bash +vaultctl get secrets prod app-secrets --context ./app-contexts.hcl --export +``` + +**Important: Exporting Secrets in Shell** + +When using the --export flag to export secrets as environment variables, the output needs to be wrapped in eval to actually set the variables in your current shell session. + +**Example:** + +```bash +eval $(vaultctl get secrets prod app-secrets --context ./app-contexts.hcl --export) +``` + +This works by generating export commands that need to be evaluated in the current shell context. + +### ``list clusters`` + +Lists the configured Vault clusters from the ``vaultctl`` configuration file. + +**Usage:** + +```bash +vaultctl list clusters +``` + +### ``show cluster`` + +Displays detailed information about a Vault cluster, including its unseal status and the list of servers. + +**Usage:** + +```bash +vaultctl show cluster +``` diff --git a/cmd/generate_root.go b/cmd/generate_root.go deleted file mode 100644 index 3e8ab4f..0000000 --- a/cmd/generate_root.go +++ /dev/null @@ -1,73 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - - "github.com/CorefluxCommunity/zeusctl/pkg/utils" - "github.com/CorefluxCommunity/zeusctl/pkg/vault" -) - -func init() { - generateRootServerSubCmd.Flags().StringVarP(&vaultGenerateRootNonce, "nonce", "n", "", "nonce for root token generation") - generateRootClusterSubCmd.Flags().StringVarP(&vaultGenerateRootNonce, "nonce", "n", "", "nonce for root token generation") - - generateRootCmd.AddCommand(generateRootServerSubCmd) - generateRootCmd.AddCommand(generateRootClusterSubCmd) - - rootCmd.AddCommand(generateRootCmd) -} - -var generateRootCmd = &cobra.Command{ - Use: "generate-root", - Short: "Generate Vault root token", - Long: `Decrypt the unseal key and generate root token for Vault cluster.`, -} - -var generateRootServerSubCmd = &cobra.Command{ - Use: "server -n ", - Short: "Generate root token for Vault cluster", - Long: `Decrypt the unseal key and generate Vault root token.`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - vaultAddr := args[0] - keyPath := args[1] - - keys, err := utils.ReadKeyFile(keyPath) - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - - if err := vault.GenerateRoot(vaultAddr, keys); err != nil { - utils.PrintFatal(err.Error(), 1) - } - }, -} - -var generateRootClusterSubCmd = &cobra.Command{ - Use: "cluster -n ", - Short: "Generate root token for Vault cluster", - Long: `Decrypt the unseal key and generate Vault root token.`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - clusterName := args[0] - - vaultAddr, err := getVaultAddress(clusterName) - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - - cluster, err := getVaultClusterConfig(clusterName) - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - - keys, err := cluster.keyring() - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - - if err := vault.GenerateRoot(vaultAddr, utils.Unique(keys)); err != nil { - utils.PrintFatal(err.Error(), 1) - } - }, -} diff --git a/cmd/get.go b/cmd/get.go index 1127164..f0fb9f6 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -1,8 +1,8 @@ package cmd import ( - "github.com/CorefluxCommunity/zeusctl/pkg/secrets" - "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/vaultctl/pkg/secrets" + "github.com/CorefluxCommunity/vaultctl/pkg/utils" "github.com/spf13/cobra" ) diff --git a/cmd/list.go b/cmd/list.go index d86b207..9043456 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -3,47 +3,32 @@ package cmd import ( "fmt" - "github.com/CorefluxCommunity/zeusctl/pkg/utils" - "github.com/CorefluxCommunity/zeusctl/pkg/yubikey" + "github.com/CorefluxCommunity/vaultctl/pkg/utils" "github.com/spf13/cobra" ) func init() { listCmd.AddCommand(listClustersSubCmd) - listCmd.AddCommand(listYubiKeysSubCmd) rootCmd.AddCommand(listCmd) } var listCmd = &cobra.Command{ Use: "list", - Short: "List connected YubiKeys and configured Vault clusters", - Long: `List connected YubiKeys and configured Vault clusters.`, + Short: "List configured Vault clusters", + Long: `List configured Vault clusters.`, } var listClustersSubCmd = &cobra.Command{ Use: "clusters", Short: "List Vault clusters", - Long: `List Vault clusters in Zeus configuration file.`, + Long: `List Vault clusters in vaultctl configuration file.`, Run: func(cmd *cobra.Command, args []string) { i := 0 for name, cluster := range config.Clusters { - keys, err := cluster[0].keyring() - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - utils.PrintHeader(name) utils.PrintKVSlice("Server(s)", cluster[0].Servers) - uniqKeys := utils.Unique(keys) - if len(keys) != len(uniqKeys) { - dupCount := len(keys) - len(uniqKeys) - utils.PrintKV("Key(s)", fmt.Sprintf("%d (%d duplicates)", len(keys), dupCount)) - } else { - utils.PrintKV("Key(s)", fmt.Sprintf("%d", len(keys))) - } - if i < len(config.Clusters)-1 { fmt.Println() } @@ -52,15 +37,3 @@ var listClustersSubCmd = &cobra.Command{ } }, } - -var listYubiKeysSubCmd = &cobra.Command{ - Use: "yubikeys", - Short: "List connected YubiKeys", - Long: `List overview of connected YubiKeys.`, - Run: func(cmd *cobra.Command, args []string) { - err := yubikey.ListYubiKeys() - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - }, -} diff --git a/cmd/login.go b/cmd/login.go index 1c77e68..703ac94 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -1,8 +1,8 @@ package cmd import ( - "github.com/CorefluxCommunity/zeusctl/pkg/utils" - "github.com/CorefluxCommunity/zeusctl/pkg/vault" + "github.com/CorefluxCommunity/vaultctl/pkg/utils" + "github.com/CorefluxCommunity/vaultctl/pkg/vault" "github.com/spf13/cobra" ) diff --git a/cmd/root.go b/cmd/root.go index 4f72043..07e32e9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,18 +8,14 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/vaultctl/pkg/utils" ) var ( // CLI config - config ZeusConfig + config VaultConfig configFile string - // Commands - // generate-root - vaultGenerateRootNonce string - // login method string // TODO: Create enum for auth methods user string @@ -30,19 +26,17 @@ var ( exportSecrets bool rootCmd = &cobra.Command{ - Use: "zeusctl", + Use: "vaultctl", } ) -type ZeusConfig struct { +type VaultConfig struct { Clusters map[string][]*VaultClusterConfig `hcl:"cluster" mapstructure:"cluster"` } type VaultClusterConfig struct { Address string `hcl:"address" mapstructure:"address"` Servers []string `hcl:"servers" mapstructure:"servers"` - Keys []string `hcl:"keys" mapstructure:"keys"` - KeyFile string `hcl:"key_file" mapstructure:"key_file"` } // Execute executes the root command. @@ -53,7 +47,7 @@ func Execute() error { func init() { cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.zeusctl/zeusctl.hcl)") + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.vaultctl/config.hcl)") } func initConfig() { @@ -65,10 +59,10 @@ func initConfig() { home, err := homedir.Dir() cobra.CheckErr(err) - // Search config in $HOME/zeusctl directory with name "zeusctl" (without extension). + // Search config in $HOME/vaultctl directory with name "config" (without extension). // TODO: Get cli config directory from shell env - viper.AddConfigPath(home + "/.zeusctl") - viper.SetConfigName("zeusctl") + viper.AddConfigPath(home + "/.vaultctl") + viper.SetConfigName("config") viper.SetConfigType("hcl") } @@ -80,7 +74,7 @@ func initConfig() { utils.PrintFatal(fmt.Sprintf("unable to decode into struct, %v", err), 1) } - // get zeusctl config direction and set as cwd + // get vaultctl config direction and set as cwd configDir, err := getConfigDir() if err != nil { utils.PrintFatal(err.Error(), 1) @@ -123,20 +117,6 @@ func getVaultAddress(clusterName string) (string, error) { return "", fmt.Errorf("no address or servers found for cluster '%s'", clusterName) } -func (vc *VaultClusterConfig) keyring() ([]string, error) { - keys := vc.Keys - if vc.KeyFile != "" { - kf, err := utils.ReadKeyFile(vc.KeyFile) - if err != nil { - return nil, err - } - - keys = append(keys, kf...) - } - - return keys, nil -} - func getConfigDir() (string, error) { home, err := homedir.Dir() if err != nil { @@ -144,7 +124,7 @@ func getConfigDir() (string, error) { } // TODO: Get cli config directory from shell env - path := home + "/.zeusctl" + path := home + "/.vaultctl" return path, nil } diff --git a/cmd/show.go b/cmd/show.go index a26b2ff..80b18ff 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -1,25 +1,21 @@ package cmd import ( - "fmt" - - "github.com/CorefluxCommunity/zeusctl/pkg/utils" - "github.com/CorefluxCommunity/zeusctl/pkg/vault" - "github.com/CorefluxCommunity/zeusctl/pkg/yubikey" + "github.com/CorefluxCommunity/vaultctl/pkg/utils" + "github.com/CorefluxCommunity/vaultctl/pkg/vault" "github.com/spf13/cobra" ) func init() { showCmd.AddCommand(showClusterSubCmd) - showCmd.AddCommand(showYubiKeySubCmd) rootCmd.AddCommand(showCmd) } var showCmd = &cobra.Command{ Use: "show", - Short: "Show details of YubiKeys and Vault clusters", - Long: `Show details of YubiKeys and Vault clusters.`, + Short: "Show details of Vault clusters", + Long: `Show details of Vault clusters.`, } var showClusterSubCmd = &cobra.Command{ @@ -35,11 +31,6 @@ var showClusterSubCmd = &cobra.Command{ utils.PrintFatal(err.Error(), 1) } - keys, err := cluster.keyring() - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - if len(cluster.Servers) == 0 { utils.PrintFatal("no Vault servers in configuration", 1) } @@ -47,31 +38,8 @@ var showClusterSubCmd = &cobra.Command{ utils.PrintHeader("Vault Cluster Status") utils.PrintKVSlice("Server(s)", cluster.Servers) - uniqKeys := utils.Unique(keys) - if len(keys) != len(uniqKeys) { - dupCount := len(keys) - len(uniqKeys) - utils.PrintKV("Key(s)", fmt.Sprintf("%d (%d duplicates)", len(keys), dupCount)) - } else { - utils.PrintKV("Key(s)", fmt.Sprintf("%d", len(keys))) - } - if err := vault.ListVaultStatus(cluster.Servers[0]); err != nil { utils.PrintFatal(err.Error(), 1) } }, } - -var showYubiKeySubCmd = &cobra.Command{ - Use: "yubikey ", - Short: "Show YubiKey details", - Long: `Show YubiKey details returned from OpenPGP application data objects.`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - sn := args[0] - - err := yubikey.ShowYubiKey(sn) - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - }, -} diff --git a/cmd/unseal.go b/cmd/unseal.go deleted file mode 100644 index a8a4dfa..0000000 --- a/cmd/unseal.go +++ /dev/null @@ -1,69 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - - "github.com/CorefluxCommunity/zeusctl/pkg/utils" - "github.com/CorefluxCommunity/zeusctl/pkg/vault" -) - -func init() { - unsealCmd.AddCommand(unsealServerSubCmd) - unsealCmd.AddCommand(unsealClusterSubCmd) - - rootCmd.AddCommand(unsealCmd) -} - -var unsealCmd = &cobra.Command{ - Use: "unseal", - Short: "Unseal Vault by server or cluster", - Long: `Decrypt PGP-encrypted unseal key and unseal Vault.`, -} - -var unsealServerSubCmd = &cobra.Command{ - Use: "server ", - Short: "Unseal Vault server", - Long: `Decrypt unseal key and unseal single Vault server.`, - Args: cobra.ExactArgs(2), - Run: func(cmd *cobra.Command, args []string) { - vaultAddr := args[0] - keyPath := args[1] - - keys, err := utils.ReadKeyFile(keyPath) - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - - if err := vault.Unseal([]string{vaultAddr}, keys); err != nil { - utils.PrintFatal(err.Error(), 1) - } - }, -} - -var unsealClusterSubCmd = &cobra.Command{ - Use: "cluster ", - Short: "Unseal Vault cluster", - Long: `Decrypt unseal key and unseal Vault cluster.`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - clusterName := args[0] - - cluster, err := getVaultClusterConfig(clusterName) - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - - keys, err := cluster.keyring() - if err != nil { - utils.PrintFatal(err.Error(), 1) - } - - if len(cluster.Servers) == 0 { - utils.PrintFatal("no Vault servers in configuration", 1) - } - - if err := vault.Unseal(cluster.Servers, utils.Unique(keys)); err != nil { - utils.PrintFatal(err.Error(), 1) - } - }, -} diff --git a/flake.lock b/flake.lock index e751083..e7b0489 100644 --- a/flake.lock +++ b/flake.lock @@ -1,24 +1,93 @@ { "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gomod2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1725515722, + "narHash": "sha256-+gljgHaflZhQXtr3WjJrGn8NXv7MruVPAORSufuCFnw=", + "owner": "nix-community", + "repo": "gomod2nix", + "rev": "1c6fd4e862bf2f249c9114ad625c64c6c29a8a08", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "gomod2nix", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1715218190, - "narHash": "sha256-R98WOBHkk8wIi103JUVQF3ei3oui4HvoZcz9tYOAwlk=", + "lastModified": 1658285632, + "narHash": "sha256-zRS5S/hoeDGUbO+L95wXG9vJNwsSYcl93XiD0HQBXLk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9a9960b98418f8c385f52de3b09a63f9c561427a", + "rev": "5342fc6fb59d0595d26883c3cadff16ce58e44f3", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.11", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1727264057, + "narHash": "sha256-KQPI8CTTnB9CrJ7LrmLC4VWbKZfljEPBXOFGZFRpxao=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "759537f06e6999e141588ff1c9be7f3a5c060106", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "gomod2nix": "gomod2nix", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 8c8bd04..05a73fa 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,65 @@ { - description = "Go encapsulated development environment"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + gomod2nix.url = "github:nix-community/gomod2nix"; + }; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; - - outputs = { self, nixpkgs }: + outputs = { self, nixpkgs, gomod2nix }: let - goVersion = "21"; - overlays = [ (final: prev: { go = prev."go_1_${toString goVersion}"; }) ]; - supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; - forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { - pkgs = import nixpkgs { inherit overlays system; }; - }); - in - { - devShells = forEachSupportedSystem ({ pkgs }: { - default = pkgs.mkShell { - buildInputs = with pkgs; [ - go - gotools - golangci-lint - ]; - }; - }); + # List of supported system architectures + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + # Helper function to create attributes for all systems + forAllSystems = f: builtins.listToAttrs (map (system: { + name = system; + value = f system; + }) systems); + overlay = final: prev: { + go = prev.go_1_21; + }; + in { + # Dev shell for all supported systems + devShells = forAllSystems (system: + let + pkgs = import nixpkgs { + system = system; + overlays = [ overlay gomod2nix.overlays.default ]; + }; + in { + default = pkgs.mkShell { + buildInputs = [ + pkgs.go + pkgs.gomod2nix + ]; + shellHook = '' + export GOPRIVATE=github.com/CorefluxCommunity/vaultctl + ''; + }; + } + ); + + # Exporting package for all supported systems + packages = forAllSystems (system: + let + pkgs = import nixpkgs { + system = system; + overlays = [ overlay gomod2nix.overlays.default ]; + }; + in { + default = pkgs.buildGoApplication { + pname = "vaultctl"; + version = "1.0.0"; + src = ./.; + + # Use the generated gomod2nix.toml + # TODO: Automate auto generate mod file during/before package build + modules = ./gomod2nix.toml; + }; + } + ); }; } - diff --git a/go.mod b/go.mod index f7055b3..f18e50f 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,31 @@ -module github.com/CorefluxCommunity/zeusctl +module github.com/CorefluxCommunity/vaultctl -go 1.21.9 +go 1.21.13 require ( github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 - github.com/hashicorp/hcl/v2 v2.21.0 - github.com/hashicorp/vault/api v1.14.0 + github.com/hashicorp/hcl/v2 v2.22.0 + github.com/hashicorp/vault/api v1.15.0 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - golang.org/x/crypto v0.25.0 - golang.org/x/term v0.22.0 + golang.org/x/crypto v0.27.0 + golang.org/x/term v0.24.0 ) require ( github.com/agext/levenshtein v1.2.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/cenkalti/backoff/v3 v3.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.6 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect @@ -50,9 +50,9 @@ require ( golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index bf1d575..ef91e2a 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -38,8 +38,8 @@ github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.6 h1:TwRYfx2z2C4cLbXmT8I5PgP/xmuqASDyiVuGYfs9GZM= -github.com/hashicorp/go-retryablehttp v0.7.6/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= @@ -51,10 +51,10 @@ github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0S github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14= -github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= -github.com/hashicorp/vault/api v1.14.0 h1:Ah3CFLixD5jmjusOgm8grfN9M0d+Y8fVR2SW0K6pJLU= -github.com/hashicorp/vault/api v1.14.0/go.mod h1:pV9YLxBGSz+cItFDd8Ii4G17waWOQ32zVjMWHe/cOqk= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= +github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -128,23 +128,23 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= diff --git a/gomod2nix.toml b/gomod2nix.toml new file mode 100644 index 0000000..2b9d585 --- /dev/null +++ b/gomod2nix.toml @@ -0,0 +1,156 @@ +schema = 3 + +[mod] + [mod."github.com/agext/levenshtein"] + version = "v1.2.1" + hash = "sha256-SuFgpvp7KGO4f2t0KnkhSUv7iRI7oGq40CjRc1tLNr4=" + [mod."github.com/apparentlymart/go-textseg/v13"] + version = "v13.0.0" + hash = "sha256-Hn+zO0J/vnG5U6JvZfRyWm8vsqYdjh4RAreWnZLCQKQ=" + [mod."github.com/apparentlymart/go-textseg/v15"] + version = "v15.0.0" + hash = "sha256-960kVVQSGhx1Gww5cfoNeM3yXaA4dwlv+AFhKh6EGlA=" + [mod."github.com/cenkalti/backoff/v4"] + version = "v4.3.0" + hash = "sha256-wfVjNZsGG1WoNC5aL+kdcy6QXPgZo4THAevZ1787md8=" + [mod."github.com/ebfe/scard"] + version = "v0.0.0-20230420082256-7db3f9b7c8a7" + hash = "sha256-fUJV9ec3CWy+Gh4ChCcWu/szAsEgqU2eVw0NNZu9B4c=" + [mod."github.com/fsnotify/fsnotify"] + version = "v1.7.0" + hash = "sha256-MdT2rQyQHspPJcx6n9ozkLbsktIOJutOqDuKpNAtoZY=" + [mod."github.com/go-jose/go-jose/v4"] + version = "v4.0.1" + hash = "sha256-jscu63JKaJr0j1oBoezhi4yS0FLRS5eupfwJE+++DXw=" + [mod."github.com/google/go-cmp"] + version = "v0.6.0" + hash = "sha256-qgra5jze4iPGP0JSTVeY5qV5AvEnEu39LYAuUCIkMtg=" + [mod."github.com/hashicorp/errwrap"] + version = "v1.1.0" + hash = "sha256-6lwuMQOfBq+McrViN3maJTIeh4f8jbEqvLy2c9FvvFw=" + [mod."github.com/hashicorp/go-cleanhttp"] + version = "v0.5.2" + hash = "sha256-N9GOKYo7tK6XQUFhvhImtL7PZW/mr4C4Manx/yPVvcQ=" + [mod."github.com/hashicorp/go-multierror"] + version = "v1.1.1" + hash = "sha256-ANzPEUJIZIlToxR89Mn7Db73d9LGI51ssy7eNnUgmlA=" + [mod."github.com/hashicorp/go-retryablehttp"] + version = "v0.7.7" + hash = "sha256-XZjxncyLPwy6YBHR3DF5bEl1y72or0JDUncTIsb/eIU=" + [mod."github.com/hashicorp/go-rootcerts"] + version = "v1.0.2" + hash = "sha256-prifkrFs+lawGTig3GwxddR0QM9E1+IpgZWCKoOnS5M=" + [mod."github.com/hashicorp/go-secure-stdlib/parseutil"] + version = "v0.1.6" + hash = "sha256-vnfrdWA2LvxpcerhB3sEX5Uhxx0doiQTrcVth1r8bfU=" + [mod."github.com/hashicorp/go-secure-stdlib/strutil"] + version = "v0.1.2" + hash = "sha256-UmCMzjamCW1d9KNvNzELqKf1ElHOXPz+ZtdJkI+DV0A=" + [mod."github.com/hashicorp/go-sockaddr"] + version = "v1.0.2" + hash = "sha256-bshn2I074/pnQ8gZU5RsfQRTrIvMC459bOfd/O/dHeo=" + [mod."github.com/hashicorp/hcl"] + version = "v1.0.0" + hash = "sha256-xsRCmYyBfglMxeWUvTZqkaRLSW+V2FvNodEDjTGg1WA=" + [mod."github.com/hashicorp/hcl/v2"] + version = "v2.22.0" + hash = "sha256-eim4PQc/SYp6rtAC5qMnhufLV4atOQgWewixHcgIbok=" + [mod."github.com/hashicorp/vault/api"] + version = "v1.15.0" + hash = "sha256-7V/QU9TT+setRdTjS7vKL05wOEd4IEhSMVt9SJr/sQ4=" + [mod."github.com/inconshreveable/mousetrap"] + version = "v1.1.0" + hash = "sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE=" + [mod."github.com/logrusorgru/aurora"] + version = "v2.0.3+incompatible" + hash = "sha256-7o5Fh4jscdYKgXfnNMbcD68Kjw8Z4LcPgHcr4ZyQYrI=" + [mod."github.com/magiconair/properties"] + version = "v1.8.7" + hash = "sha256-XQ2bnc2s7/IH3WxEO4GishZurMyKwEclZy1DXg+2xXc=" + [mod."github.com/mitchellh/go-homedir"] + version = "v1.1.0" + hash = "sha256-oduBKXHAQG8X6aqLEpqZHs5DOKe84u6WkBwi4W6cv3k=" + [mod."github.com/mitchellh/go-wordwrap"] + version = "v1.0.0" + hash = "sha256-bgG/RLuwh8hn34/LMfGX3Yr9+TIFFSYCM9jakRlfzsk=" + [mod."github.com/mitchellh/mapstructure"] + version = "v1.5.0" + hash = "sha256-ztVhGQXs67MF8UadVvG72G3ly0ypQW0IRDdOOkjYwoE=" + [mod."github.com/pelletier/go-toml/v2"] + version = "v2.2.2" + hash = "sha256-ukxk1Cfm6cQW18g/aa19tLcUu5BnF7VmfAvrDHAOl6A=" + [mod."github.com/ryanuber/go-glob"] + version = "v1.0.0" + hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY=" + [mod."github.com/sagikazarmark/locafero"] + version = "v0.4.0" + hash = "sha256-7I1Oatc7GAaHgAqBFO6Tv4IbzFiYeU9bJAfJhXuWaXk=" + [mod."github.com/sagikazarmark/slog-shim"] + version = "v0.1.0" + hash = "sha256-F92BQXXmn3mCwu3mBaGh+joTRItQDSDhsjU6SofkYdA=" + [mod."github.com/sourcegraph/conc"] + version = "v0.3.0" + hash = "sha256-mIdMs9MLAOBKf3/0quf1iI3v8uNWydy7ae5MFa+F2Ko=" + [mod."github.com/spf13/afero"] + version = "v1.11.0" + hash = "sha256-+rV3cDZr13N8E0rJ7iHmwsKYKH+EhV+IXBut+JbBiIE=" + [mod."github.com/spf13/cast"] + version = "v1.6.0" + hash = "sha256-hxioqRZfXE0AE5099wmn3YG0AZF8Wda2EB4c7zHF6zI=" + [mod."github.com/spf13/cobra"] + version = "v1.8.1" + hash = "sha256-yDF6yAHycV1IZOrt3/hofR+QINe+B2yqkcIaVov3Ky8=" + [mod."github.com/spf13/pflag"] + version = "v1.0.5" + hash = "sha256-w9LLYzxxP74WHT4ouBspH/iQZXjuAh2WQCHsuvyEjAw=" + [mod."github.com/spf13/viper"] + version = "v1.19.0" + hash = "sha256-MZ8EAvdgpGbw6kmUz8UOaAAAMdPPGd14TrCBAY+A1T4=" + [mod."github.com/subosito/gotenv"] + version = "v1.6.0" + hash = "sha256-LspbjTniiq2xAICSXmgqP7carwlNaLqnCTQfw2pa80A=" + [mod."github.com/zclconf/go-cty"] + version = "v1.13.0" + hash = "sha256-2hMDPoptLAB5o2bAyee1jPenOAXnVNGE+dStn0dgQzI=" + [mod."go.uber.org/atomic"] + version = "v1.9.0" + hash = "sha256-D8OtLaViqPShz1w8ijhIHmjw9xVaRu0qD2hXKj63r4Q=" + [mod."go.uber.org/multierr"] + version = "v1.9.0" + hash = "sha256-tlDRooh/V4HDhZohsUrxot/Y6uVInVBtRWCZbj/tPds=" + [mod."golang.org/x/crypto"] + version = "v0.27.0" + hash = "sha256-8HP4+gr4DbXI22GhdgZmCWr1ijtI9HNLsTcE0kltY9o=" + [mod."golang.org/x/exp"] + version = "v0.0.0-20230905200255-921286631fa9" + hash = "sha256-CyeVwjp12Wqh4ptqfi3KHCWPzOFhE8fSrP3sMjMXvec=" + [mod."golang.org/x/mod"] + version = "v0.17.0" + hash = "sha256-CLaPeF6uTFuRDv4oHwOQE6MCMvrzkUjWN3NuyywZjKU=" + [mod."golang.org/x/net"] + version = "v0.25.0" + hash = "sha256-IjFfXLYNj27WLF7vpkZ6mfFXBnp+7QER3OQ0RgjxN54=" + [mod."golang.org/x/sync"] + version = "v0.8.0" + hash = "sha256-usvF0z7gq1vsX58p4orX+8WHlv52pdXgaueXlwj2Wss=" + [mod."golang.org/x/sys"] + version = "v0.25.0" + hash = "sha256-PXZ9EQZ7SFpcL7d3E1+KGTxziYlHEIZPfoXEbnaVD3I=" + [mod."golang.org/x/term"] + version = "v0.24.0" + hash = "sha256-PfC5psjzEWKRm1DlnBXX0ntw9OskJFrq1RRjyBa1lOk=" + [mod."golang.org/x/text"] + version = "v0.18.0" + hash = "sha256-aNvJW4gQs+MTfdz6DZqyyHQS2GJ9W8L8qKPVODPn4+k=" + [mod."golang.org/x/time"] + version = "v0.5.0" + hash = "sha256-W6RgwgdYTO3byIPOFxrP2IpAZdgaGowAaVfYby7AULU=" + [mod."golang.org/x/tools"] + version = "v0.21.1-0.20240508182429-e35e4ccd0d2d" + hash = "sha256-KfnS+3fREPAWQUBoUedPupQp9yLrugxMmmEoHvyzKNE=" + [mod."gopkg.in/ini.v1"] + version = "v1.67.0" + hash = "sha256-V10ahGNGT+NLRdKUyRg1dos5RxLBXBk1xutcnquc/+4=" + [mod."gopkg.in/yaml.v3"] + version = "v3.0.1" + hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU=" diff --git a/main.go b/main.go index 689bc26..46e6f90 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/CorefluxCommunity/zeusctl/cmd" + "github.com/CorefluxCommunity/vaultctl/cmd" ) func main() { diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go deleted file mode 100644 index ba7a7df..0000000 --- a/pkg/crypto/crypto.go +++ /dev/null @@ -1,117 +0,0 @@ -package crypto - -import ( - "encoding/base64" - "errors" - "fmt" - "syscall" - - "golang.org/x/term" - - "github.com/CorefluxCommunity/zeusctl/pkg/utils" - "github.com/CorefluxCommunity/zeusctl/pkg/yubikeypgp" - "github.com/CorefluxCommunity/zeusctl/pkg/yubikeyscard" -) - -const ( - unsealKeyLengthMin int = 16 - unsealKeyLengthMax int = 33 -) - -// decryptUnsealKeys wraps decryptUnsealKey to decrypt a slice of unseal keys -// and provide console messages. -func DecryptUnsealKeys(encryptedKeys []string) ([]string, error) { - yks := new(yubikeyscard.YubiKeys) - if err := yks.Connect(); err != nil { - return nil, err - } - - defer yks.Disconnect() - - var keys []string - for _, ek := range encryptedKeys { - key, err := decryptUnsealKey(yks, ek) - if err != nil { - utils.PrintError(err.Error()) - } else { - keys = append(keys, key) - } - } - - if len(keys) == 0 { - return nil, errors.New("no Vault unseal keys found, cannot proceed with unseal operation") - } - - msg := fmt.Sprintf("decrypted %d Vault unseal key(s)", len(keys)) - utils.PrintSuccess(msg) - - return keys, nil -} - -// decryptUnsealKey performs a base64 decode, then decrypts a PGP-encrypted -// Vault unseal key. -func decryptUnsealKey(yks *yubikeyscard.YubiKeys, cipherTxtB64 string) (unsealKey string, err error) { - encryptedKey, err := base64.StdEncoding.DecodeString(cipherTxtB64) - if err != nil { - err = errors.New("encrypted unseal key is not base64 encoded") - return - } - - retries := 1 - for retries > 0 { - md, retries, err := yubikeypgp.ReadMessage(yks, encryptedKey, promptPIN) - if err != nil { - switch { - case retries == 0: - utils.PrintFatal("PIN bank locked, no retries remaining", 1) - case retries < 0: - return "", err - default: - utils.PrintWarning(err.Error()) - continue - } - } - - serial := md.YubiKey.AppRelatedData.AID.Serial - utils.PrintInfo(fmt.Sprintf("decrypted unseal key with key ID %X found on YubiKey %x", md.DecryptedWith, serial)) - - unsealKey = string(md.Body) - break - } - - // unsealKey is a byte slice of unicode characters, divide length by 2 to get raw byte length - n := len(unsealKey) / 2 - if n < unsealKeyLengthMin { - err = fmt.Errorf("unseal key length is shorter than minimum %d bytes", unsealKeyLengthMin) - return - } - if n > unsealKeyLengthMax { - err = fmt.Errorf("unseal key length is longer than maximum %d bytes", unsealKeyLengthMax) - return - } - - return -} - -// promptPin will read a PIN from an interactive terminal. -func promptPIN() ([]byte, error) { - fmt.Print("\U0001F513 Enter YubiKey OpenPGP PIN: ") - p, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { - return []byte{}, err - } - - fmt.Println() - - if len(p) < 6 || len(p) > 127 { - return []byte{}, errors.New("expected PIN length of 6-127 characters") - } - - for i := range p { - if p[i] < 0x30 || p[i] > 0x39 { - return []byte{}, errors.New("only digits 0-9 are valid PIN characters") - } - } - - return p, nil -} diff --git a/pkg/secrets/kv.go b/pkg/secrets/kv.go index e95239e..8304636 100644 --- a/pkg/secrets/kv.go +++ b/pkg/secrets/kv.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsimple" - "github.com/CorefluxCommunity/zeusctl/pkg/vault" + "github.com/CorefluxCommunity/vaultctl/pkg/vault" ) type ContextConfig struct { @@ -130,7 +130,7 @@ func getSavedToken() (string, error) { return "", err } - tokenFile := filepath.Join(homeDir, ".zeusctl", "token") + tokenFile := filepath.Join(homeDir, ".vaultctl", "token") tokenBytes, err := os.ReadFile(tokenFile) if err != nil { return "", err diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 6807228..adfdb98 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,60 +2,17 @@ package utils import ( "fmt" - "io" "os" "strings" "github.com/logrusorgru/aurora" ) -const keyFileSizeMax int64 = 8192 - const ( printKVPadWidth int = 30 printHeaderPadWidth int = 10 ) -// ReadFile will read an unseal key file from the provided path and return a -// slice of strings containing base64-encoded PGP-encrypted Vault unseal keys. -func ReadKeyFile(path string) ([]string, error) { - buf, err := readFile(path, keyFileSizeMax) - if err != nil { - return nil, err - } - - return strings.Split(strings.TrimSpace(string(buf)), "\n"), nil -} - -// readFile will read a file from the provided path up to the byte length -// limit provided. -func readFile(path string, maxBytes int64) ([]byte, error) { - var buf []byte - - file, err := os.Open(path) - if err != nil { - return nil, err - } - - defer file.Close() - - fileStat, err := file.Stat() - if err != nil { - return nil, err - } - - if fileStat.Size() > maxBytes { - return nil, fmt.Errorf("unseal key is larger that the maximum file size of %d bytes", maxBytes) - } - - buf, err = io.ReadAll(file) - if err != nil { - return nil, err - } - - return buf, nil -} - // PrintKV will bold print the key followed by padding to the specified // total width, then the value. func PrintKV(key string, value string) { @@ -113,52 +70,3 @@ func PrintFatal(msg string, code int) { fmt.Println(aurora.Red(aurora.Bold("[fatal] ")), msg) os.Exit(code) } - -// Unique is a function that removes duplicate strings from a slice of strings -// and returns the deduplicated slice. -func Unique(orig []string) []string { - var dedup []string - - for _, s := range orig { - present := false - - for _, d := range dedup { - if s == d { - present = true - break - } - } - - if !present { - dedup = append(dedup, s) - } - } - - return dedup -} - -// fmtFingerprint accepts a byte array containing a PGP fingerprint and -// returns a formatted string that displays the fingerprint in 2-byte -// hexadecimal blocks. -func fmtFingerprint(fp [20]byte) string { - var fpString string - - for i := 0; i < len(fp); i += 2 { - fpString = strings.ToUpper(fmt.Sprintf(fpString+"%x ", fp[i:i+2])) - } - - return strings.TrimSpace(fpString[:24] + " " + fpString[24:]) -} - -// fmtFingerprintTerse accepts a byte array containing a PGP fingerprint and -// returns a short form formatted string that displays the last 8 bytes of the -// fingerprint in 2-byte hexadecimal blocks. -func FmtFingerprintTerse(fp [20]byte) string { - var fpString string - - for i := 12; i < len(fp); i += 2 { - fpString = strings.ToUpper(fmt.Sprintf(fpString+"%x", fp[i:i+2])) - } - - return fpString -} diff --git a/pkg/vault/auth.go b/pkg/vault/auth.go index 7490a21..8737a2f 100644 --- a/pkg/vault/auth.go +++ b/pkg/vault/auth.go @@ -66,7 +66,7 @@ func storeToken(token string) error { return fmt.Errorf("failed to get user home directory: %w", err) } - configDir := filepath.Join(homeDir, ".zeusctl") + configDir := filepath.Join(homeDir, ".vaultctl") err = os.MkdirAll(configDir, 0700) if err != nil { return fmt.Errorf("failed to create config directory: %w", err) diff --git a/pkg/vault/operator.go b/pkg/vault/operator.go index 373e463..fab5d2a 100644 --- a/pkg/vault/operator.go +++ b/pkg/vault/operator.go @@ -3,88 +3,10 @@ package vault import ( "fmt" - "github.com/CorefluxCommunity/zeusctl/pkg/crypto" - "github.com/CorefluxCommunity/zeusctl/pkg/utils" + "github.com/CorefluxCommunity/vaultctl/pkg/utils" "github.com/hashicorp/vault/api" ) -// connect to Vault server and execute unseal operation -func (vault *VaultClient) unseal(keys []string) (*api.SealStatusResponse, error) { - resp, err := vault.ApiClient.Sys().SealStatus() - if err != nil { - return nil, err - } - - if !resp.Initialized { - return resp, fmt.Errorf("%s - Vault server is not initialized", vault.url.Host) - } - - // if node is already unsealed, skip it - if !resp.Sealed { - utils.PrintSuccess(vault.url.Host + " - already unsealed, skipping unseal operation") - return resp, nil - } - - for _, key := range keys { - resp, err = vault.ApiClient.Sys().Unseal(key) - if err != nil { - return nil, err - } - - if !resp.Sealed { - break - } - } - - utils.PrintInfo(fmt.Sprintf("%s - provided %d unseal key share(s) toward unseal progress", vault.url.Host, len(keys))) - - resp, err = vault.ApiClient.Sys().SealStatus() - if err != nil { - return nil, err - } - - if !resp.Sealed { - utils.PrintSuccess(fmt.Sprintf("%s - Vault unsealed", vault.url.Host)) - } - - return resp, nil -} - -// connect to Vault server and execute unseal operation -func (vault *VaultClient) generateRoot(keys []string) (*api.GenerateRootStatusResponse, error) { - resp, err := vault.ApiClient.Sys().GenerateRootStatus() - if err != nil { - return nil, err - } - - // if node is already unsealed, skip it - if !resp.Started { - utils.PrintWarning(vault.url.Host + " - root token generation process has not been started") - return resp, nil - } - - nonce := resp.Nonce - for _, key := range keys { - resp, err = vault.ApiClient.Sys().GenerateRootUpdate(key, nonce) - if err != nil { - return nil, err - } - - msg := fmt.Sprintf("%s - provided unseal key share, root token generation progress: %d of %d key shares", - vault.url.Host, resp.Progress, resp.Required) - utils.PrintInfo(msg) - - if resp.Complete { - msg = fmt.Sprintf("%s - root token generation complete", vault.url.Host) - utils.PrintSuccess(msg) - - return resp, nil - } - } - - return resp, nil -} - func printSealStatus(resp *api.SealStatusResponse) { status := "unsealed" if resp.Sealed { @@ -100,86 +22,6 @@ func printSealStatus(resp *api.SealStatusResponse) { utils.PrintKV("Version", resp.Version) } -func printGenRootStatus(resp *api.GenerateRootStatusResponse) { - status := "not started" - if resp.Started { - status = "started" - - if resp.Complete { - status = "complete" - } - } - - utils.PrintKV("Root generation", status) - - if resp.Started { - utils.PrintKV("Nonce", resp.Nonce) - utils.PrintKV("Progress", fmt.Sprintf("%d/%d", resp.Progress, resp.Required)) - - if resp.PGPFingerprint != "" { - utils.PrintKV("PGP fingerprint", resp.PGPFingerprint) - } - } - - if resp.EncodedRootToken != "" { - utils.PrintKV("Encoded root token", resp.EncodedRootToken) - } -} - -// Unseal will decrypt the provided unseal key(s) and unseal each of the -// provided Vault cluster nodes. -func Unseal(vaultAddrs []string, encryptedKeys []string) error { - keys, err := crypto.DecryptUnsealKeys(encryptedKeys) - if err != nil { - return err - } - - for i, addr := range vaultAddrs { - vault, err := NewVaultClient(addr) - if err != nil { - return err - } - - resp, err := vault.unseal(keys) - if err != nil { - return err - } - - if i == len(vaultAddrs)-1 { - fmt.Println() - utils.PrintHeader("Vault Cluster Status") - printSealStatus(resp) - } - } - - return nil -} - -// GenerateRoot will decrypt the provided unseal key and enter the key share -// to progress the root generation attempt. -func GenerateRoot(vaultAddr string, encryptedKeys []string) error { - keys, err := crypto.DecryptUnsealKeys(encryptedKeys) - if err != nil { - return err - } - - vault, err := NewVaultClient(vaultAddr) - if err != nil { - return err - } - - resp, err := vault.generateRoot(keys) - if err != nil { - return err - } - - fmt.Println() - utils.PrintHeader("Root Token Generation Status") - printGenRootStatus(resp) - - return nil -} - // ListVaultStatus will output of the status the provided Vault address. func ListVaultStatus(vaultAddr string) error { vault, err := NewVaultClient(vaultAddr) diff --git a/pkg/yubikey/yubikey.go b/pkg/yubikey/yubikey.go deleted file mode 100644 index e6e84ff..0000000 --- a/pkg/yubikey/yubikey.go +++ /dev/null @@ -1,125 +0,0 @@ -package yubikey - -import ( - "encoding/binary" - "fmt" - "strings" - "time" - - "github.com/CorefluxCommunity/zeusctl/pkg/utils" - "github.com/CorefluxCommunity/zeusctl/pkg/yubikeyscard" -) - -// ListYubiKeys will output the basic details of connected YubiKeys. -func ListYubiKeys() error { - // connect YubiKey smart card interface, disconnect on return - yks := new(yubikeyscard.YubiKeys) - if err := yks.Connect(); err != nil { - return err - } - - defer yks.Disconnect() - - for i, yk := range yks.YubiKeys { - ard := yk.AppRelatedData - crd := yk.CardRelatedData - - utils.PrintHeader(fmt.Sprint(i+1, ": ", yk.ReaderLabel)) - utils.PrintKV("Manufacturer", "Yubico") - utils.PrintKV("Serial number", fmt.Sprintf("%x", ard.AID.Serial)) - - if crd.Name != nil { - utils.PrintKV("Name of cardholder", strings.Replace(fmt.Sprintf("%s", crd.Name), "<<", " ", -1)) - } - - utils.PrintKV("Signature key", fmt.Sprintf("rsa%d/%s", - binary.BigEndian.Uint16(ard.AlgoAttrSign.RSAModLen[:]), - utils.FmtFingerprintTerse(ard.Fingerprints.Sign))) - utils.PrintKV("Encryption key", fmt.Sprintf("rsa%d/%s", - binary.BigEndian.Uint16(ard.AlgoAttrEnc.RSAModLen[:]), - utils.FmtFingerprintTerse(ard.Fingerprints.Enc))) - utils.PrintKV("Authentication key", fmt.Sprintf("rsa%d/%s", - binary.BigEndian.Uint16(ard.AlgoAttrAuth.RSAModLen[:]), - utils.FmtFingerprintTerse(ard.Fingerprints.Auth))) - - if i < len(yks.YubiKeys)-1 { - fmt.Println() - } - } - - return nil -} - -// ShowYubiKey will search the connected YubiKeys for the specified serial -// number and output the details including smart card and application-related -// data. -func ShowYubiKey(sn string) error { - // connect YubiKey smart card interface, disconnect on return - yks := new(yubikeyscard.YubiKeys) - if err := yks.Connect(); err != nil { - return err - } - - defer yks.Disconnect() - - yk := yks.FindBySN(sn) - if yk == nil { - return fmt.Errorf("could not locate YubiKey that supports OpenPGP with serial number '%s'", sn) - } - - ard := yk.AppRelatedData - crd := yk.CardRelatedData - - utils.PrintHeader("YubiKey Status") - - utils.PrintKV("Reader", yk.ReaderLabel) - utils.PrintKV("Application ID", fmt.Sprintf("%x%x%x%x%x%x", - ard.AID.RID, ard.AID.App, ard.AID.Version, - ard.AID.Manufacturer, ard.AID.Serial, ard.AID.RFU)) - utils.PrintKV("Application type", "OpenPGP") - utils.PrintKV("Version", fmt.Sprintf("%d.%d", ard.AID.Version[0], ard.AID.Version[1])) - utils.PrintKV("Manufacturer", "Yubico") - utils.PrintKV("Serial number", fmt.Sprintf("%x", ard.AID.Serial)) - utils.PrintKV("Name of cardholder", strings.Replace(fmt.Sprintf("%s", crd.Name), "<<", " ", -1)) - utils.PrintKV("Language prefs", string(crd.LanguagePrefs)) - - switch crd.Salutation { - case 0x30: - utils.PrintKV("Pronoun", "unspecified") - case 0x31: - utils.PrintKV("Pronoun", "he") - case 0x32: - utils.PrintKV("Pronoun", "she") - case 0x39: - utils.PrintKV("Pronoun", "they") - } - - utils.PrintKV("Max. PIN lengths", fmt.Sprintf("%d %d %d", - ard.PWStatus.PW1MaxLenFmt, - ard.PWStatus.PW1MaxLenRC, - ard.PWStatus.PW3MaxLenFmt)) - utils.PrintKV("PIN retry counter", fmt.Sprintf("%d %d %d", - ard.PWStatus.PW1RetryCtr, - ard.PWStatus.PW1RCRetryCtr, - ard.PWStatus.PW3RetryCtr)) - - utils.PrintKV("Signature key", utils.FmtFingerprintTerse(ard.Fingerprints.Sign)) - utils.PrintKV(" algorithm", fmt.Sprintf("rsa%d", - binary.BigEndian.Uint16(ard.AlgoAttrSign.RSAModLen[:]))) - signGenDate := int64(binary.BigEndian.Uint32(ard.KeyGenDates.Sign[:])) - utils.PrintKV(" created", time.Unix(signGenDate, 0).String()) - - utils.PrintKV("Encryption key", utils.FmtFingerprintTerse(ard.Fingerprints.Enc)) - utils.PrintKV(" algorithm", fmt.Sprintf("rsa%d", - binary.BigEndian.Uint16(ard.AlgoAttrEnc.RSAModLen[:]))) - encGenDate := int64(binary.BigEndian.Uint32(ard.KeyGenDates.Enc[:])) - utils.PrintKV(" created", time.Unix(encGenDate, 0).String()) - - utils.PrintKV("Authentication key", utils.FmtFingerprintTerse(ard.Fingerprints.Auth)) - utils.PrintKV(" algorithm", fmt.Sprintf("rsa%d", - binary.BigEndian.Uint16(ard.AlgoAttrAuth.RSAModLen[:]))) - authGenDate := int64(binary.BigEndian.Uint32(ard.KeyGenDates.Auth[:])) - utils.PrintKV(" created", time.Unix(authGenDate, 0).String()) - - return nil -} diff --git a/pkg/yubikeypgp/yubikeypgp.go b/pkg/yubikeypgp/yubikeypgp.go deleted file mode 100644 index 6a45462..0000000 --- a/pkg/yubikeypgp/yubikeypgp.go +++ /dev/null @@ -1,223 +0,0 @@ -package yubikeypgp - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - - "golang.org/x/crypto/openpgp/packet" - - "github.com/CorefluxCommunity/zeusctl/pkg/yubikeyscard" -) - -const ( - sessionKeyLength = 16 - encryptedKeyPacketHeaderLength = 3 - encryptedKeyPacketKeyInfoLength = 12 - symmetricallyEncryptedVersion = 1 -) - -type PinPromptFunction func() ([]byte, error) - -// MessageDetails contains the result of parsing an OpenPGP encrypted and/or -// signed message. -type MessageDetails struct { - IsEncrypted bool // true if the message was encrypted. - DecryptedWith uint64 // key ID of decryption key used to decrypt session key - YubiKey *yubikeyscard.YubiKey // YubiKey containing private key used to decrypt session key - Body []byte // the contents of the message. -} - -type encryptedKeyPacket struct { - tag uint8 - length int - version uint8 - keyID uint64 - keyAlgo uint8 - keySize uint16 - encryptedBytes []byte -} - -// ReadMessage will decrypt a PGP-encrypted message by using a YubiKey to first -// obtain the session key (DEK). Decrypt will then decrypt the symmetrically -// encrypted portion of the message and return the resultant plain text. -// In the event of an incorrect PIN, Decrypt will return an empty byte array -// and the number of remaining PIN retries. -func ReadMessage(yks *yubikeyscard.YubiKeys, msg []byte, prompt PinPromptFunction) (md *MessageDetails, retries int, err error) { - md = new(MessageDetails) - retries = -1 - - // read encrypted key packet fields and deserialize to struct - ek, err := readEncKeyPacket(bytes.NewReader(msg)) - if err != nil { - return - } - - md.IsEncrypted = true - - // locate YubiKey with matching decryption key - yk := yks.FindByKeyID(ek.keyID) - if yk == nil { - err = fmt.Errorf("decryption key %X could not be found on any YubiKeys", ek.keyID) - return - } - - // check if PIN is cached, if not retrieve PIN input from user, then validate format - pin := yk.CachedPIN(2) - - if pin == nil { - pin, err = prompt() - if err != nil { - return - } - } - - // verify the PIN (bank 2) with the OpenPGP smart card applet - retries, err = yubikeyscard.Verify(yk.Card, 2, pin) - if err != nil { - return - } else { - // add verified PIN to the cache - yk.SetCachedPIN(2, pin) - } - - // decipher the session key - sk, err := yubikeyscard.Decipher(yk.Card, ek.encryptedBytes) - if err != nil { - return - } - - if len(sk) != (sessionKeyLength + 3) { - err = errors.New("unable to decipher PGP session key") - return - } - - // get cipher function from first octect - c := packet.CipherFunction(sk[0]) - if c != packet.CipherAES128 { - err = errors.New("unsupported cipher function, only AES-128-CFB supported") - return - } - - // after cipher function, the next 16 bytes contain the session key - sessionKey := sk[1 : sessionKeyLength+1] - - // read the message from the symmetrically encrypted packet using session key - md.Body, err = readSymEncPacket(bytes.NewReader(msg[ek.length:]), sessionKey, c) - if err != nil { - return - } - - md.DecryptedWith = ek.keyID - md.YubiKey = yk - - return -} - -func readHeader(r io.Reader) (tag uint8, length int, contents io.Reader, err error) { - var buf [3]byte - - _, err = io.ReadFull(r, buf[:]) - if err != nil { - return - } - - if buf[0]&0xc0 != 0xc0 { - err = errors.New("invalid PGP packet header, only new format supported") - return - } - - if buf[1] < 192 && buf[1] > 223 { - err = errors.New("invalid PGP packet length, expected two-octect length format") - return - } - - tag = buf[0] & 0x1f - length = int(binary.BigEndian.Uint16([]byte{buf[1] - 192, buf[2]})+192) + 3 - contents = r - return tag, length, contents, nil -} - -func readEncKeyPacket(r io.Reader) (ek encryptedKeyPacket, err error) { - var buf [encryptedKeyPacketKeyInfoLength]byte - - tag, length, contents, err := readHeader(r) - if err != nil { - return - } - - if tag != 1 { - err = errors.New("invalid PGP packet type, only encrypted key and symmetrically encrypted packets supported") - return - } - - n, err := io.ReadFull(contents, buf[:]) - if err != nil { - return - } - - if n != encryptedKeyPacketKeyInfoLength { - err = errors.New("invalid PGP packet, body too short") - return - } - - if buf[0] != 3 { - err = errors.New("invalid PGP encrypted key packet, only version 3 supported") - return - } - - if buf[9] != uint8(packet.PubKeyAlgoRSA) { - err = errors.New("invalid PGP encrypted key packet, only RSA supported") - return - } - - ek.tag = tag - ek.length = length - ek.version = buf[0] - ek.keyID = binary.BigEndian.Uint64(buf[1:9]) - ek.keyAlgo = buf[9] - ek.keySize = binary.BigEndian.Uint16(buf[10:12]) - - ek.encryptedBytes = make([]byte, length-(encryptedKeyPacketHeaderLength+encryptedKeyPacketKeyInfoLength)) - if _, err = io.ReadFull(contents, ek.encryptedBytes); err != nil { - return - } - - return ek, nil -} - -// readSymEncPacket decrypts the symmetrically encypted portion of the message -// with the provided session key and cipher function. -func readSymEncPacket(r io.Reader, key []byte, cipherFunc packet.CipherFunction) ([]byte, error) { - packets := packet.NewReader(r) - - for { - p, err := packets.Next() - if err != nil { - return nil, err - } - - switch p := p.(type) { - case *packet.SymmetricallyEncrypted: - decrypted, err := p.Decrypt(cipherFunc, key) - if err != nil { - return nil, err - } - - if err := packets.Push(decrypted); err != nil { - return nil, err - } - case *packet.LiteralData: - body, err := io.ReadAll(p.Body) - if err != nil { - return nil, err - } - - return body, nil - default: - return nil, errors.New("unexpected PGP packet type encountered") - } - } -} diff --git a/pkg/yubikeyscard/apdu.go b/pkg/yubikeyscard/apdu.go deleted file mode 100644 index eeba0c8..0000000 --- a/pkg/yubikeyscard/apdu.go +++ /dev/null @@ -1,125 +0,0 @@ -package yubikeyscard - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - - "github.com/ebfe/scard" -) - -var appID = []byte{0xd2, 0x76, 0x00, 0x01, 0x24, 0x01} // OpenPGP applet ID - -// commandAPDU represents an application data unit sent to a smartcard. -type commandAPDU struct { - cla, ins, p1, p2 uint8 // Class, Instruction, Parameter 1, Parameter 2 - data []byte // Command data - le uint8 // Command data length - pib bool // Padding indicator byte present - elf bool // Use extended length fields -} - -// responseAPDU represents an application data unit received from a smart card -type responseAPDU struct { - data []byte // response data - sw1, sw2 uint8 // status words 1 and 2 -} - -// serialize serializes a command APDU. -func (ca commandAPDU) serialize() ([]byte, error) { - buf := new(bytes.Buffer) - - // write 4 header bytes to buffer - if _, err := buf.Write([]byte{ca.cla, ca.ins, ca.p1, ca.p2}); err != nil { - return nil, err - } - - // if a payload exists, calculate the length, prepend it to the payload, and write to buffer - if len(ca.data) > 0 { - lc := len(ca.data) - - // subtract one byte from length if padding indicator byte present - if ca.pib { - lc-- - } - - // check if extended length fields (3 bytes) should be used - if ca.elf { - lcElf := make([]byte, 2) - binary.BigEndian.PutUint16(lcElf, uint16(lc)) - - if _, err := buf.Write(append([]byte{0}, lcElf...)); err != nil { - return nil, err - } - } else { - if _, err := buf.Write([]byte{uint8(lc)}); err != nil { - return nil, err - } - } - - if _, err := buf.Write(ca.data); err != nil { - return nil, err - } - } - - if _, err := buf.Write([]byte{ca.le}); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -// transmit will send the serialized APDU command to the applet. -func (ca commandAPDU) transmit(card *scard.Card) (responseAPDU, error) { - ra := new(responseAPDU) - - cmd, err := ca.serialize() - if err != nil { - return *ra, err - } - - rsp, err := card.Transmit(cmd) - if err != nil { - return *ra, err - } - - if err = ra.deserialize(rsp); err != nil { - return *ra, err - } - - return *ra, nil -} - -// deserialize deserializes a response APDU. -func (ra *responseAPDU) deserialize(data []byte) error { - if len(data) < 2 { - return fmt.Errorf("can not deserialize data: payload too short (%d < 2)", len(data)) - } - - r := bytes.NewReader(data) - - ra.data = make([]byte, len(data)-2) - _, err := io.ReadFull(r, ra.data) - if err != nil { - return err - } - - sw := make([]byte, 2) - _, err = io.ReadFull(r, sw) - if err != nil { - return err - } - - ra.sw1 = sw[0] - ra.sw2 = sw[1] - - return nil -} - -func (ra *responseAPDU) success() bool { - success := []byte{0x90, 0x00} - status := []byte{ra.sw1, ra.sw2} - - return bytes.Equal(status, success) -} diff --git a/pkg/yubikeyscard/data_object.go b/pkg/yubikeyscard/data_object.go deleted file mode 100644 index 2516b89..0000000 --- a/pkg/yubikeyscard/data_object.go +++ /dev/null @@ -1,180 +0,0 @@ -package yubikeyscard - -import ( - "bytes" - "encoding/binary" -) - -const ( - MaxResponseLength uint16 = 256 -) - -type DataObject struct { - tag uint16 - constructed bool - parent uint16 - binary bool - extLen uint8 - desc string -} - -var doURL = DataObject{tag: 0x5F50, constructed: false, parent: 0, binary: false, extLen: 2, desc: "URL"} -var doHistBytes = DataObject{tag: 0x5F52, constructed: false, parent: 0, binary: true, extLen: 0, desc: "Historical Bytes"} -var doCardRelData = DataObject{tag: 0x0065, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Cardholder Related Data"} -var doName = DataObject{tag: 0x005B, constructed: false, parent: 0x65, binary: false, extLen: 0, desc: "Name"} -var doLangPrefs = DataObject{tag: 0x5F2D, constructed: false, parent: 0x65, binary: false, extLen: 0, desc: "Language preferences"} -var doSalutation = DataObject{tag: 0x5F35, constructed: false, parent: 0x65, binary: false, extLen: 0, desc: "Salutation"} -var doAppRelData = DataObject{tag: 0x006E, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Application Related Data"} -var doLoginData = DataObject{tag: 0x005E, constructed: false, parent: 0, binary: true, extLen: 2, desc: "Login Data"} -var doAID = DataObject{tag: 0x004F, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Application Idenfifier (AID)"} -var doDiscrDOs = DataObject{tag: 0x0073, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Discretionary Data Objects"} -var doCardCaps = DataObject{tag: 0x0047, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Card Capabilities"} -var doExtLenCaps = DataObject{tag: 0x00C0, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Extended Card Capabilities"} -var doAlgoAttrSign = DataObject{tag: 0x00C1, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Algorithm Attributes Signature"} -var doAlgoAttrEnc = DataObject{tag: 0x00C2, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Algorithm Attributes Encryption"} -var doAlgoAttrAuth = DataObject{tag: 0x00C3, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Algorithm Attributes Authentication"} -var doPWStatus = DataObject{tag: 0x00C4, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Password Status Bytes"} -var doFingerprints = DataObject{tag: 0x00C5, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Fingerprints"} -var doCAFingerprints = DataObject{tag: 0x00C6, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "CA Fingerprints"} -var doKeyGenDate = DataObject{tag: 0x00CD, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "Generation times of key pairs"} -var doSecSuppTmpl = DataObject{tag: 0x007A, constructed: true, parent: 0, binary: true, extLen: 0, desc: "Security Support Template"} -var doDigSigCtr = DataObject{tag: 0x0093, constructed: false, parent: 0x7A, binary: true, extLen: 0, desc: "Digital Signature Counter"} -var doPrivateDO1 = DataObject{tag: 0x0101, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 1"} -var doPrivateDO2 = DataObject{tag: 0x0102, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 2"} -var doPrivateDO3 = DataObject{tag: 0x0103, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 3"} -var doPrivateDO4 = DataObject{tag: 0x0104, constructed: false, parent: 0, binary: false, extLen: 2, desc: "Private DO 4"} -var doCardholderCrt = DataObject{tag: 0x7F21, constructed: true, parent: 0, binary: true, extLen: 1, desc: "Cardholder certificate"} - -// V3.0 -var doGenFeatMgmt = DataObject{tag: 0x7F74, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "General Feature Management"} -var doAESKeyData = DataObject{tag: 0x00D5, constructed: false, parent: 0, binary: true, extLen: 0, desc: "AES key data"} -var doUIFSig = DataObject{tag: 0x00D6, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Signature"} -var doUIFDec = DataObject{tag: 0x00D7, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Decryption"} -var doUIFAut = DataObject{tag: 0x00D8, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Authentication"} -var doUIFAtt = DataObject{tag: 0x00D8, constructed: false, parent: 0x6E, binary: true, extLen: 0, desc: "UIF for Yubico Attestation key"} -var doKDFDO = DataObject{tag: 0x00F9, constructed: false, parent: 0, binary: true, extLen: 0, desc: "KDF data object"} -var doAlgoInfo = DataObject{tag: 0x00FA, constructed: false, parent: 0, binary: true, extLen: 2, desc: "Algorithm Information"} - -var DataObjects = []DataObject{ - doURL, doHistBytes, doCardRelData, doName, doLangPrefs, doSalutation, - doAppRelData, doLoginData, doAID, doDiscrDOs, doCardCaps, doExtLenCaps, - doAlgoAttrSign, doAlgoAttrEnc, doAlgoAttrAuth, doPWStatus, doFingerprints, - doCAFingerprints, doKeyGenDate, doSecSuppTmpl, doDigSigCtr, doPrivateDO1, - doPrivateDO2, doPrivateDO3, doPrivateDO4, doCardholderCrt, doGenFeatMgmt, - doAESKeyData, doUIFSig, doUIFDec, doUIFAut, doUIFAtt, doKDFDO, doAlgoInfo, -} - -func (do *DataObject) tagBytes() []byte { - b := make([]byte, 2) - binary.BigEndian.PutUint16(b, uint16(do.tag)) - return b -} - -func (do *DataObject) tagP1() byte { - return do.tagBytes()[0] -} - -func (do *DataObject) tagP2() byte { - return do.tagBytes()[1] -} - -func (do *DataObject) parentBytes() []byte { - b := make([]byte, 2) - binary.BigEndian.PutUint16(b, uint16(do.parent)) - return b -} - -func (do *DataObject) children() []DataObject { - var c []DataObject - - for _, d := range DataObjects { - if bytes.Equal(d.parentBytes(), do.tagBytes()) { - c = append(c, d) - } - } - - return c -} - -// doFindTLV function is based on GnuPG implementation of do_find_tlv in common/tlv.c -func doFindTLV(data []byte, tag uint16, nestLevel int) []byte { - var o int = 0 - var n int = len(data) - var tagLen uint16 - var thisTag uint16 - var tagFound bool - var composite bool - - for !tagFound { - if n < 2 { // Buffer definitely too short for tag and length. - return nil - } - - if data[o] == 0 || data[o] == 0xff { // Skip optional filler between TLV objects. - o++ - n-- - } - - composite = (data[o] & 0x20) != 0 - - if (data[o] & 0x1f) == 0x1f { // more tag bytes to follow - o++ - n-- - - if n < 2 { // Buffer definitely too short for tag and length. - return nil - } - if (data[o] & 0x1f) == 0x1f { // We support only up to 2 bytes. - return nil - } - - thisTag = binary.BigEndian.Uint16([]byte{data[o-1], data[o] & 0x7f}) - } else { - thisTag = binary.BigEndian.Uint16([]byte{0, data[o]}) - } - - tagLen = binary.BigEndian.Uint16([]byte{0, data[o+1]}) - o += 2 - n -= 2 - if tagLen < 0x80 { - // do nothing - } else if tagLen == 0x81 { // One byte length follows. - if n != 0 { // we expected 1 more bytes with the length - return nil - } - - tagLen = binary.BigEndian.Uint16([]byte{0, data[o]}) - o++ - n-- - } else if tagLen == 0x82 { // Two byte length follows - if n < 2 { // We expected 2 more bytes with the length. - return nil - } - - tagLen = binary.BigEndian.Uint16([]byte{data[o], data[o+1]}) - o += 2 - n -= 2 - } else { // APDU limit is 65535, thus it does not make sense to assume longer length fields. */ - return nil - } - - if composite && nestLevel < 100 { // Dive into this composite DO after checking for a too deep nesting - tmpData := doFindTLV(data[o:], tag, nestLevel+1) - - if len(tmpData) > 0 { - return tmpData - } - } - - if thisTag == tag { - tagFound = true - } else if int(tagLen) > n { // Buffer too short to skip to the next tag. - return nil - } else { - o += int(tagLen) - n -= int(tagLen) - } - } - - return data[o : o+int(tagLen)] -} diff --git a/pkg/yubikeyscard/iso7816.go b/pkg/yubikeyscard/iso7816.go deleted file mode 100644 index 09e9dd6..0000000 --- a/pkg/yubikeyscard/iso7816.go +++ /dev/null @@ -1,141 +0,0 @@ -package yubikeyscard - -import ( - "errors" - "fmt" - - "github.com/ebfe/scard" -) - -// Decipher data with private key on smart card -func Decipher(card *scard.Card, data []byte) ([]byte, error) { - ca := commandAPDU{ - cla: 0, - ins: 0x2a, - p1: 0x80, - p2: 0x86, - data: append(append([]byte{0}, data...), 1), // prepend RSA padding indicator byte and append footer - le: 0, - pib: true, - elf: true, - } - - if len(data)%16 != 0 { - return nil, errors.New("decipher input blocks should be in multiples of 16 bytes") - } - - ra, err := ca.transmit(card) - if err != nil { - return nil, err - } - - if !ra.success() { - return nil, errors.New("decipher operation unsuccessful") - } - - return ra.data, nil -} - -func GetData(card *scard.Card, do DataObject) ([]byte, error) { - data := []byte{} - - ca := commandAPDU{ - cla: 0, - ins: 0xca, - p1: do.tagP1(), - p2: do.tagP2(), - le: 0, - } - - ra, err := ca.transmit(card) - if err != nil { - return nil, err - } - - data = append(data, ra.data...) - - for !ra.success() { - if ra.sw1 == 0x61 { - ca = commandAPDU{ - cla: 0, - ins: 0xc0, - p1: 0, - p2: 0, - le: 0, - } - - ra, err = ca.transmit(card) - if err != nil { - return nil, err - } - - data = append(data, ra.data...) - } else { - return nil, errors.New("error occurred, could not get data segment") - } - } - - return data, nil -} - -func SelectApp(card *scard.Card) error { - ca := commandAPDU{ - cla: 0, - ins: 0xa4, - p1: 0x04, - p2: 0, - data: appID, - le: 0, - } - - ra, err := ca.transmit(card) - if err != nil { - return err - } - - if !ra.success() { - return errors.New("this YubiKey does not support OpenPGP") - } - - return nil -} - -// Verify is used to check the PIN for the provided bank and set appropriate -// access. Verify will return the number of tries remaining. If an error other -// than an invalid PIN occurs, -1 will be returned for the number of remaining -// retries. -func Verify(card *scard.Card, bank uint8, pin []byte) (int, error) { - ca := commandAPDU{ - cla: 0, - ins: 0x20, - p1: 0, - p2: 0x80 + bank, - data: pin, - le: 0, - } - - if bank < 1 || bank > 3 { - return -1, errors.New("invalid PIN bank, use banks 1-3") - } - - ra, err := ca.transmit(card) - if err != nil { - return -1, err - } - - if !ra.success() { - retries, err := pw1PINRetries(card) - if err != nil { - return -1, err - } - - verb := "retry" - if retries > 1 { - verb = "retries" - } - - return retries, fmt.Errorf("invalid PIN, %d %s remaining", retries, verb) - } - - return 3, nil -} diff --git a/pkg/yubikeyscard/yubikeyscard.go b/pkg/yubikeyscard/yubikeyscard.go deleted file mode 100644 index bae647b..0000000 --- a/pkg/yubikeyscard/yubikeyscard.go +++ /dev/null @@ -1,448 +0,0 @@ -package yubikeyscard - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "regexp" - "time" - - "github.com/ebfe/scard" -) - -const ( - AlgoIdRSA uint8 = 1 - AlgoIdECDH uint8 = 12 - AlgoIdECDSA uint8 = 13 -) - -const ( - scardPresentTimeout int = 1 - scardGetStatusChangeTimeout int = 5 -) - -var yubikeyManufacturerID = [2]byte{0, 6} - -type YubiKeys struct { - YubiKeys []*YubiKey - Context *scard.Context -} - -type YubiKey struct { - Card *scard.Card - ReaderLabel string - CardRelatedData CardRelatedData - AppRelatedData AppRelatedData - PINCache [3][]byte -} - -type CardRelatedData struct { - Name []byte - LanguagePrefs []byte - Salutation byte -} - -type AppRelatedData struct { - AID AID - AlgoAttrSign AlgoAttr - AlgoAttrEnc AlgoAttr - AlgoAttrAuth AlgoAttr - PWStatus PWStatus - Fingerprints Fingerprints - KeyGenDates KeyGenDates -} - -type AID struct { - RID [5]byte - App byte - Version [2]byte - Manufacturer [2]byte - Serial [4]byte - RFU [2]byte -} - -type AlgoAttr struct { - ID byte - RSAModLen [2]byte - RSAPubKeyExpLen [2]byte - ECurveOID []byte - PrivKeyImpFmt byte -} - -type PWStatus struct { - PW1Validity byte - PW1MaxLenFmt byte - PW1MaxLenRC byte - PW3MaxLenFmt byte - PW1RetryCtr byte - PW1RCRetryCtr byte - PW3RetryCtr byte -} - -type Fingerprints struct { - Sign [20]byte - Enc [20]byte - Auth [20]byte -} - -type KeyGenDates struct { - Sign [4]byte - Enc [4]byte - Auth [4]byte -} - -// Connect establishes the system context and opens sessions with all available -// YubiKeys. -func (yks *YubiKeys) Connect() error { - // establish system context - ctx, err := scard.EstablishContext() - if err != nil { - return err - } - - yks.Context = ctx - - // list available smart card readers - readers, err := ctx.ListReaders() - if err != nil { - return err - } - - // wait for all smards card to reach present state - presentReaders, err := waitUntilCardsPresent(ctx, readers) - if err != nil { - return err - } - - // ignore other smart cards - for _, r := range presentReaders { - yk := new(YubiKey) - - // connect to card - card, err := ctx.Connect(r, scard.ShareExclusive, scard.ProtocolAny) - if err != nil { - return err - } - - // skip smart cards that do not support the OpenPGP applet - if err = SelectApp(card); err != nil { - continue - } - - // build YubiKey struct - re := regexp.MustCompile("^(.*?) [0-9]{2}$") - yk.ReaderLabel = re.ReplaceAllString(r, "$1") - yk.Card = card - - if err = yk.refreshCardRelatedData(); err != nil { - return err - } - - if err = yk.refreshAppRelatedData(); err != nil { - return err - } - - // skip smart cards not manufactured by YubiCo - if yk.AppRelatedData.AID.Manufacturer != yubikeyManufacturerID { - continue - } - - yks.YubiKeys = append(yks.YubiKeys, yk) - } - - // if no YubiKeys are found, release context, and throw error - if len(yks.YubiKeys) == 0 { - // Release reader context - err = ctx.Release() - if err != nil { - return err - } - - return errors.New("no YubiKeys found") - } - - return nil -} - -// Disconnect will reset all open sessions smart cards and release the system -// context. -func (yks *YubiKeys) Disconnect() error { - for _, yk := range yks.YubiKeys { - // Disconnect card by sending reset command - err := yk.Card.Disconnect(scard.ResetCard) - if err != nil { - return err - } - } - - // Release reader context - err := yks.Context.Release() - if err != nil { - return err - } - - return nil -} - -// FindBySN will search the connected YubiKeys for matching serial numbers and -// if found, will return a pointer to that YubiKey. -func (yks *YubiKeys) FindBySN(sn string) *YubiKey { - for _, yk := range yks.YubiKeys { - if sn == fmt.Sprintf("%x", yk.AppRelatedData.AID.Serial) { - return yk - } - } - - return nil -} - -// FindByKeyID will search the connected YubiKeys for a matching PGP key ID and -// if found, will return a pointer to that YubiKey. -func (yks *YubiKeys) FindByKeyID(keyID uint64) *YubiKey { - for _, yk := range yks.YubiKeys { - fps := yk.AppRelatedData.Fingerprints - - for _, fp := range [][20]byte{fps.Sign, fps.Enc, fps.Auth} { - if binary.BigEndian.Uint64(fp[12:20]) == keyID { - return yk - } - } - - } - - return nil -} - -// CachedPIN returns the cached PIN for the provided bank if available. If PIN is not -// cached, CachedPIN will return nil. -func (yk *YubiKey) CachedPIN(bank uint8) []byte { - if bank < 1 || bank > 3 { - return nil - } - - return yk.PINCache[bank-1] -} - -// SetCachedPIN adds a verified PIN to the cache. -func (yk *YubiKey) SetCachedPIN(bank uint8, pin []byte) error { - if bank < 1 || bank > 3 { - return errors.New("invalid PIN bank, use banks 1-3") - } - - yk.PINCache[bank-1] = pin - return nil -} - -func waitUntilCardsPresent(ctx *scard.Context, readers []string) ([]string, error) { - start := time.Now() - var presentReaders []string - rs := make([]scard.ReaderState, len(readers)) - - for i := range rs { - rs[i].Reader = readers[i] - rs[i].CurrentState = scard.StateUnaware - } - - for { - ready := 0 - for i := range rs { - rs[i].CurrentState = rs[i].EventState - if rs[i].EventState&scard.StatePresent != 0 { - ready++ - - for _, pr := range presentReaders { - if pr == readers[i] { - continue - } - } - - presentReaders = append(presentReaders, readers[i]) - } - - } - - if ready == len(readers) { - return presentReaders, nil - } - - err := ctx.GetStatusChange(rs, time.Duration(scardPresentTimeout)*time.Second) - if err != nil { - return nil, err - } - - if time.Since(start) > time.Duration(scardGetStatusChangeTimeout)*time.Second { - return presentReaders, nil - } - } -} - -func pw1PINRetries(card *scard.Card) (int, error) { - data, err := GetData(card, doPWStatus) - if err != nil { - return 0, err - } - - return int(data[4]), nil -} - -func (yk *YubiKey) refreshCardRelatedData() error { - crd := &yk.CardRelatedData - - data, err := GetData(yk.Card, doCardRelData) - if err != nil { - return err - } - - for _, c := range doCardRelData.children() { - d := doFindTLV(data, c.tag, 1) - r := bytes.NewReader(d) - - switch c.tag { - case doName.tag: - crd.Name = make([]byte, r.Len()) - if _, err := io.ReadFull(r, crd.Name); err != nil { - return err - } - case doLangPrefs.tag: - crd.LanguagePrefs = make([]byte, r.Len()) - if _, err := io.ReadFull(r, crd.LanguagePrefs); err != nil { - return err - } - case doSalutation.tag: - if crd.Salutation, err = r.ReadByte(); err != nil { - return err - } - } - if err != nil { - return err - } - } - - return nil -} - -func (yk *YubiKey) refreshAppRelatedData() error { - ard := &yk.AppRelatedData - - data, err := GetData(yk.Card, doAppRelData) - if err != nil { - return err - } - - for _, c := range doAppRelData.children() { - cData := doFindTLV(data, c.tag, 1) - buf := bytes.NewReader(cData) - - switch c.tag { - case doAID.tag: - err = ard.AID.deserialize(buf) - case doAlgoAttrSign.tag: - err = ard.AlgoAttrSign.deserialize(buf) - case doAlgoAttrEnc.tag: - err = ard.AlgoAttrEnc.deserialize(buf) - case doAlgoAttrAuth.tag: - err = ard.AlgoAttrAuth.deserialize(buf) - case doPWStatus.tag: - err = ard.PWStatus.deserialize(buf) - case doFingerprints.tag: - err = ard.Fingerprints.deserialize(buf) - case doKeyGenDate.tag: - err = ard.KeyGenDates.deserialize(buf) - } - - if err != nil { - return err - } - } - - return nil -} - -func (aid *AID) deserialize(r *bytes.Reader) (err error) { - if _, err = io.ReadFull(r, aid.RID[:]); err != nil { - return - } - - if aid.App, err = r.ReadByte(); err != nil { - return err - } - - for _, a := range []*[2]byte{&aid.Version, &aid.Manufacturer} { - if _, err := io.ReadFull(r, a[:]); err != nil { - return err - } - } - - if _, err := io.ReadFull(r, aid.Serial[:]); err != nil { - return err - } - - if _, err := io.ReadFull(r, aid.RFU[:]); err != nil { - return err - } - - return nil -} - -func (aa *AlgoAttr) deserialize(r *bytes.Reader) (err error) { - if aa.ID, err = r.ReadByte(); err != nil { - return err - } - - switch aa.ID { - case AlgoIdRSA: - for _, a := range []*[2]byte{&aa.RSAModLen, &aa.RSAPubKeyExpLen} { - if _, err := io.ReadFull(r, a[:]); err != nil { - return err - } - } - case AlgoIdECDH, AlgoIdECDSA: - aa.ECurveOID = make([]byte, r.Len()-1) - if _, err := io.ReadFull(r, aa.ECurveOID); err != nil { - return err - } - - } - - if aa.PrivKeyImpFmt, err = r.ReadByte(); err != nil { - return err - } - - return nil -} - -func (pws *PWStatus) deserialize(r *bytes.Reader) (err error) { - pwb := []*byte{&pws.PW1Validity, &pws.PW1MaxLenFmt, &pws.PW1MaxLenRC, - &pws.PW3MaxLenFmt, &pws.PW1RetryCtr, &pws.PW1RCRetryCtr, &pws.PW3RetryCtr} - - for _, p := range pwb { - *p, err = r.ReadByte() - if err != nil { - return - } - } - - return -} - -func (fps *Fingerprints) deserialize(r *bytes.Reader) error { - for _, fp := range []*[20]byte{&fps.Sign, &fps.Enc, &fps.Auth} { - if _, err := io.ReadFull(r, fp[:]); err != nil { - return err - } - } - - return nil -} - -func (kgds *KeyGenDates) deserialize(r *bytes.Reader) error { - for _, kgd := range []*[4]byte{&kgds.Sign, &kgds.Enc, &kgds.Auth} { - if _, err := io.ReadFull(r, kgd[:]); err != nil { - return err - } - } - - return nil -} From f8dfe41911a9a6d7df9987c696825b3b84bdbc28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 00:25:06 +0000 Subject: [PATCH 6/6] Bump golang.org/x/crypto from 0.27.0 to 0.29.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.29.0. - [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.29.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 10 ++++------ go.sum | 20 ++++++++------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index f18e50f..6f6c729 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,12 @@ module github.com/CorefluxCommunity/vaultctl go 1.21.13 require ( - github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 github.com/hashicorp/hcl/v2 v2.22.0 github.com/hashicorp/vault/api v1.15.0 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - golang.org/x/crypto v0.27.0 - golang.org/x/term v0.24.0 ) require ( @@ -47,12 +44,13 @@ require ( github.com/zclconf/go-cty v1.13.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.29.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index ef91e2a..210a582 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7 h1:HYAhfGa9dEemCZgGZWL5AvVsctBCsHxl2CI0HUXzHQE= -github.com/ebfe/scard v0.0.0-20230420082256-7db3f9b7c8a7/go.mod h1:BkYEeWL6FbT4Ek+TcOBnPzEKnL7kOq2g19tTQXkorHY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= @@ -128,23 +126,21 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=