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

Keymaster tool preview #150

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
265 changes: 265 additions & 0 deletions cmd/keymaster-tool/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package main

import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
stdlog "log"
"os"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/howeyc/gopass"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/ssh"
"gopkg.in/alecthomas/kingpin.v2"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using Cobra here. What do you think of using that instead?
https://pkg.go.dev/github.com/spf13/cobra


"github.com/Cloud-Foundations/Dominator/lib/log/cmdlogger"
"github.com/Cloud-Foundations/golib/pkg/log"
"github.com/Cloud-Foundations/keymaster/lib/certgen"
)

var (
app = kingpin.New("keyutil", "Tooling for puresigner2 secret management")
debug = app.Flag("debug", "Enable debug mode.").Bool()
inSecretARN = app.Flag("secret-arn", "Location of secret to use").String()
inAWSRegion = app.Flag("aws-region", "AWS region for secret").Default("us-west-2").String()

generateCmd = app.Command("generate-new", "Generate a new keypair and encrypt")
keyTypeIn = generateCmd.Flag("type", "Type of Key (ed25519 | rsa)").Default("ed25519").String()

printPublicCmd = app.Command("printPublic", "Verify EMS secret")
printPublicFilenameIn = printPublicCmd.Flag("inFilename", "File to Read").Required().String()
printFormat = printPublicCmd.Flag("printFormat", "format to use pem|ssh").Default("pem").String()
)

const rsaBits = 3072

func getPasswordFromConsole() ([]byte, error) {
fmt.Fprintf(os.Stderr, "Passphrase for key ")
return gopass.GetPasswd()
}

func getPassphraseFromAWS(awsSecretId string, awsRegion string) ([]byte, error) {
svc := secretsmanager.New(session.New(),
aws.NewConfig().WithRegion(awsRegion))
input := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(awsSecretId),
VersionStage: aws.String("AWSCURRENT"), // VersionStage defaults to AWSCURRENT if unspecified
}
result, err := svc.GetSecretValue(input)
if err != nil {
return nil, err
}
return []byte(*result.SecretString), nil

}

func getPassPhrase(secretArn string, awsRegion string) ([]byte, error) {
if secretArn != "" {
return getPassphraseFromAWS(secretArn, awsRegion)
}
return getPasswordFromConsole()

}

func armoredEncryptPrivateKey(privateKey interface{}, passphrase []byte) ([]byte, error) {
encryptionType := "PGP MESSAGE"
armoredBuf := new(bytes.Buffer)
armoredWriter, err := armor.Encode(armoredBuf, encryptionType, nil)
if err != nil {
return nil, err
}
var plaintextWriter io.WriteCloser
plaintextWriter, err = openpgp.SymmetricallyEncrypt(armoredWriter,
passphrase, nil, nil)
if err != nil {
return nil, err
}
derPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, err
}
privateKeyPEM := &pem.Block{
Type: "PRIVATE KEY",
Bytes: derPrivateKey,
}
if err := pem.Encode(plaintextWriter, privateKeyPEM); err != nil {
return nil, err
}
if err := plaintextWriter.Close(); err != nil {
return nil, err
}
if err := armoredWriter.Close(); err != nil {
return nil, err
}
return armoredBuf.Bytes(), nil
}

func generateNewKeyPair(passPhrase []byte, keyType string, outWriter io.Writer, logger log.DebugLogger) error {
var privateKey crypto.Signer
var err error
switch keyType {
case "ed25519":
_, privateKey, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
return err
}
case "rsa":
privateKey, err = rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
return err
}
default:
return fmt.Errorf("bad key type")
}
armoredBytes, err := armoredEncryptPrivateKey(privateKey, passPhrase)
if err != nil {
return err
}
_, err = outWriter.Write(armoredBytes)

return err
}

func pgpDecryptFileData(cipherText []byte, password []byte) ([]byte, error) {
decbuf := bytes.NewBuffer(cipherText)
armorBlock, err := armor.Decode(decbuf)
if err != nil {
fmt.Printf("ciphertext=%s", string(cipherText))
return nil, fmt.Errorf("cannot decode armored file")
}
failed := false
prompt := func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
// If the given passphrase isn't correct, the function will be called
// again, forever.
// This method will fail fast.
// Ref: https://godoc.org/golang.org/x/crypto/openpgp#PromptFunction
if failed {
return nil, fmt.Errorf("decryption failed")
}
failed = true
return password, nil
}
md, err := openpgp.ReadMessage(armorBlock.Body, nil, prompt, nil)
if err != nil {
return nil, fmt.Errorf("cannot decrypt key: %s", err)
}
return ioutil.ReadAll(md.UnverifiedBody)
}

func decryptDecodeArmoredPrivateKey(cipherText []byte, passPhrase []byte) (crypto.Signer, error) {
plaintext, err := pgpDecryptFileData(cipherText, passPhrase)
if err != nil {
return nil, err
}
return certgen.GetSignerFromPEMBytes(plaintext)
}

func goSSHPubToFileString(pub ssh.PublicKey, comment string) (string, error) {
pubBytes := pub.Marshal()
encoded := base64.StdEncoding.EncodeToString(pubBytes)
return pub.Type() + " " + encoded + " " + comment, nil
}

//copied from https://golang.org/src/crypto/tls/generate_cert.go
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
case ed25519.PrivateKey:
return k.Public().(ed25519.PublicKey)
case *ed25519.PrivateKey:
return k.Public().(*ed25519.PublicKey)
default:
return nil
}
}

func printPublicKey(passPhrase []byte, inFilename string, outFormat string, outWriter io.Writer, logger log.DebugLogger) error {
inFile, err := os.Open(inFilename) // For read access.
if err != nil {
return err
}
defer inFile.Close()
cipherText, err := io.ReadAll(inFile)
if err != nil {
return err
}
signer, err := decryptDecodeArmoredPrivateKey(cipherText, passPhrase)
if err != nil {
return err
}
pubKey := publicKey(signer)
if pubKey == nil {
return fmt.Errorf("Invalid private key type")
}
switch outFormat {
case "pem":
pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return err
}
block := &pem.Block{
Type: "PUBLIC KEY",
Bytes: pubKeyBytes,
}
return pem.Encode(outWriter, block)
case "ssh":
sshPub, err := ssh.NewPublicKey(pubKey)
if err != nil {
return err
}
sshPubFileString, err := goSSHPubToFileString(sshPub, "keymaster")
if err != nil {
return err
}
_, err = fmt.Fprintf(outWriter, "%s\n", sshPubFileString)
return err

default:
return fmt.Errorf("invalid outpur format")
}
}

func main() {

logger := cmdlogger.New()
//var err error
switch kingpin.MustParse(app.Parse(os.Args[1:])) {
// Register user
case generateCmd.FullCommand():
passPhrase, err := getPassPhrase(*inSecretARN, *inAWSRegion)
if err != nil {
stdlog.Fatalf("Error: %s", err)
}
err = generateNewKeyPair(passPhrase, *keyTypeIn, os.Stdout, logger)
if err != nil {
stdlog.Fatalf("Error: %s", err)
}
case printPublicCmd.FullCommand():
passPhrase, err := getPassPhrase(*inSecretARN, *inAWSRegion)
if err != nil {
stdlog.Fatalf("Error: %s", err)
}
err = printPublicKey(passPhrase, *printPublicFilenameIn, *printFormat, os.Stdout, logger)
if err != nil {
stdlog.Fatalf("Error: %s", err)
}

}

}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
golang.org/x/net v0.26.0
golang.org/x/oauth2 v0.21.0
golang.org/x/term v0.21.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/ldap.v2 v2.5.1
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v2 v2.4.0
Expand All @@ -45,6 +46,8 @@ require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.32.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
Expand Down Expand Up @@ -361,6 +364,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
Expand Down
Loading