Skip to content
Closed
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
42 changes: 40 additions & 2 deletions internal/provider/resource_private_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provider

import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
Expand Down Expand Up @@ -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{
Expand All @@ -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 {
Expand Down Expand Up @@ -82,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,
Expand Down Expand Up @@ -114,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{
Expand All @@ -129,12 +147,30 @@ func CreatePrivateKey(d *schema.ResourceData, meta interface{}) error {
Type: "EC PRIVATE KEY",
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)
}
Expand All @@ -154,6 +190,8 @@ func publicKey(priv interface{}) interface{} {
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
case *ed25519.PrivateKey:
return k.Public()
default:
return nil
}
Expand Down
80 changes: 80 additions & 0 deletions internal/provider/resource_private_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,83 @@ 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 "private_key_openssh" {
value = "${tls_private_key.test.private_key_openssh}"
}
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 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
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
},
},
},
})
}
84 changes: 84 additions & 0 deletions internal/provider/util.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}
10 changes: 5 additions & 5 deletions website/docs/r/private_key.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -50,12 +50,12 @@ 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_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 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
Expand Down