Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd: support private keys in kes identity #461

Merged
merged 3 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Loading
Loading