From 12e173467ce0cd50a22b841a78bc9a7d4e681afb Mon Sep 17 00:00:00 2001 From: Camilo Viecco Date: Tue, 15 Feb 2022 15:38:33 -0800 Subject: [PATCH 1/4] initial commands --- cmd/keymaster-tool/main.go | 223 +++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 cmd/keymaster-tool/main.go diff --git a/cmd/keymaster-tool/main.go b/cmd/keymaster-tool/main.go new file mode 100644 index 00000000..f9cfbd4b --- /dev/null +++ b/cmd/keymaster-tool/main.go @@ -0,0 +1,223 @@ +package main + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + stdlog "log" + "os" + + "github.com/howeyc/gopass" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "gopkg.in/alecthomas/kingpin.v2" + + "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.Printf("Passphrase for key ") + return gopass.GetPasswd() +} + +func getPassphraseFromARN(secretArn string) ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func getPassPhrase(secretArn string) ([]byte, error) { + if secretArn != "" { + return getPassphraseFromARN(secretArn) + } + 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 { + 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) +} + +//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, 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(passPhrase, cipherText) + if err != nil { + return err + } + pubKey := publicKey(signer) + if pubKey == nil { + return fmt.Errorf("Invalid private key type") + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) + if err != nil { + return err + } + _, err = outWriter.Write(pubKeyBytes) + + return err +} + +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) + 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) + if err != nil { + stdlog.Fatalf("Error: %s", err) + } + err = printPublicKey(passPhrase, *printPublicFilenameIn, os.Stdout, logger) + if err != nil { + stdlog.Fatalf("Error: %s", err) + } + + } + +} From 48c6cdfe1d64c234b49fcea291f9a97d5c31c582 Mon Sep 17 00:00:00 2001 From: Camilo Viecco Date: Tue, 15 Feb 2022 19:18:33 -0800 Subject: [PATCH 2/4] now read secret from aws secret manager --- cmd/keymaster-tool/main.go | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/cmd/keymaster-tool/main.go b/cmd/keymaster-tool/main.go index f9cfbd4b..5cd78b26 100644 --- a/cmd/keymaster-tool/main.go +++ b/cmd/keymaster-tool/main.go @@ -15,6 +15,9 @@ import ( 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" @@ -42,17 +45,28 @@ var ( const rsaBits = 3072 func getPasswordFromConsole() ([]byte, error) { - fmt.Printf("Passphrase for key ") + fmt.Fprintf(os.Stderr, "Passphrase for key ") return gopass.GetPasswd() } -func getPassphraseFromARN(secretArn string) ([]byte, error) { - return nil, fmt.Errorf("not implemented") +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) ([]byte, error) { +func getPassPhrase(secretArn string, awsRegion string) ([]byte, error) { if secretArn != "" { - return getPassphraseFromARN(secretArn) + return getPassphraseFromAWS(secretArn, awsRegion) } return getPasswordFromConsole() @@ -121,6 +135,7 @@ 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 @@ -176,7 +191,7 @@ func printPublicKey(passPhrase []byte, inFilename string, outWriter io.Writer, l if err != nil { return err } - signer, err := decryptDecodeArmoredPrivateKey(passPhrase, cipherText) + signer, err := decryptDecodeArmoredPrivateKey(cipherText, passPhrase) if err != nil { return err } @@ -188,9 +203,12 @@ func printPublicKey(passPhrase []byte, inFilename string, outWriter io.Writer, l if err != nil { return err } - _, err = outWriter.Write(pubKeyBytes) + block := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + } - return err + return pem.Encode(outWriter, block) } func main() { @@ -200,7 +218,7 @@ func main() { switch kingpin.MustParse(app.Parse(os.Args[1:])) { // Register user case generateCmd.FullCommand(): - passPhrase, err := getPassPhrase(*inSecretARN) + passPhrase, err := getPassPhrase(*inSecretARN, *inAWSRegion) if err != nil { stdlog.Fatalf("Error: %s", err) } @@ -209,7 +227,7 @@ func main() { stdlog.Fatalf("Error: %s", err) } case printPublicCmd.FullCommand(): - passPhrase, err := getPassPhrase(*inSecretARN) + passPhrase, err := getPassPhrase(*inSecretARN, *inAWSRegion) if err != nil { stdlog.Fatalf("Error: %s", err) } From 8f94af9f045ef6f0eb7539c67f87989659b65fa0 Mon Sep 17 00:00:00 2001 From: Camilo Viecco Date: Wed, 16 Feb 2022 14:51:33 -0800 Subject: [PATCH 3/4] outpur in ssh or pem format --- cmd/keymaster-tool/main.go | 44 +++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/cmd/keymaster-tool/main.go b/cmd/keymaster-tool/main.go index 5cd78b26..7ac234dd 100644 --- a/cmd/keymaster-tool/main.go +++ b/cmd/keymaster-tool/main.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/base64" "encoding/pem" "fmt" "io" @@ -21,6 +22,7 @@ import ( "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" "github.com/Cloud-Foundations/Dominator/lib/log/cmdlogger" @@ -165,6 +167,12 @@ func decryptDecodeArmoredPrivateKey(cipherText []byte, passPhrase []byte) (crypt 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) { @@ -181,7 +189,7 @@ func publicKey(priv interface{}) interface{} { } } -func printPublicKey(passPhrase []byte, inFilename string, outWriter io.Writer, logger log.DebugLogger) error { +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 @@ -199,16 +207,32 @@ func printPublicKey(passPhrase []byte, inFilename string, outWriter io.Writer, l if pubKey == nil { return fmt.Errorf("Invalid private key type") } - pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) - if err != nil { + 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 - } - block := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubKeyBytes, - } - return pem.Encode(outWriter, block) + default: + return fmt.Errorf("invalid outpur format") + } } func main() { @@ -231,7 +255,7 @@ func main() { if err != nil { stdlog.Fatalf("Error: %s", err) } - err = printPublicKey(passPhrase, *printPublicFilenameIn, os.Stdout, logger) + err = printPublicKey(passPhrase, *printPublicFilenameIn, *printFormat, os.Stdout, logger) if err != nil { stdlog.Fatalf("Error: %s", err) } From 6d68a2342283d801490ab3190beadcf89ccf3a0a Mon Sep 17 00:00:00 2001 From: Camilo Viecco Date: Thu, 1 Aug 2024 14:31:40 -0700 Subject: [PATCH 4/4] module fixes --- go.mod | 3 +++ go.sum | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/go.mod b/go.mod index abe06101..cb75c2c3 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index c358ff6d..4bafbca5 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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=