Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tls_private_key resource: adding support for ED25519 key algorithm #151

Merged
merged 22 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
da29de7
r/private_key: Add support for ed25519 algorithm
invidian Oct 3, 2019
e136ab5
r/private_key: Add private_key_openssh attribute
invidian Dec 23, 2019
f37272e
Utility package to marshal `crypto.PrivateKey` to OpenSSH PEM format
Feb 18, 2022
f6387da
Removing `marshalED25519PrivateKey` from `util.go` in favour of the (…
Feb 18, 2022
b9489f5
Adding type `Algorithm` to use in maps and signatures
Feb 18, 2022
35b3027
Switching to use the `openssh` package for generating OpenSSH PEM for…
Feb 18, 2022
76d63a4
Adding `public_key_fingerprint_sha256` attribute to `tls_private_key`…
Feb 18, 2022
05c476a
Update `tls_private_key` resource testing to reflect all the recent c…
Feb 18, 2022
bdce1fa
Adding attribute `public_key_fingerprint_sha256` to `tls_public_key` …
Feb 21, 2022
d420691
Updating website documentation for `tls_private_key` resource and `tl…
Feb 21, 2022
899eb2b
Update internal/openssh/lib_test.go (typo)
Feb 21, 2022
67f57a5
Update website/docs/r/private_key.html.md
Feb 21, 2022
33eabeb
Update internal/openssh/lib_test.go
Feb 21, 2022
c908135
Fixing indentation
Feb 21, 2022
419db34
Removing dependency on `testify` as requested by Katy Moe
Feb 21, 2022
c9e8373
Rewarding description for 2 fields
Feb 21, 2022
918bcb5
Moving "types" into "types.go" and out of "util.go"
Feb 23, 2022
da02733
Adding input argument validations to `tls_private_key`
Feb 23, 2022
e4e9ac2
Updating markdown documentation to address PR feedback
Feb 23, 2022
14c1d82
Avoided creating exported constants in `internal/openssh` library as …
Feb 23, 2022
cfde7ba
Fix typo: marshall -> marshal
Feb 23, 2022
c7c4de6
Adding a 'copyright header' on the 'internal/openssh/lib.go' file
Feb 24, 2022
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
201 changes: 201 additions & 0 deletions internal/openssh/lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package openssh

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/binary"
"encoding/pem"
"errors"
"fmt"
"math/big"

"golang.org/x/crypto/ssh"
)

// This module cherry-picks from https://go-review.googlesource.com/c/crypto/+/218620,
// that provides a solution for https://github.com/golang/go/issues/37132, that highlights
// the need for methods to marshal private keys to the OpenSSH format in `x/crypto/ssh`.
//
// TODO: https://github.com/hashicorp/terraform-provider-tls/issues/154
// This provides utilities to serialize a Private Keys in OpenSSH format:
// once this goes mainstream, we can remove this module.

const magic = "openssh-key-v1\x00"

// MarshalPrivateKey returns a *pem.Block with the private key serialized in the OpenSSH format.
//
// See: https://coolaj86.com/articles/the-openssh-private-key-format/
//
// NOTE: OpenSSH doesn't handle elliptic curve P-224, so `x/crypto/ssh` doesn't either
// and this applies to this function as well.
func MarshalPrivateKey(key crypto.PrivateKey, comment string) (*pem.Block, error) {
return marshalOpenSSHPrivateKey(key, comment, unencryptedOpenSSHMarshaller)
}

type openSSHMarshallerFunc func(msg interface{}) (ProtectedKeyBlock []byte, cipherName, kdfName, kdfOptions string, err error)

func generateOpenSSHPadding(block []byte, blockSize int) []byte {
for i, l := 0, len(block); (l+i)%blockSize != 0; i++ {
block = append(block, byte(i+1))
}
return block
}

func unencryptedOpenSSHMarshaller(msg interface{}) ([]byte, string, string, string, error) {
privKeyBlock := ssh.Marshal(msg)
key := generateOpenSSHPadding(privKeyBlock, 8)
return key, "none", "none", "", nil
}

func marshalOpenSSHPrivateKey(key crypto.PrivateKey, comment string, openSSHMarshaller openSSHMarshallerFunc) (*pem.Block, error) {
var w struct {
CipherName string
KdfName string
KdfOpts string
NumKeys uint32
PubKey []byte
PrivKeyBlock []byte
}
var pk1 struct {
Check1 uint32
Check2 uint32
Keytype string
Rest []byte `ssh:"rest"`
}

// Random check bytes.
var check uint32
if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil {
return nil, err
}

pk1.Check1 = check
pk1.Check2 = check
w.NumKeys = 1

// Use a []byte directly on ed25519 keys.
if k, ok := key.(*ed25519.PrivateKey); ok {
key = *k
}

switch k := key.(type) {
case *rsa.PrivateKey:
E := new(big.Int).SetInt64(int64(k.PublicKey.E))
// Marshal public key:
// E and N are in reversed order in the public and private key.
pubKey := struct {
KeyType string
E *big.Int
N *big.Int
}{
ssh.KeyAlgoRSA,
E, k.PublicKey.N,
}
w.PubKey = ssh.Marshal(pubKey)

// Marshal private key.
key := struct {
N *big.Int
E *big.Int
D *big.Int
Iqmp *big.Int
P *big.Int
Q *big.Int
Comment string
}{
k.PublicKey.N, E,
k.D, k.Precomputed.Qinv, k.Primes[0], k.Primes[1],
comment,
}
pk1.Keytype = ssh.KeyAlgoRSA
pk1.Rest = ssh.Marshal(key)
case ed25519.PrivateKey:
pub := make([]byte, ed25519.PublicKeySize)
priv := make([]byte, ed25519.PrivateKeySize)
copy(pub, k[ed25519.PublicKeySize:])
copy(priv, k)

// Marshal public key.
pubKey := struct {
KeyType string
Pub []byte
}{
ssh.KeyAlgoED25519, pub,
}
w.PubKey = ssh.Marshal(pubKey)

// Marshal private key.
key := struct {
Pub []byte
Priv []byte
Comment string
}{
pub, priv,
comment,
}
pk1.Keytype = ssh.KeyAlgoED25519
pk1.Rest = ssh.Marshal(key)
case *ecdsa.PrivateKey:
var curve, keyType string
switch name := k.Curve.Params().Name; name {
case "P-256":
curve = "nistp256"
keyType = ssh.KeyAlgoECDSA256
case "P-384":
curve = "nistp384"
keyType = ssh.KeyAlgoECDSA384
case "P-521":
curve = "nistp521"
keyType = ssh.KeyAlgoECDSA521
default:
return nil, errors.New("ssh: unhandled elliptic curve " + name)
}

pub := elliptic.Marshal(k.Curve, k.PublicKey.X, k.PublicKey.Y)

// Marshal public key.
pubKey := struct {
KeyType string
Curve string
Pub []byte
}{
keyType, curve, pub,
}
w.PubKey = ssh.Marshal(pubKey)

// Marshal private key.
key := struct {
Curve string
Pub []byte
D *big.Int
Comment string
}{
curve, pub, k.D,
comment,
}
pk1.Keytype = keyType
pk1.Rest = ssh.Marshal(key)
default:
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
}

// Marshal key in Open SSH format
var err error
w.PrivKeyBlock, w.CipherName, w.KdfName, w.KdfOpts, err = openSSHMarshaller(pk1)
if err != nil {
return nil, err
}

// Then marshal/wrap the above in PEM format
b := ssh.Marshal(w)
block := &pem.Block{
Type: "OPENSSH PRIVATE KEY",
Bytes: append([]byte(magic), b...),
}

return block, nil
}
98 changes: 98 additions & 0 deletions internal/openssh/lib_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package openssh

import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/pem"
"golang.org/x/crypto/ssh"
"testing"
)

func TestOpenSSHFormat_MarshalAndUnmarshal_RSA(t *testing.T) {
// Given an RSA private key
rsaOrig, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
t.Errorf("Failed to generate RSA private key: %v", err)
}

// Marshal it to OpenSSH PEM format
pemOpenSSHPrvKey, err := MarshalPrivateKey(rsaOrig, "")
if err != nil {
t.Errorf("Failed to marshall RSA private key to OpenSSH PEM: %v", err)
}
pemOpenSSHPrvKeyBytes := pem.EncodeToMemory(pemOpenSSHPrvKey)

// Parse it back into an RSA private key
rawPrivateKey, err := ssh.ParseRawPrivateKey(pemOpenSSHPrvKeyBytes)
rsaParsed, ok := rawPrivateKey.(*rsa.PrivateKey)
if !ok {
t.Errorf("Failed to type assert RSA private key: %v", rawPrivateKey)
}

// Confirm RSA is valid
err = rsaParsed.Validate()
if err != nil {
t.Errorf("Parsed RSA private key is not valid: %v", err)
}
// Confirm it matches the original key by comparing the public ones
if !rsaParsed.Equal(rsaOrig) {
t.Errorf("Parsed RSA private key doesn't match the original")
}
}

func TestOpenSSHFormat_MarshalAndUnmarshal_ECDSA(t *testing.T) {
// Given an ECDSA private key
ecdsaOrig, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
t.Errorf("Failed to generate ECDSA private key: %v", err)
}

// Marshal it to OpenSSH PEM format
pemOpenSSHPrvKey, err := MarshalPrivateKey(ecdsaOrig, "")
if err != nil {
t.Errorf("Failed to marshall ECDSA private key to OpenSSH PEM: %v", err)
}
pemOpenSSHPrvKeyBytes := pem.EncodeToMemory(pemOpenSSHPrvKey)

// Parse it back into an ECDSA private key
rawPrivateKey, err := ssh.ParseRawPrivateKey(pemOpenSSHPrvKeyBytes)
ecdsaParsed, ok := rawPrivateKey.(*ecdsa.PrivateKey)
if !ok {
t.Errorf("Failed to type assert ECDSA private key: %v", rawPrivateKey)
}

// Confirm it matches the original key by comparing the public ones
if !ecdsaParsed.Equal(ecdsaOrig) {
t.Errorf("Parsed ECDSA private key doesn't match the original")
}
}

func TestOpenSSHFormat_MarshalAndUnmarshal_ED25519(t *testing.T) {
// Given an ED25519 private key
_, ed25519Orig, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Errorf("Failed to generate ED25519 private key: %v", err)
}

// Marshal it to OpenSSH PEM format
pemOpenSSHPrvKey, err := MarshalPrivateKey(ed25519Orig, "")
if err != nil {
t.Errorf("Failed to marshall ED25519 private key to OpenSSH PEM: %v", err)
}
pemOpenSSHPrvKeyBytes := pem.EncodeToMemory(pemOpenSSHPrvKey)

// Parse it back into an ED25519 private key
rawPrivateKey, err := ssh.ParseRawPrivateKey(pemOpenSSHPrvKeyBytes)
ed25519Parsed, ok := rawPrivateKey.(*ed25519.PrivateKey)
if !ok {
t.Errorf("Failed to type assert ED25519 private key: %v", rawPrivateKey)
}

// Confirm it matches the original key by comparing the public ones
if !ed25519Parsed.Equal(ed25519Orig) {
t.Errorf("Parsed ED25519 private key doesn't match the original")
}
}
27 changes: 20 additions & 7 deletions internal/provider/data_source_public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,41 @@ func dataSourcePublicKey() *schema.Resource {
Sensitive: true,
Description: "PEM formatted string to use as the private key",
},

"algorithm": {
Type: schema.TypeString,
Computed: true,
Description: "Name of the algorithm to use to generate the private key",
},

"public_key_pem": {
Type: schema.TypeString,
Computed: true,
Type: schema.TypeString,
Description: "Public key data in PEM format",
Computed: true,
},

"public_key_openssh": {
Type: schema.TypeString,
Computed: true,
Type: schema.TypeString,
Description: "Public key data in OpenSSH-compatible PEM format",
Computed: true,
},

"public_key_fingerprint_md5": {
Type: schema.TypeString,
Computed: true,
Type: schema.TypeString,
Description: "Fingerprint of the public key data in OpenSSH MD5 hash format",
Computed: true,
},

"public_key_fingerprint_sha256": {
Type: schema.TypeString,
Description: "Fingerprint of the public key data in OpenSSH SHA256 hash format",
Computed: true,
},
},
}
}

func dataSourcePublicKeyRead(d *schema.ResourceData, meta interface{}) error {
func dataSourcePublicKeyRead(d *schema.ResourceData, _ interface{}) error {
// Read private key
bytes := []byte("")
if v, ok := d.GetOk("private_key_pem"); ok {
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/data_source_public_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestAccPublicKey_dataSource(t *testing.T) {

const testAccDataSourcePublicKeyConfig = `
data "tls_public_key" "test" {
private_key_pem = <<EOF
private_key_pem = <<EOF
%s
EOF
}
Expand Down
Loading