Skip to content

Commit

Permalink
Fix logic for extracting publicKey and subjectKeyId for ED25519 key (#…
Browse files Browse the repository at this point in the history
…182)

* Fix inability to generate "subject key ID" for ED25519

In the process, drastically simplified its implementation.

* Fixed bug where we were passing around ED25519 keys by reference, while it's library clearly prefers the use of copy

This is more about remain consistent with the interface of the library we are using, than actually optimal choice

* Add tests to prove that we can use ED25519 keys for all our certificates

* Use generic `crypto.Signer` interface when getting a `crypto.PublicKey` from a `crypto.PrivateKey`

* Generate Subject Key ID using ASN1 marshalling

* Fixing typos

* Splitted tests for performance

* Adding nil pointer checks
  • Loading branch information
Ivan De Marino authored Apr 5, 2022
1 parent 53ab2c1 commit 8e76bad
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 40 deletions.
37 changes: 26 additions & 11 deletions internal/provider/common_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
Expand All @@ -12,7 +13,6 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"math/big"
"sort"
Expand Down Expand Up @@ -68,24 +68,39 @@ func supportedKeyUsages() []string {
}

// generateSubjectKeyID generates a SHA-1 hash of the subject public key.
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
var publicKeyBytes []byte
func generateSubjectKeyID(pubKey crypto.PublicKey) ([]byte, error) {
var pubKeyBytes []byte
var err error

switch pub := pub.(type) {
// Marshal public key to bytes or set an error
switch pub := pubKey.(type) {
case *rsa.PublicKey:
publicKeyBytes, err = asn1.Marshal(*pub)
if err != nil {
return nil, err
if pub != nil {
pubKeyBytes, err = asn1.Marshal(*pub)
} else {
err = fmt.Errorf("received 'nil' pointer instead of public key")
}
case *ecdsa.PublicKey:
publicKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y)
pubKeyBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y)
case ed25519.PublicKey:
pubKeyBytes, err = asn1.Marshal(pub)
case *ed25519.PublicKey:
if pub != nil {
pubKeyBytes, err = asn1.Marshal(*pub)
} else {
err = fmt.Errorf("received 'nil' pointer instead of public key")
}
default:
return nil, errors.New("only RSA and ECDSA public keys supported")
err = fmt.Errorf("unsupported public key type %T", pub)
}

// If any of the cases above failed, an error would have been set
if err != nil {
return nil, fmt.Errorf("failed to marshal public key of type %T: %w", pubKey, err)
}

hash := sha1.Sum(publicKeyBytes)
return hash[:], nil
pubKeyHash := sha1.Sum(pubKeyBytes)
return pubKeyHash[:], nil
}

// setCertificateSubjectSchema sets on the given reference to map of schema.Schema
Expand Down
43 changes: 21 additions & 22 deletions internal/provider/common_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var keyGenerators = map[Algorithm]keyGenerator{
if err != nil {
return nil, fmt.Errorf("failed to generate ED25519 key: %s", err)
}
return &key, err
return key, err
},
}

Expand Down Expand Up @@ -97,7 +97,7 @@ func parsePrivateKeyPEM(keyPEMBytes []byte) (crypto.PrivateKey, Algorithm, error
// Identify the Algorithm of the crypto.PrivateKey
algorithm, err := privateKeyToAlgorithm(prvKey)
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("failed to determine key algorithm for private key of type %T: %w", prvKey, err)
}

return prvKey, algorithm, nil
Expand All @@ -109,53 +109,52 @@ func parsePrivateKeyPEM(keyPEMBytes []byte) (crypto.PrivateKey, Algorithm, error
func parsePrivateKeyOpenSSHPEM(keyOpenSSHPEMBytes []byte) (crypto.PrivateKey, Algorithm, error) {
prvKey, err := ssh.ParseRawPrivateKey(keyOpenSSHPEMBytes)
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("failed to parse openssh private key: %w", err)
}

algorithm, err := privateKeyToAlgorithm(prvKey)
if err != nil {
return nil, "", err
return nil, "", fmt.Errorf("failed to determine key algorithm for private key of type %T: %w", prvKey, err)
}

return prvKey, algorithm, nil
}

// privateKeyToPublicKey takes a crypto.PrivateKey and extracts the corresponding crypto.PublicKey,
// after having figured out its type.
func privateKeyToPublicKey(prvKey crypto.PrivateKey) crypto.PublicKey {
switch k := prvKey.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
case *ed25519.PrivateKey:
return k.Public()
default:
return nil
func privateKeyToPublicKey(prvKey crypto.PrivateKey) (crypto.PublicKey, error) {
signer, ok := prvKey.(crypto.Signer)
if !ok {
return nil, fmt.Errorf("unsupported private key type: %T", prvKey)
}

return signer.Public(), nil
}

// privateKeyToAlgorithm identifies the Algorithm used by a given crypto.PrivateKey.
func privateKeyToAlgorithm(prvKey crypto.PrivateKey) (Algorithm, error) {
switch k := prvKey.(type) {
case *rsa.PrivateKey:
switch prvKey.(type) {
case rsa.PrivateKey, *rsa.PrivateKey:
return RSA, nil
case *ecdsa.PrivateKey:
case ecdsa.PrivateKey, *ecdsa.PrivateKey:
return ECDSA, nil
case *ed25519.PrivateKey:
case ed25519.PrivateKey, *ed25519.PrivateKey:
return ED25519, nil
default:
return "", fmt.Errorf("failed to identify key algorithm for unsupported private key: %#v", k)
return "", fmt.Errorf("unsupported private key type: %T", prvKey)
}
}

// setPublicKeyAttributes takes a crypto.PrivateKey, extracts the corresponding crypto.PublicKey and then
// encodes related attributes on the given schema.ResourceData.
func setPublicKeyAttributes(d *schema.ResourceData, prvKey crypto.PrivateKey) error {
pubKey := privateKeyToPublicKey(prvKey)
pubKey, err := privateKeyToPublicKey(prvKey)
if err != nil {
return fmt.Errorf("failed to get public key from private key: %w", err)
}
pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return fmt.Errorf("failed to marshal public key error: %s", err)
return fmt.Errorf("failed to marshal public key: %w", err)
}
pubKeyPemBlock := &pem.Block{
Type: PreamblePublicKey.String(),
Expand All @@ -170,7 +169,7 @@ func setPublicKeyAttributes(d *schema.ResourceData, prvKey crypto.PrivateKey) er

// NOTE: ECDSA keys with elliptic curve P-224 are not supported by `x/crypto/ssh`,
// so this will return an error: in that case, we set the below fields to emptry strings
sshPubKey, err := ssh.NewPublicKey(privateKeyToPublicKey(prvKey))
sshPubKey, err := ssh.NewPublicKey(pubKey)
var pubKeySSH, pubKeySSHFingerprintMD5, pubKeySSHFingerprintSHA256 string
if err == nil {
sshPubKeyBytes := ssh.MarshalAuthorizedKey(sshPubKey)
Expand Down
162 changes: 159 additions & 3 deletions internal/provider/resource_locally_signed_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package provider
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"regexp"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -127,7 +127,7 @@ func TestLocallySignedCert(t *testing.T) {
// Verify certificate
_, err = cert.Verify(x509.VerifyOptions{Roots: certPool})
if err == nil {
return errors.New("incorrectly verified certificate")
return fmt.Errorf("incorrectly verified certificate")
} else if _, ok := err.(x509.UnknownAuthorityError); !ok {
return fmt.Errorf("incorrect verify error: expected UnknownAuthorityError, got %v", err)
}
Expand Down Expand Up @@ -412,7 +412,7 @@ func TestAccLocallySignedCert_HandleKeyAlgorithmDeprecation(t *testing.T) {
// Verify certificate
_, err = cert.Verify(x509.VerifyOptions{Roots: certPool})
if err == nil {
return errors.New("incorrectly verified certificate")
return fmt.Errorf("incorrectly verified certificate")
} else if _, ok := err.(x509.UnknownAuthorityError); !ok {
return fmt.Errorf("incorrect verify error: expected UnknownAuthorityError, got %v", err)
}
Expand Down Expand Up @@ -463,3 +463,159 @@ EOT
}
`, testCertRequest, validity, earlyRenewal, testCACert, caKeyAlgorithmCfg, testCAPrivateKey)
}

func TestAccResourceLocallySignedCert_FromED25519PrivateKeyResource(t *testing.T) {
r.UnitTest(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
{
Config: `
resource "tls_private_key" "ca_prv_test" {
algorithm = "ED25519"
}
resource "tls_self_signed_cert" "ca_cert_test" {
private_key_pem = tls_private_key.ca_prv_test.private_key_pem
subject {
organization = "test-organization"
}
is_ca_certificate = true
validity_period_hours = 8760
allowed_uses = [
"cert_signing",
]
}
resource "tls_private_key" "test" {
algorithm = "ED25519"
}
resource "tls_cert_request" "test" {
private_key_pem = tls_private_key.test.private_key_pem
subject {
common_name = "test.com"
}
}
resource "tls_locally_signed_cert" "test" {
validity_period_hours = 1
early_renewal_hours = 0
allowed_uses = [
"server_auth",
"client_auth",
]
cert_request_pem = tls_cert_request.test.cert_request_pem
ca_cert_pem = tls_self_signed_cert.ca_cert_test.cert_pem
ca_private_key_pem = tls_private_key.ca_prv_test.private_key_pem
}
`,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ca_key_algorithm", "ED25519"),
r.TestMatchResourceAttr("tls_locally_signed_cert.test", "cert_pem", regexp.MustCompile(`-----BEGIN CERTIFICATE-----((.|\n)+?)-----END CERTIFICATE-----`)),
),
},
},
})
}

func TestAccResourceLocallySignedCert_FromECDSAPrivateKeyResource(t *testing.T) {
r.UnitTest(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
{
Config: `
resource "tls_private_key" "ca_prv_test" {
algorithm = "ECDSA"
}
resource "tls_self_signed_cert" "ca_cert_test" {
private_key_pem = tls_private_key.ca_prv_test.private_key_pem
subject {
organization = "test-organization"
}
is_ca_certificate = true
validity_period_hours = 8760
allowed_uses = [
"cert_signing",
]
}
resource "tls_private_key" "test" {
algorithm = "ECDSA"
}
resource "tls_cert_request" "test" {
private_key_pem = tls_private_key.test.private_key_pem
subject {
common_name = "test.com"
}
}
resource "tls_locally_signed_cert" "test" {
validity_period_hours = 1
early_renewal_hours = 0
allowed_uses = [
"server_auth",
"client_auth",
]
cert_request_pem = tls_cert_request.test.cert_request_pem
ca_cert_pem = tls_self_signed_cert.ca_cert_test.cert_pem
ca_private_key_pem = tls_private_key.ca_prv_test.private_key_pem
}
`,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ca_key_algorithm", "ECDSA"),
r.TestMatchResourceAttr("tls_locally_signed_cert.test", "cert_pem", regexp.MustCompile(`-----BEGIN CERTIFICATE-----((.|\n)+?)-----END CERTIFICATE-----`)),
),
},
},
})
}

func TestAccResourceLocallySignedCert_FromRSAPrivateKeyResource(t *testing.T) {
r.UnitTest(t, r.TestCase{
Providers: testProviders,
Steps: []r.TestStep{
{
Config: `
resource "tls_private_key" "ca_prv_test" {
algorithm = "RSA"
}
resource "tls_self_signed_cert" "ca_cert_test" {
private_key_pem = tls_private_key.ca_prv_test.private_key_pem
subject {
organization = "test-organization"
}
is_ca_certificate = true
validity_period_hours = 8760
allowed_uses = [
"cert_signing",
]
}
resource "tls_private_key" "test" {
algorithm = "RSA"
}
resource "tls_cert_request" "test" {
private_key_pem = tls_private_key.test.private_key_pem
subject {
common_name = "test.com"
}
}
resource "tls_locally_signed_cert" "test" {
validity_period_hours = 1
early_renewal_hours = 0
allowed_uses = [
"server_auth",
"client_auth",
]
cert_request_pem = tls_cert_request.test.cert_request_pem
ca_cert_pem = tls_self_signed_cert.ca_cert_test.cert_pem
ca_private_key_pem = tls_private_key.ca_prv_test.private_key_pem
}
`,
Check: r.ComposeTestCheckFunc(
r.TestCheckResourceAttr("tls_locally_signed_cert.test", "ca_key_algorithm", "RSA"),
r.TestMatchResourceAttr("tls_locally_signed_cert.test", "cert_pem", regexp.MustCompile(`-----BEGIN CERTIFICATE-----((.|\n)+?)-----END CERTIFICATE-----`)),
),
},
},
})
}
4 changes: 2 additions & 2 deletions internal/provider/resource_private_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ func createResourcePrivateKey(d *schema.ResourceData, _ interface{}) error {
if k.Curve.Params().Name == "P-224" {
doMarshalOpenSSHKeyPemBlock = false
}
case *ed25519.PrivateKey:
keyBytes, err := x509.MarshalPKCS8PrivateKey(*k)
case ed25519.PrivateKey:
keyBytes, err := x509.MarshalPKCS8PrivateKey(k)
if err != nil {
return fmt.Errorf("error encoding key to PEM: %s", err)
}
Expand Down
8 changes: 6 additions & 2 deletions internal/provider/resource_self_signed_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func resourceSelfSignedCert() *schema.Resource {
}
}

func createSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
func createSelfSignedCert(d *schema.ResourceData, _ interface{}) error {
key, algorithm, err := parsePrivateKeyPEM([]byte(d.Get("private_key_pem").(string)))
if err != nil {
return err
Expand Down Expand Up @@ -73,5 +73,9 @@ func createSelfSignedCert(d *schema.ResourceData, meta interface{}) error {
cert.URIs = append(cert.URIs, uri)
}

return createCertificate(d, &cert, &cert, privateKeyToPublicKey(key), key)
publicKey, err := privateKeyToPublicKey(key)
if err != nil {
return fmt.Errorf("failed to get public key from private key: %w", err)
}
return createCertificate(d, &cert, &cert, publicKey, key)
}
Loading

0 comments on commit 8e76bad

Please sign in to comment.