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 .
This commit is contained in:
Martin Atkins 2016-01-16 17:30:41 -08:00
parent e8006f1539
commit 25bd43d6f4
3 changed files with 123 additions and 14 deletions

View File

@ -9,6 +9,8 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"golang.org/x/crypto/ssh"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
) )
@ -80,6 +82,16 @@ func resourcePrivateKey() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, 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 var keyPemBlock *pem.Block
switch k := key.(type) { switch k := key.(type) {
case *rsa.PrivateKey: 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: case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k) keyBytes, err := x509.MarshalECPrivateKey(k)
if err != nil { if err != nil {
return fmt.Errorf("error encoding key to PEM: %s", err) 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: default:
return fmt.Errorf("unsupported private key type") return fmt.Errorf("unsupported private key type")
} }
keyPem := string(pem.EncodeToMemory(keyPemBlock)) keyPem := string(pem.EncodeToMemory(keyPemBlock))
pubKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey(key)) pubKey := publicKey(key)
pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal public key: %s", err) return fmt.Errorf("failed to marshal public key: %s", err)
} }
pubKeyPemBlock := &pem.Block{
Type: "PUBLIC KEY",
Bytes: pubKeyBytes,
}
d.SetId(hashForState(string((pubKeyBytes)))) d.SetId(hashForState(string((pubKeyBytes))))
d.Set("private_key_pem", keyPem) 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 return nil
} }

View File

@ -18,18 +18,35 @@ func TestPrivateKeyRSA(t *testing.T) {
resource "tls_private_key" "test" { resource "tls_private_key" "test" {
algorithm = "RSA" algorithm = "RSA"
} }
output "key_pem" { output "private_key_pem" {
value = "${tls_private_key.test.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 { Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["key_pem"] gotPrivate := s.RootModule().Outputs["private_key_pem"]
if !strings.HasPrefix(got, "-----BEGIN RSA PRIVATE KEY----") { if !strings.HasPrefix(gotPrivate, "-----BEGIN RSA PRIVATE KEY----") {
return fmt.Errorf("key is missing RSA key PEM preamble") return fmt.Errorf("private key is missing RSA key PEM preamble")
} }
if len(got) > 1700 { if len(gotPrivate) > 1700 {
return fmt.Errorf("key PEM looks too long for a 2048-bit key (got %v characters)", len(got)) return fmt.Errorf("private key PEM looks too long for a 2048-bit key (got %v characters)", len(gotPrivate))
} }
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 return nil
}, },
}, },
@ -67,15 +84,67 @@ func TestPrivateKeyECDSA(t *testing.T) {
resource "tls_private_key" "test" { resource "tls_private_key" "test" {
algorithm = "ECDSA" algorithm = "ECDSA"
} }
output "key_pem" { output "private_key_pem" {
value = "${tls_private_key.test.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 { Check: func(s *terraform.State) error {
got := s.RootModule().Outputs["key_pem"] gotPrivate := s.RootModule().Outputs["private_key_pem"]
if !strings.HasPrefix(got, "-----BEGIN EC PRIVATE KEY----") { if !strings.HasPrefix(gotPrivate, "-----BEGIN EC PRIVATE KEY----") {
return fmt.Errorf("Key is missing EC key PEM preamble") 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 return nil
}, },
}, },

View File

@ -50,6 +50,12 @@ The following attributes are exported:
* `algorithm` - The algorithm that was selected for the key. * `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.
* `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 ## Generating a New Key