From 924e3b1e50ce97ff69872b7586725a29b214968f Mon Sep 17 00:00:00 2001 From: Mateusz Gozdek Date: Fri, 4 Oct 2019 00:20:04 +0200 Subject: [PATCH 1/2] r/private_key: Add support for ed25519 algorithm This commit add support for ED25519 algorithm when generating tls_private_key resource. Refs #26 Signed-off-by: Mateusz Gozdek --- internal/provider/resource_private_key.go | 18 ++++ .../provider/resource_private_key_test.go | 67 +++++++++++++++ internal/provider/util.go | 84 +++++++++++++++++++ website/docs/r/private_key.html.md | 12 +-- 4 files changed, 175 insertions(+), 6 deletions(-) diff --git a/internal/provider/resource_private_key.go b/internal/provider/resource_private_key.go index 4ba8bd16..4067be75 100644 --- a/internal/provider/resource_private_key.go +++ b/internal/provider/resource_private_key.go @@ -2,6 +2,7 @@ package provider import ( "crypto/ecdsa" + "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" @@ -35,6 +36,13 @@ var keyAlgos map[string]keyAlgo = map[string]keyAlgo{ return nil, fmt.Errorf("invalid ecdsa_curve; must be P224, P256, P384 or P521") } }, + "ED25519": func(d *schema.ResourceData) (interface{}, error) { + _, key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate ed25519 key: %s", err) + } + return &key, err + }, } var keyParsers map[string]keyParser = map[string]keyParser{ @@ -44,6 +52,9 @@ var keyParsers map[string]keyParser = map[string]keyParser{ "ECDSA": func(der []byte) (interface{}, error) { return x509.ParseECPrivateKey(der) }, + "ED25519": func(der []byte) (interface{}, error) { + return x509.ParsePKCS8PrivateKey(der) + }, } func resourcePrivateKey() *schema.Resource { @@ -129,6 +140,11 @@ func CreatePrivateKey(d *schema.ResourceData, meta interface{}) error { Type: "EC PRIVATE KEY", Bytes: keyBytes, } + case *ed25519.PrivateKey: + keyPemBlock = &pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Bytes: marshalED25519PrivateKey(*k), + } default: return fmt.Errorf("unsupported private key type") } @@ -154,6 +170,8 @@ func publicKey(priv interface{}) interface{} { return &k.PublicKey case *ecdsa.PrivateKey: return &k.PublicKey + case *ed25519.PrivateKey: + return k.Public() default: return nil } diff --git a/internal/provider/resource_private_key_test.go b/internal/provider/resource_private_key_test.go index d8f7f50b..4347bc06 100644 --- a/internal/provider/resource_private_key_test.go +++ b/internal/provider/resource_private_key_test.go @@ -222,3 +222,70 @@ func TestPrivateKeyECDSA(t *testing.T) { }, }) } + +func TestPrivateKeyED25519(t *testing.T) { + r.UnitTest(t, r.TestCase{ + Providers: testProviders, + Steps: []r.TestStep{ + { + Config: ` + resource "tls_private_key" "test" { + algorithm = "ED25519" + } + output "private_key_pem" { + value = "${tls_private_key.test.private_key_pem}" + } + output "public_key_pem" { + value = "${tls_private_key.test.public_key_pem}" + } + output "public_key_openssh" { + value = "${tls_private_key.test.public_key_openssh}" + } + output "public_key_fingerprint_md5" { + value = "${tls_private_key.test.public_key_fingerprint_md5}" + } + `, + Check: func(s *terraform.State) error { + gotPrivateUntyped := s.RootModule().Outputs["private_key_pem"].Value + gotPrivate, ok := gotPrivateUntyped.(string) + if !ok { + return fmt.Errorf("output for \"private_key_pem\" is not a string") + } + + if !strings.HasPrefix(gotPrivate, "-----BEGIN OPENSSH PRIVATE KEY----") { + return fmt.Errorf("private key is missing RSA key PEM preamble") + } + + gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value + gotPublic, ok := gotPublicUntyped.(string) + if !ok { + return fmt.Errorf("output for \"public_key_pem\" is not a string") + } + if !strings.HasPrefix(gotPublic, "-----BEGIN PUBLIC KEY----") { + return fmt.Errorf("public key is missing public key PEM preamble") + } + + gotPublicSSHUntyped := s.RootModule().Outputs["public_key_openssh"].Value + gotPublicSSH, ok := gotPublicSSHUntyped.(string) + if !ok { + return fmt.Errorf("output for \"public_key_openssh\" is not a string") + } + if !strings.HasPrefix(gotPublicSSH, "ssh-ed25519 ") { + return fmt.Errorf("SSH public key is missing ssh-ed25519 prefix") + } + + gotPublicFingerprintUntyped := s.RootModule().Outputs["public_key_fingerprint_md5"].Value + gotPublicFingerprint, ok := gotPublicFingerprintUntyped.(string) + if !ok { + return fmt.Errorf("output for \"public_key_fingerprint_md5\" is not a string") + } + if !(gotPublicFingerprint[2] == ':') { + return fmt.Errorf("MD5 public key fingerprint is missing : in the correct place") + } + + return nil + }, + }, + }, + }) +} diff --git a/internal/provider/util.go b/internal/provider/util.go index 19c151fe..9d087352 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -1,9 +1,11 @@ package provider import ( + "crypto/ed25519" "crypto/x509" "encoding/pem" "fmt" + "math/rand" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "golang.org/x/crypto/ssh" @@ -103,3 +105,85 @@ func readPublicKey(d *schema.ResourceData, rsaKey interface{}) error { } return nil } + +// From https://github.com/mikesmitty/edkey/blob/master/edkey.go +// +// Writes ed25519 private keys into the new OpenSSH private key format. +// I have no idea why this isn't implemented anywhere yet, you can do seemingly +// everything except write it to disk in the OpenSSH private key format. +func marshalED25519PrivateKey(key ed25519.PrivateKey) []byte { + // Add our key header (followed by a null byte) + magic := append([]byte("openssh-key-v1"), 0) + + var w struct { + CipherName string + KdfName string + KdfOpts string + NumKeys uint32 + PubKey []byte + PrivKeyBlock []byte + } + + // Fill out the private key fields + pk1 := struct { + Check1 uint32 + Check2 uint32 + Keytype string + Pub []byte + Priv []byte + Comment string + Pad []byte `ssh:"rest"` + }{} + + // Set our check ints + ci := rand.Uint32() + pk1.Check1 = ci + pk1.Check2 = ci + + // Set our key type + pk1.Keytype = ssh.KeyAlgoED25519 + + // Add the pubkey to the optionally-encrypted block + pk, ok := key.Public().(ed25519.PublicKey) + if !ok { + //fmt.Fprintln(os.Stderr, "ed25519.PublicKey type assertion failed on an ed25519 public key. This should never ever happen.") + return nil + } + pubKey := []byte(pk) + pk1.Pub = pubKey + + // Add our private key + pk1.Priv = []byte(key) + + // Might be useful to put something in here at some point + pk1.Comment = "" + + // Add some padding to match the encryption block size within PrivKeyBlock (without Pad field) + // 8 doesn't match the documentation, but that's what ssh-keygen uses for unencrypted keys. *shrug* + bs := 8 + blockLen := len(ssh.Marshal(pk1)) + padLen := (bs - (blockLen % bs)) % bs + pk1.Pad = make([]byte, padLen) + + // Padding is a sequence of bytes like: 1, 2, 3... + for i := 0; i < padLen; i++ { + pk1.Pad[i] = byte(i + 1) + } + + // Generate the pubkey prefix "\0\0\0\nssh-ed25519\0\0\0 " + prefix := []byte{0x0, 0x0, 0x0, 0x0b} + prefix = append(prefix, []byte(ssh.KeyAlgoED25519)...) + prefix = append(prefix, []byte{0x0, 0x0, 0x0, 0x20}...) + + // Only going to support unencrypted keys for now + w.CipherName = "none" + w.KdfName = "none" + w.KdfOpts = "" + w.NumKeys = 1 + w.PubKey = append(prefix, pubKey...) + w.PrivKeyBlock = ssh.Marshal(pk1) + + magic = append(magic, ssh.Marshal(w)...) + + return magic +} diff --git a/website/docs/r/private_key.html.md b/website/docs/r/private_key.html.md index 916dfbf0..cf5d7d84 100644 --- a/website/docs/r/private_key.html.md +++ b/website/docs/r/private_key.html.md @@ -35,7 +35,7 @@ resource "tls_private_key" "example" { The following arguments are supported: * `algorithm` - (Required) The name of the algorithm to use for -the key. Currently-supported values are "RSA" and "ECDSA". +the key. Currently-supported values are "RSA", "ECDSA" and "ED25519". * `rsa_bits` - (Optional) When `algorithm` is "RSA", the size of the generated RSA key in bits. Defaults to 2048. @@ -49,13 +49,13 @@ default. The following attributes are exported: * `algorithm` - The algorithm that was selected for the key. -* `private_key_pem` - The private key data in PEM format. +* `private_key_pem` - The private key data in PEM format. For "ED25519" keys, + the key is in OpenSSH-compatible format. * `public_key_pem` - The public key data in PEM format. * `public_key_openssh` - The public key data in OpenSSH `authorized_keys` - format, if the selected private key format is compatible. All RSA keys - are supported, and ECDSA keys with curves "P256", "P384" and "P521" - are supported. This attribute is empty if an incompatible ECDSA curve - is selected. + format, if the selected private key format is compatible. All RSA and ED25519 + keys are supported, and ECDSA keys with curves "P256", "P384" and "P521" are + supported. This attribute is empty if an incompatible ECDSA curve is selected. * `public_key_fingerprint_md5` - The md5 hash of the public key data in OpenSSH MD5 hash format, e.g. `aa:bb:cc:...`. Only available if the selected private key format is compatible, as per the rules for From 5181615a08629e0d1b5a50e49f4a679d2264b284 Mon Sep 17 00:00:00 2001 From: Mateusz Gozdek Date: Mon, 23 Dec 2019 11:35:55 +0100 Subject: [PATCH 2/2] r/private_key: Add private_key_openssh attribute This commit adds private_key_openssh attribute, which always contains private key in format, which is compatible with OpenSSH. This allows to produce ED25519 private key in OpenSSL compatible format in private_key_pem attribute and OpenSSH-compatible format in this new attribute. Other key types are the same in private_key_pem and private_key_openssh, as OpenSSH can read them. In the future, this could be changed to produce all private keys OpenSSH native format. Refs #26 Signed-off-by: Mateusz Gozdek --- internal/provider/resource_private_key.go | 24 +++++++++++++++++-- .../provider/resource_private_key_test.go | 17 +++++++++++-- website/docs/r/private_key.html.md | 4 ++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/internal/provider/resource_private_key.go b/internal/provider/resource_private_key.go index 4067be75..4f512116 100644 --- a/internal/provider/resource_private_key.go +++ b/internal/provider/resource_private_key.go @@ -93,6 +93,12 @@ func resourcePrivateKey() *schema.Resource { Sensitive: true, }, + "private_key_openssh": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + "public_key_pem": { Type: schema.TypeString, Computed: true, @@ -125,6 +131,7 @@ func CreatePrivateKey(d *schema.ResourceData, meta interface{}) error { } var keyPemBlock *pem.Block + var openSSHKeyPemBlock *pem.Block switch k := key.(type) { case *rsa.PrivateKey: keyPemBlock = &pem.Block{ @@ -141,16 +148,29 @@ func CreatePrivateKey(d *schema.ResourceData, meta interface{}) error { Bytes: keyBytes, } case *ed25519.PrivateKey: + keyBytes, err := x509.MarshalPKCS8PrivateKey(*k) + if err != nil { + return fmt.Errorf("error encoding key to PEM: %s", err) + } + keyPemBlock = &pem.Block{ + Type: "PRIVATE KEY", + Bytes: keyBytes, + } + openSSHKeyPemBlock = &pem.Block{ Type: "OPENSSH PRIVATE KEY", Bytes: marshalED25519PrivateKey(*k), } default: return fmt.Errorf("unsupported private key type") } - keyPem := string(pem.EncodeToMemory(keyPemBlock)) + d.Set("private_key_pem", string(pem.EncodeToMemory(keyPemBlock))) + + if openSSHKeyPemBlock == nil { + openSSHKeyPemBlock = keyPemBlock + } - d.Set("private_key_pem", keyPem) + d.Set("private_key_openssh", string(pem.EncodeToMemory(openSSHKeyPemBlock))) return readPublicKey(d, key) } diff --git a/internal/provider/resource_private_key_test.go b/internal/provider/resource_private_key_test.go index 4347bc06..897b0003 100644 --- a/internal/provider/resource_private_key_test.go +++ b/internal/provider/resource_private_key_test.go @@ -235,6 +235,9 @@ func TestPrivateKeyED25519(t *testing.T) { output "private_key_pem" { value = "${tls_private_key.test.private_key_pem}" } + output "private_key_openssh" { + value = "${tls_private_key.test.private_key_openssh}" + } output "public_key_pem" { value = "${tls_private_key.test.public_key_pem}" } @@ -252,8 +255,18 @@ func TestPrivateKeyED25519(t *testing.T) { return fmt.Errorf("output for \"private_key_pem\" is not a string") } - if !strings.HasPrefix(gotPrivate, "-----BEGIN OPENSSH PRIVATE KEY----") { - return fmt.Errorf("private key is missing RSA key PEM preamble") + if !strings.HasPrefix(gotPrivate, "-----BEGIN PRIVATE KEY----") { + return fmt.Errorf("private key is missing ED25519 key PEM preamble") + } + + gotPrivateOpenSSHUntyped := s.RootModule().Outputs["private_key_openssh"].Value + gotPrivateOpenSSH, ok := gotPrivateOpenSSHUntyped.(string) + if !ok { + return fmt.Errorf("output for \"private_key_openssh\" is not a string") + } + + if !strings.HasPrefix(gotPrivateOpenSSH, "-----BEGIN OPENSSH PRIVATE KEY----") { + return fmt.Errorf("private key is missing OPENSSH key PEM preamble") } gotPublicUntyped := s.RootModule().Outputs["public_key_pem"].Value diff --git a/website/docs/r/private_key.html.md b/website/docs/r/private_key.html.md index cf5d7d84..f3c61003 100644 --- a/website/docs/r/private_key.html.md +++ b/website/docs/r/private_key.html.md @@ -49,8 +49,8 @@ default. The following attributes are exported: * `algorithm` - The algorithm that was selected for the key. -* `private_key_pem` - The private key data in PEM format. For "ED25519" keys, - the key is in OpenSSH-compatible format. +* `private_key_pem` - The private key data in PEM format. +* `private_key_openssh` - The private key data in OpenSSH-compatible format. * `public_key_pem` - The public key data in PEM format. * `public_key_openssh` - The public key data in OpenSSH `authorized_keys` format, if the selected private key format is compatible. All RSA and ED25519