From 25bd43d6f4793f65c15943facf3a842477a67af5 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 16 Jan 2016 17:30:41 -0800 Subject: [PATCH] Export public keys from tls_private_key In most cases private keys are used to produce certs and cert requests, but there are some less-common cases where the PEM-formatted keypair is used alone. The public_key_pem attribute supports such cases. This also includes a public_key_openssh attribute, which allows this resource to be used to generate temporary OpenSSH credentials, so that e.g. a Terraform configuration could generate its own keypair to use with the aws_key_pair resource. This has the same caveats as all cases where we generate private keys in Terraform, but could be useful for temporary/throwaway environments where the state either doesn't live for long or is stored securely. This builds on work started by Simarpreet Singh in #4441 . --- builtin/providers/tls/resource_private_key.go | 42 ++++++++- .../tls/resource_private_key_test.go | 89 ++++++++++++++++--- .../docs/providers/tls/r/private_key.html.md | 6 ++ 3 files changed, 123 insertions(+), 14 deletions(-) diff --git a/builtin/providers/tls/resource_private_key.go b/builtin/providers/tls/resource_private_key.go index f3fdd3f9bc..8270cc624f 100644 --- a/builtin/providers/tls/resource_private_key.go +++ b/builtin/providers/tls/resource_private_key.go @@ -9,6 +9,8 @@ import ( "encoding/pem" "fmt" + "golang.org/x/crypto/ssh" + "github.com/hashicorp/terraform/helper/schema" ) @@ -80,6 +82,16 @@ func resourcePrivateKey() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "public_key_pem": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "public_key_openssh": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -100,25 +112,47 @@ func CreatePrivateKey(d *schema.ResourceData, meta interface{}) error { var keyPemBlock *pem.Block switch k := key.(type) { case *rsa.PrivateKey: - keyPemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} + keyPemBlock = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(k), + } case *ecdsa.PrivateKey: - b, err := x509.MarshalECPrivateKey(k) + keyBytes, err := x509.MarshalECPrivateKey(k) if err != nil { return fmt.Errorf("error encoding key to PEM: %s", err) } - keyPemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + keyPemBlock = &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + } default: return fmt.Errorf("unsupported private key type") } keyPem := string(pem.EncodeToMemory(keyPemBlock)) - pubKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey(key)) + pubKey := publicKey(key) + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) if err != nil { return fmt.Errorf("failed to marshal public key: %s", err) } + pubKeyPemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + } d.SetId(hashForState(string((pubKeyBytes)))) d.Set("private_key_pem", keyPem) + d.Set("public_key_pem", string(pem.EncodeToMemory(pubKeyPemBlock))) + + sshPubKey, err := ssh.NewPublicKey(pubKey) + if err == nil { + // Not all EC types can be SSH keys, so we'll produce this only + // if an appropriate type was selected. + sshPubKeyBytes := ssh.MarshalAuthorizedKey(sshPubKey) + d.Set("public_key_openssh", string(sshPubKeyBytes)) + } else { + d.Set("public_key_openssh", "") + } return nil } diff --git a/builtin/providers/tls/resource_private_key_test.go b/builtin/providers/tls/resource_private_key_test.go index e0bcf44c49..00fc8abbd6 100644 --- a/builtin/providers/tls/resource_private_key_test.go +++ b/builtin/providers/tls/resource_private_key_test.go @@ -18,18 +18,35 @@ func TestPrivateKeyRSA(t *testing.T) { resource "tls_private_key" "test" { algorithm = "RSA" } - output "key_pem" { + 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}" + } `, Check: func(s *terraform.State) error { - got := s.RootModule().Outputs["key_pem"] - if !strings.HasPrefix(got, "-----BEGIN RSA PRIVATE KEY----") { - return fmt.Errorf("key is missing RSA key PEM preamble") + gotPrivate := s.RootModule().Outputs["private_key_pem"] + if !strings.HasPrefix(gotPrivate, "-----BEGIN RSA PRIVATE KEY----") { + return fmt.Errorf("private key is missing RSA key PEM preamble") + } + if len(gotPrivate) > 1700 { + return fmt.Errorf("private key PEM looks too long for a 2048-bit key (got %v characters)", len(gotPrivate)) } - if len(got) > 1700 { - return fmt.Errorf("key PEM looks too long for a 2048-bit key (got %v characters)", len(got)) + + gotPublic := s.RootModule().Outputs["public_key_pem"] + if !strings.HasPrefix(gotPublic, "-----BEGIN PUBLIC KEY----") { + return fmt.Errorf("public key is missing public key PEM preamble") + } + + gotPublicSSH := s.RootModule().Outputs["public_key_openssh"] + if !strings.HasPrefix(gotPublicSSH, "ssh-rsa ") { + return fmt.Errorf("SSH public key is missing ssh-rsa prefix") } + return nil }, }, @@ -67,15 +84,67 @@ func TestPrivateKeyECDSA(t *testing.T) { resource "tls_private_key" "test" { algorithm = "ECDSA" } - output "key_pem" { + 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}" + } `, Check: func(s *terraform.State) error { - got := s.RootModule().Outputs["key_pem"] - if !strings.HasPrefix(got, "-----BEGIN EC PRIVATE KEY----") { - return fmt.Errorf("Key is missing EC key PEM preamble") + gotPrivate := s.RootModule().Outputs["private_key_pem"] + if !strings.HasPrefix(gotPrivate, "-----BEGIN EC PRIVATE KEY----") { + return fmt.Errorf("Private key is missing EC key PEM preamble") + } + + gotPublic := s.RootModule().Outputs["public_key_pem"] + if !strings.HasPrefix(gotPublic, "-----BEGIN PUBLIC KEY----") { + return fmt.Errorf("public key is missing public key PEM preamble") + } + + gotPublicSSH := s.RootModule().Outputs["public_key_openssh"] + if gotPublicSSH != "" { + return fmt.Errorf("P224 EC key should not generate OpenSSH public key") + } + + return nil + }, + }, + r.TestStep{ + Config: ` + resource "tls_private_key" "test" { + algorithm = "ECDSA" + ecdsa_curve = "P256" + } + 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}" + } + `, + Check: func(s *terraform.State) error { + gotPrivate := s.RootModule().Outputs["private_key_pem"] + if !strings.HasPrefix(gotPrivate, "-----BEGIN EC PRIVATE KEY----") { + return fmt.Errorf("Private key is missing EC key PEM preamble") } + + gotPublic := s.RootModule().Outputs["public_key_pem"] + if !strings.HasPrefix(gotPublic, "-----BEGIN PUBLIC KEY----") { + return fmt.Errorf("public key is missing public key PEM preamble") + } + + gotPublicSSH := s.RootModule().Outputs["public_key_openssh"] + if !strings.HasPrefix(gotPublicSSH, "ecdsa-sha2-nistp256 ") { + return fmt.Errorf("P256 SSH public key is missing ecdsa prefix") + } + return nil }, }, diff --git a/website/source/docs/providers/tls/r/private_key.html.md b/website/source/docs/providers/tls/r/private_key.html.md index 1a4a2cec43..0afd116756 100644 --- a/website/source/docs/providers/tls/r/private_key.html.md +++ b/website/source/docs/providers/tls/r/private_key.html.md @@ -50,6 +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. +* `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 "P251" + are supported. This attribute is empty if an incompatible ECDSA curve + is selected. ## Generating a New Key