Skip to content

Commit

Permalink
cmd: support private keys in kes identity (#461)
Browse files Browse the repository at this point in the history
* cmd: support private keys in `kes identity`

This commit is part of the CLI refactoring https://github.com/minio/kes/milestone/6

This commit adds support for parsing PEM private key files
and printing their private key as API key.

Now, the `kes identity` command is now able to generate API keys, print
identities of API keys or certificates and convert private key files to
API keys. The existing sub-commands `new`, `of`, `info` `ls` are still
supported.

As of now, only private keys of type Ed25519 are supported.

Signed-off-by: Andreas Auernhammer <github@aead.dev>

* Update cmd/kes/identity.go

Co-authored-by: Shubhendu <shubhendu@minio.io>
Signed-off-by: Andreas Auernhammer <github@aead.dev>

* Update internal/cli/term.go

Co-authored-by: Harshavardhana <harsha@minio.io>
Signed-off-by: Andreas Auernhammer <github@aead.dev>

---------

Signed-off-by: Andreas Auernhammer <github@aead.dev>
Co-authored-by: Shubhendu <shubhendu@minio.io>
Co-authored-by: Harshavardhana <harsha@minio.io>
  • Loading branch information
3 people committed May 5, 2024
1 parent e06e710 commit 802ce81
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 181 deletions.
6 changes: 3 additions & 3 deletions cmd/kes/autocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func installAutoCompletion() {

home, err := os.UserHomeDir()
if err != nil {
cli.Fatalf("failed to detect home directory: %v")
cli.Fatalf("failed to detect home directory: %v", home)
}
if home == "" {
home = "~"
Expand All @@ -119,11 +119,11 @@ func installAutoCompletion() {

binaryPath, err := os.Executable()
if err != nil {
cli.Fatalf("failed to detect binary path: %v")
cli.Fatalf("failed to detect binary path: %v", err)
}
binaryPath, err = filepath.Abs(binaryPath)
if err != nil {
cli.Fatalf("failed to turn binary path into an absolute path: %v")
cli.Fatalf("failed to turn binary path into an absolute path: %v", err)
}

var (
Expand Down
142 changes: 122 additions & 20 deletions cmd/kes/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ package main
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
Expand All @@ -27,12 +29,14 @@ import (
"github.com/minio/kes/internal/cli"
"github.com/minio/kes/internal/https"
"github.com/minio/kms-go/kes"
sdk "github.com/minio/kms-go/kes"
flag "github.com/spf13/pflag"
"golang.org/x/term"
)

const identityCmdUsage = `Usage:
kes identity <command>
kes identity [KEY | FILE]
Commands:
new Create a new KES identity.
Expand All @@ -45,36 +49,134 @@ Options:
`

func identityCmd(args []string) {
cmd := flag.NewFlagSet(args[0], flag.ContinueOnError)
cmd.Usage = func() { fmt.Fprint(os.Stderr, identityCmdUsage) }
if len(args) >= 2 {
subCmds := commands{
"new": newIdentityCmd,
"of": ofIdentityCmd,
"info": infoIdentityCmd,
"ls": lsIdentityCmd,
}
if cmd, ok := subCmds[args[1]]; ok {
cmd(args[1:])
return
}
}

subCmds := commands{
"new": newIdentityCmd,
"of": ofIdentityCmd,
"info": infoIdentityCmd,
"ls": lsIdentityCmd,
flags := flag.NewFlagSet(args[0], flag.ContinueOnError)
flags.Usage = func() { fmt.Fprint(os.Stderr, identityCmdUsage) }
if err := flags.Parse(args[1:]); err != nil {
if errors.Is(err, flag.ErrHelp) {
os.Exit(2)
}
cli.Exit(err)
}

if len(args) < 2 {
cmd.Usage()
os.Exit(2)
if flags.NArg() > 1 {
cli.Exit("too many arguments")
}
if cmd, ok := subCmds[args[1]]; ok {
cmd(args[1:])

if flags.NArg() == 0 {
key, err := sdk.GenerateAPIKey(nil)
if err != nil {
cli.Exitf("failed to generate API key: %v", err)
}

if !cli.IsTerminal() {
fmt.Print(key)
return
}

buf := &strings.Builder{}
fmt.Fprintln(buf, "Your API key:")
fmt.Fprintln(buf)
fmt.Fprintln(buf, " ", tui.NewStyle().Bold(true).Render(key.String()))
fmt.Fprintln(buf)
fmt.Fprintln(buf, "This is the only time it is shown. Keep it secret and secure!")
fmt.Fprintln(buf)
fmt.Fprintln(buf, "Your API key's identity:")
fmt.Fprintln(buf)
fmt.Fprintln(buf, " ", tui.NewStyle().Bold(true).Render(key.Identity().String()))
fmt.Fprintln(buf)
fmt.Fprintln(buf, "The identity is not a secret and can be shared securely.")
fmt.Fprintln(buf, "Peers need your identity in order to verify your API key.")
fmt.Fprintln(buf)
fmt.Fprintln(buf, "This identity can be re-computed again via:")
fmt.Fprintln(buf)
fmt.Fprintf(buf, " $ minkms identity %s", key.String())
fmt.Println(tui.NewStyle().Border(tui.HiddenBorder()).Padding(0, 0, 0, 0).Render(buf.String()))
return
}

if err := cmd.Parse(args[1:]); err != nil {
if errors.Is(err, flag.ErrHelp) {
os.Exit(2)
printIdentity := func(identity sdk.Identity) {
if !cli.IsTerminal() {
fmt.Print(identity)
return
}
cli.Fatalf("%v. See 'kes identity --help'", err)

buf := &strings.Builder{}
fmt.Fprintln(buf, "Identity:")
fmt.Fprintln(buf)
fmt.Fprintln(buf, " ", tui.NewStyle().Bold(true).Render(identity.String()))
fmt.Fprintln(buf)
fmt.Fprintln(buf, "An identity is a fingerprint of your API key")
fmt.Fprint(buf, "or certificate and can be shared securely.")
fmt.Println(tui.NewStyle().Border(tui.HiddenBorder()).Padding(0, 0, 0, 0).Render(buf.String()))
}
if key, err := sdk.ParseAPIKey(flags.Arg(0)); err == nil {
printIdentity(key.Identity())
return
}
if cmd.NArg() > 0 {
cli.Fatalf("%q is not an identity command. See 'kes identity --help'", cmd.Arg(0))

filename := flags.Arg(0)
raw, err := os.ReadFile(filename)
if err != nil {
cli.Exit(err)
}
raw = bytes.TrimSpace(raw)

var block *pem.Block
for len(raw) > 0 {
if block, raw = pem.Decode(raw); block == nil {
cli.Exitf("'%s' contains no valid PEM certificate or private key", filename)
}

switch {
case block.Type == "CERTIFICATE":
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
cli.Exit(err)
}
h := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
printIdentity(sdk.Identity(hex.EncodeToString(h[:])))
return
case strings.Contains(block.Type, "PRIVATE KEY"): // Type may be PRIVATE KEY, EC PRIVATE KEY, ...
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
cli.Exit(err)
}

key, ok := priv.(ed25519.PrivateKey)
if !ok {
cli.Exitf("'%s' contains unsupported private key. Only ed25519 private keys are supported", filename)
}
apiKey := "kes:v1:" + base64.StdEncoding.EncodeToString(append([]byte{0}, key[:ed25519.SeedSize]...))

if !cli.IsTerminal() {
fmt.Print(apiKey)
return
}
buf := &strings.Builder{}
fmt.Fprintln(buf, "Your API key:")
fmt.Fprintln(buf)
fmt.Fprintln(buf, " ", tui.NewStyle().Bold(true).Render(apiKey))
fmt.Fprintln(buf)
fmt.Fprintf(buf, "It corresponds to the private key in: %s.\n", filename)
fmt.Fprint(buf, "Keep it secret and secure!")
fmt.Println(tui.NewStyle().Border(tui.HiddenBorder()).Padding(0, 0, 0, 0).Render(buf.String()))
return
}
}
cmd.Usage()
os.Exit(2)
cli.Exitf("'%s' contains no valid PEM certificate or private key", filename)
}

const newIdentityCmdUsage = `Usage:
Expand Down
15 changes: 15 additions & 0 deletions internal/cli/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2024 - MinIO, Inc. All rights reserved.
// Use of this source code is governed by the AGPLv3
// license that can be found in the LICENSE file.

package cli

// Environment variable used by the KES CLI.
const (
// EnvServer is the server endpoint the client uses. If not set,
// clients will use '127.0.0.1:7373'.
EnvServer = "MINIO_KES_SERVER"

// EnvAPIKey is used by the client to authenticate to the server.
EnvAPIKey = "MINIO_KES_API_KEY"
)
30 changes: 30 additions & 0 deletions internal/cli/exit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2024 - MinIO, Inc. All rights reserved.
// Use of this source code is governed by the AGPLv3
// license that can be found in the LICENSE file.

package cli

import (
"fmt"
"os"

tui "github.com/charmbracelet/lipgloss"
)

// Exit prints args as error message and aborts with exit code 1.
func Exit(args ...any) {
const FG tui.Color = "#ac0000"
s := tui.NewStyle().Foreground(FG).Render("Error: ")

fmt.Fprintln(os.Stderr, s+fmt.Sprint(args...))
os.Exit(1)
}

// Exitf formats args as error message and aborts with exit code 1.
func Exitf(format string, args ...any) {
const FG tui.Color = "#ac0000"
s := tui.NewStyle().Foreground(FG).Render("Error: ")

fmt.Fprintln(os.Stderr, s+fmt.Sprintf(format, args...))
os.Exit(1)
}
17 changes: 2 additions & 15 deletions internal/cli/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,17 @@ package cli

import (
"fmt"
"os"

tui "github.com/charmbracelet/lipgloss"
)

var errPrefix = tui.NewStyle().Foreground(tui.Color("#ac0000")).Render("Error: ")

// Fatal writes an error prefix and the operands
// to OS stderr. Then, Fatal terminates the program by
// calling os.Exit(1).
func Fatal(v ...any) {
fmt.Fprint(os.Stderr, errPrefix)
fmt.Fprint(os.Stderr, v...)
fmt.Fprintln(os.Stderr)
os.Exit(1)
}
func Fatal(v ...any) { Exit(v...) }

// Fatalf writes an error prefix and the operands,
// formatted according to the format specifier, to OS stderr.
// Then, Fatalf terminates the program by calling os.Exit(1).
func Fatalf(format string, v ...any) {
fmt.Fprintf(os.Stderr, errPrefix+format+"\n", v...)
os.Exit(1)
}
func Fatalf(format string, v ...any) { Exitf(format, v...) }

// Print formats using the default formats for its operands and
// writes to standard output. Spaces are added between operands
Expand Down
47 changes: 0 additions & 47 deletions internal/cli/init.go

This file was deleted.

0 comments on commit 802ce81

Please sign in to comment.