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:
parent
e8006f1539
commit
25bd43d6f4
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue