diff --git a/.gitignore b/.gitignore index 1498b86e42..c1161c27bc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,9 @@ *.test .DS_Store .fseventsd +.envrc .vagrant/ +.idea/ /pkg Thumbs.db bin/ diff --git a/agent/config/builder.go b/agent/config/builder.go index 583923f19e..297d65e458 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -588,6 +588,8 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { "leaf_cert_ttl": "LeafCertTTL", "csr_max_per_second": "CSRMaxPerSecond", "csr_max_concurrent": "CSRMaxConcurrent", + "private_key_type": "PrivateKeyType", + "private_key_bits": "PrivateKeyBits", }) } diff --git a/agent/connect/ca/provider_consul.go b/agent/connect/ca/provider_consul.go index e2559a7657..1b0d10d48f 100644 --- a/agent/connect/ca/provider_consul.go +++ b/agent/connect/ca/provider_consul.go @@ -138,7 +138,7 @@ func (c *ConsulProvider) GenerateRoot() error { // Generate a private key if needed newState := *providerState if c.config.PrivateKey == "" { - _, pk, err := connect.GeneratePrivateKey() + _, pk, err := connect.GeneratePrivateKeyWithConfig(c.config.PrivateKeyType, c.config.PrivateKeyBits) if err != nil { return err } @@ -184,7 +184,7 @@ func (c *ConsulProvider) GenerateIntermediateCSR() (string, error) { } // Create a new private key and CSR. - signer, pk, err := connect.GeneratePrivateKey() + signer, pk, err := connect.GeneratePrivateKeyWithConfig(c.config.PrivateKeyType, c.config.PrivateKeyBits) if err != nil { return "", err } diff --git a/agent/connect/ca/provider_consul_config.go b/agent/connect/ca/provider_consul_config.go index f3af3a4898..b290832ac4 100644 --- a/agent/connect/ca/provider_consul_config.go +++ b/agent/connect/ca/provider_consul_config.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" "github.com/mitchellh/mapstructure" ) @@ -41,6 +42,8 @@ func ParseConsulCAConfig(raw map[string]interface{}) (*structs.ConsulCAProviderC func defaultCommonConfig() structs.CommonCAProviderConfig { return structs.CommonCAProviderConfig{ - LeafCertTTL: 3 * 24 * time.Hour, + LeafCertTTL: 3 * 24 * time.Hour, + PrivateKeyType: connect.DefaultPrivateKeyType, + PrivateKeyBits: connect.DefaultPrivateKeyBits, } } diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index c9c0076deb..f1fef4706c 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -104,6 +104,8 @@ func (v *VaultProvider) GenerateRoot() error { _, err = v.client.Logical().Write(v.config.RootPKIPath+"root/generate/internal", map[string]interface{}{ "common_name": fmt.Sprintf("Vault CA Root Authority %s", uuid), "uri_sans": spiffeID.URI().String(), + "key_type": v.config.PrivateKeyType, + "key_bits": v.config.PrivateKeyBits, }) if err != nil { return err @@ -174,8 +176,8 @@ func (v *VaultProvider) generateIntermediateCSR() (string, error) { // Generate a new intermediate CSR for the root to sign. data, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{ "common_name": "Vault CA Intermediate Authority", - "key_bits": 224, - "key_type": "ec", + "key_type": v.config.PrivateKeyType, + "key_bits": v.config.PrivateKeyBits, "uri_sans": spiffeID.URI().String(), }) if err != nil { diff --git a/agent/connect/generate.go b/agent/connect/generate.go index 47ea5f43e7..0e9bc911db 100644 --- a/agent/connect/generate.go +++ b/agent/connect/generate.go @@ -6,30 +6,93 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" + "strings" ) -// GeneratePrivateKey generates a new Private key -func GeneratePrivateKey() (crypto.Signer, string, error) { - var pk *ecdsa.PrivateKey +const ( + DefaultPrivateKeyType = "ec" + DefaultPrivateKeyBits = 256 +) - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +func pemEncodeKey(key []byte, blockType string) (string, error) { + var buf bytes.Buffer + + if err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: key}); err != nil { + return "", fmt.Errorf("error encoding private key: %s", err) + } + return buf.String(), nil +} + +func generateRSAKey(keyBits int) (crypto.Signer, string, error) { + var pk *rsa.PrivateKey + + pk, err := rsa.GenerateKey(rand.Reader, keyBits) if err != nil { - return nil, "", fmt.Errorf("error generating private key: %s", err) + return nil, "", fmt.Errorf("error generating RSA private key: %s", err) + } + + bs := x509.MarshalPKCS1PrivateKey(pk) + pemBlock, err := pemEncodeKey(bs, "RSA PRIVATE KEY") + if err != nil { + return nil, "", err + } + + return pk, pemBlock, nil +} + +func generateECDSAKey(keyBits int) (crypto.Signer, string, error) { + var pk *ecdsa.PrivateKey + var curve elliptic.Curve + + switch keyBits { + case 224: + curve = elliptic.P224() + case 256: + curve = elliptic.P256() + case 384: + curve = elliptic.P384() + case 521: + curve = elliptic.P521() + default: + return nil, "", fmt.Errorf("error generating ECDSA private key: unknown curve length %d", keyBits) + } + + pk, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, "", fmt.Errorf("error generating ECDSA private key: %s", err) } bs, err := x509.MarshalECPrivateKey(pk) if err != nil { - return nil, "", fmt.Errorf("error generating private key: %s", err) + return nil, "", fmt.Errorf("error marshaling ECDSA private key: %s", err) } - var buf bytes.Buffer - err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs}) + pemBlock, err := pemEncodeKey(bs, "EC PRIVATE KEY") if err != nil { - return nil, "", fmt.Errorf("error encoding private key: %s", err) + return nil, "", err } - return pk, buf.String(), nil + return pk, pemBlock, nil +} + +// GeneratePrivateKey generates a new Private key +func GeneratePrivateKeyWithConfig(keyType string, keyBits int) (crypto.Signer, string, error) { + switch strings.ToLower(keyType) { + case "rsa": + return generateRSAKey(keyBits) + case "ec": + return generateECDSAKey(keyBits) + default: + return nil, "", fmt.Errorf("unknown private key type requested: %s", keyType) + } +} + +func GeneratePrivateKey() (crypto.Signer, string, error) { + // TODO: find any calls to this func, replace with calls to GeneratePrivateKeyWithConfig() + // using prefs `private_key_type` and `private_key_bits` + return GeneratePrivateKeyWithConfig(DefaultPrivateKeyType, DefaultPrivateKeyBits) } diff --git a/agent/connect/generate_test.go b/agent/connect/generate_test.go new file mode 100644 index 0000000000..24715b0e84 --- /dev/null +++ b/agent/connect/generate_test.go @@ -0,0 +1,150 @@ +package connect + +import ( + "fmt" + "testing" + "time" + + "crypto/x509" + "encoding/pem" + + "github.com/hashicorp/consul/agent/structs" + "github.com/stretchr/testify/require" +) + +type KeyConfig struct { + keyType string + keyBits int +} + +var goodParams, badParams []KeyConfig + +func init() { + goodParams = []KeyConfig{ + {keyType: "rsa", keyBits: 2048}, + {keyType: "rsa", keyBits: 4096}, + {keyType: "ec", keyBits: 224}, + {keyType: "ec", keyBits: 256}, + {keyType: "ec", keyBits: 384}, + {keyType: "ec", keyBits: 521}, + } + badParams = []KeyConfig{ + {keyType: "rsa", keyBits: 0}, + {keyType: "rsa", keyBits: 1024}, + {keyType: "rsa", keyBits: 24601}, + {keyType: "ec", keyBits: 0}, + {keyType: "ec", keyBits: 512}, + {keyType: "ec", keyBits: 321}, + {keyType: "ecdsa", keyBits: 256}, // test for "ecdsa" instead of "ec" + {keyType: "aes", keyBits: 128}, + } +} + +func makeConfig(kc KeyConfig) structs.CommonCAProviderConfig { + return structs.CommonCAProviderConfig{ + LeafCertTTL: 3 * 24 * time.Hour, + PrivateKeyType: kc.keyType, + PrivateKeyBits: kc.keyBits, + } +} + +func testGenerateRSAKey(t *testing.T, bits int) { + r := require.New(t) + _, rsaBlock, err := GeneratePrivateKeyWithConfig("rsa", bits) + r.NoError(err) + r.Contains(rsaBlock, "RSA PRIVATE KEY") + + rsaBytes, _ := pem.Decode([]byte(rsaBlock)) + r.NotNil(rsaBytes) + + rsaKey, err := x509.ParsePKCS1PrivateKey(rsaBytes.Bytes) + r.NoError(err) + r.NoError(rsaKey.Validate()) + r.Equal(bits/8, rsaKey.Size()) // note: returned size is in bytes. 2048/8==256 +} + +func testGenerateECDSAKey(t *testing.T, bits int) { + r := require.New(t) + _, pemBlock, err := GeneratePrivateKeyWithConfig("ec", bits) + r.NoError(err) + r.Contains(pemBlock, "EC PRIVATE KEY") + + block, _ := pem.Decode([]byte(pemBlock)) + r.NotNil(block) + + pk, err := x509.ParseECPrivateKey(block.Bytes) + r.NoError(err) + r.Equal(bits, pk.Curve.Params().BitSize) +} + +// Tests to make sure we are able to generate every type of private key supported by the x509 lib. +func TestGenerateKeys(t *testing.T) { + t.Parallel() + for _, params := range goodParams { + t.Run(fmt.Sprintf("TestGenerateKeys-%s-%d", params.keyType, params.keyBits), + func(t *testing.T) { + switch params.keyType { + case "rsa": + testGenerateRSAKey(t, params.keyBits) + case "ec": + testGenerateECDSAKey(t, params.keyBits) + default: + t.Fatalf("unkown key type: %s", params.keyType) + } + }) + } +} + +// Tests a variety of valid private key configs to make sure they're accepted. +func TestValidateGoodConfigs(t *testing.T) { + t.Parallel() + for _, params := range goodParams { + config := makeConfig(params) + t.Run(fmt.Sprintf("TestValidateGoodConfigs-%s-%d", params.keyType, params.keyBits), + func(t *testing.T) { + require.New(t).NoError(config.Validate(), "unexpected error: type=%s bits=%d", + params.keyType, params.keyBits) + }) + + } +} + +// Tests a variety of invalid private key configs to make sure they're caught. +func TestValidateBadConfigs(t *testing.T) { + t.Parallel() + for _, params := range badParams { + config := makeConfig(params) + t.Run(fmt.Sprintf("TestValidateBadConfigs-%s-%d", params.keyType, params.keyBits), func(t *testing.T) { + require.New(t).Error(config.Validate(), "expected error: type=%s bits=%d", + params.keyType, params.keyBits) + }) + } +} + +// Tests the ability of a CA to sign a CSR using a different key type. If the key types differ, the test should fail. +func TestSignatureMismatches(t *testing.T) { + t.Parallel() + r := require.New(t) + for _, p1 := range goodParams { + for _, p2 := range goodParams { + if p1 == p2 { + continue + } + t.Run(fmt.Sprintf("TestMismatches-%s%d-%s%d", p1.keyType, p1.keyBits, p2.keyType, p2.keyBits), func(t *testing.T) { + ca := TestCAWithKeyType(t, nil, p1.keyType, p1.keyBits) + r.Equal(p1.keyType, ca.PrivateKeyType) + r.Equal(p1.keyBits, ca.PrivateKeyBits) + certPEM, keyPEM, err := testLeaf(t, "foobar.service.consul", ca, p2.keyType, p2.keyBits) + if p1.keyType == p2.keyType { + r.NoError(err) + _, err := ParseCert(certPEM) + r.NoError(err) + _, err = ParseSigner(keyPEM) + r.NoError(err) + } else { + r.Error(err) + } + }) + } + } +} diff --git a/agent/connect/parsing.go b/agent/connect/parsing.go index 163f36083e..ff0f0813db 100644 --- a/agent/connect/parsing.go +++ b/agent/connect/parsing.go @@ -3,6 +3,7 @@ package connect import ( "crypto" "crypto/ecdsa" + "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/x509" @@ -97,6 +98,7 @@ func ParseCSR(pemValue string) (*x509.CertificateRequest, error) { func KeyId(raw interface{}) ([]byte, error) { switch raw.(type) { case *ecdsa.PublicKey: + case *rsa.PublicKey: default: return nil, fmt.Errorf("invalid key type: %T", raw) } diff --git a/agent/connect/testing_ca.go b/agent/connect/testing_ca.go index a82ec91aa8..1f06ca8096 100644 --- a/agent/connect/testing_ca.go +++ b/agent/connect/testing_ca.go @@ -3,8 +3,6 @@ package connect import ( "bytes" "crypto" - "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" @@ -27,21 +25,17 @@ const TestClusterID = "11111111-2222-3333-4444-555555555555" // unique names for the CA certs. var testCACounter uint64 -// TestCA creates a test CA certificate and signing key and returns it -// in the CARoot structure format. The returned CA will be set as Active = true. -// -// If xc is non-nil, then the returned certificate will have a signing cert -// that is cross-signed with the previous cert, and this will be set as -// SigningCert. -func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot { +func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *structs.CARoot { var result structs.CARoot result.Active = true result.Name = fmt.Sprintf("Test CA %d", atomic.AddUint64(&testCACounter, 1)) // Create the private key we'll use for this CA cert. - signer, keyPEM := testPrivateKey(t) + signer, keyPEM := testPrivateKey(t, keyType, keyBits) result.SigningKey = keyPEM result.SigningKeyID = HexString(testKeyID(t, signer.Public())) + result.PrivateKeyType = keyType + result.PrivateKeyBits = keyBits // The serial number for the cert sn, err := testSerialNumber() @@ -127,9 +121,23 @@ func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot { return &result } -// TestLeaf returns a valid leaf certificate and it's private key for the named -// service with the given CA Root. -func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string) { +// TestCA creates a test CA certificate and signing key and returns it +// in the CARoot structure format. The returned CA will be set as Active = true. +// +// If xc is non-nil, then the returned certificate will have a signing cert +// that is cross-signed with the previous cert, and this will be set as +// SigningCert. +func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot { + return testCA(t, xc, DefaultPrivateKeyType, DefaultPrivateKeyBits) +} + +// TestCAWithKeyType is similar to TestCA, except that it +// takes two additional arguments to override the default private key type and size. +func TestCAWithKeyType(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *structs.CARoot { + return testCA(t, xc, keyType, keyBits) +} + +func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string, keyBits int) (string, string, error) { // Parse the CA cert and signing key from the root cert := root.SigningCert if cert == "" { @@ -137,11 +145,11 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string } caCert, err := ParseCert(cert) if err != nil { - t.Fatalf("error parsing CA cert: %s", err) + return "", "", fmt.Errorf("error parsing CA cert: %s", err) } caSigner, err := ParseSigner(root.SigningKey) if err != nil { - t.Fatalf("error parsing signing key: %s", err) + return "", "", fmt.Errorf("error parsing signing key: %s", err) } // Build the SPIFFE ID @@ -155,13 +163,18 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string // The serial number for the cert sn, err := testSerialNumber() if err != nil { - t.Fatalf("error generating serial number: %s", err) + return "", "", fmt.Errorf("error generating serial number: %s", err) } // Generate fresh private key - pkSigner, pkPEM, err := GeneratePrivateKey() + pkSigner, pkPEM, err := GeneratePrivateKeyWithConfig(keyType, keyBits) if err != nil { - t.Fatalf("failed to generate private key: %s", err) + return "", "", fmt.Errorf("failed to generate private key: %s", err) + } + + sigAlgo := x509.ECDSAWithSHA256 + if keyType == "rsa" { + sigAlgo = x509.SHA256WithRSA } // Cert template for generation @@ -169,7 +182,7 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string SerialNumber: sn, Subject: pkix.Name{CommonName: service}, URIs: []*url.URL{spiffeId.URI()}, - SignatureAlgorithm: x509.ECDSAWithSHA256, + SignatureAlgorithm: sigAlgo, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement | @@ -190,14 +203,24 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string bs, err := x509.CreateCertificate( rand.Reader, &template, caCert, pkSigner.Public(), caSigner) if err != nil { - t.Fatalf("error generating certificate: %s", err) + return "", "", fmt.Errorf("error generating certificate: %s", err) } err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) if err != nil { - t.Fatalf("error encoding private key: %s", err) + return "", "", fmt.Errorf("error encoding private key: %s", err) } - return buf.String(), pkPEM + return buf.String(), pkPEM, nil +} + +// TestLeaf returns a valid leaf certificate and it's private key for the named +// service with the given CA Root. +func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string) { + certPEM, keyPEM, err := testLeaf(t, service, root, root.PrivateKeyType, root.PrivateKeyBits) + if err != nil { + t.Fatalf(err.Error()) + } + return certPEM, keyPEM } // TestCSR returns a CSR to sign the given service along with the PEM-encoded @@ -209,7 +232,7 @@ func TestCSR(t testing.T, uri CertURI) (string, string) { } // Create the private key we'll use - signer, pkPEM := testPrivateKey(t) + signer, pkPEM := testPrivateKey(t, DefaultPrivateKeyType, DefaultPrivateKeyBits) // Create the CSR itself var csrBuf bytes.Buffer @@ -253,24 +276,13 @@ func testKeyID(t testing.T, raw interface{}) []byte { // which will be the same for multiple CAs/Leafs. Also note that our UUID // generator also reads from crypto rand and is called far more often during // tests than this will be. -func testPrivateKey(t testing.T) (crypto.Signer, string) { - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +func testPrivateKey(t testing.T, keyType string, keyBits int) (crypto.Signer, string) { + pk, pkPEM, err := GeneratePrivateKeyWithConfig(keyType, keyBits) if err != nil { t.Fatalf("error generating private key: %s", err) } - bs, err := x509.MarshalECPrivateKey(pk) - if err != nil { - t.Fatalf("error generating private key: %s", err) - } - - var buf bytes.Buffer - err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs}) - if err != nil { - t.Fatalf("error encoding private key: %s", err) - } - - return pk, buf.String() + return pk, pkPEM } // testSerialNumber generates a serial number suitable for a certificate. For @@ -298,21 +310,12 @@ type TestAgentRPC interface { RPC(method string, args interface{}, reply interface{}) error } -// TestCAConfigSet sets a CARoot returned by TestCA into the TestAgent state. It -// requires that TestAgent had connect enabled in it's config. If ca is nil, a -// new CA is created. -// -// It returns the CARoot passed or created. -// -// Note that we have to use an interface for the TestAgent.RPC method since we -// can't introduce an import cycle by importing `agent.TestAgent` here directly. -// It also means this will work in a few other places we mock that method. -func TestCAConfigSet(t testing.T, a TestAgentRPC, - ca *structs.CARoot) *structs.CARoot { +func testCAConfigSet(t testing.T, a TestAgentRPC, + ca *structs.CARoot, keyType string, keyBits int) *structs.CARoot { t.Helper() if ca == nil { - ca = TestCA(t, nil) + ca = TestCAWithKeyType(t, nil, keyType, keyBits) } newConfig := &structs.CAConfiguration{ Provider: "consul", @@ -320,6 +323,8 @@ func TestCAConfigSet(t testing.T, a TestAgentRPC, "PrivateKey": ca.SigningKey, "RootCert": ca.RootCert, "RotationPeriod": 180 * 24 * time.Hour, + "PrivateKeyType": ca.PrivateKeyType, + "PrivateKeyBits": ca.PrivateKeyBits, }, } args := &structs.CARequest{ @@ -334,3 +339,24 @@ func TestCAConfigSet(t testing.T, a TestAgentRPC, } return ca } + +// TestCAConfigSet sets a CARoot returned by TestCA into the TestAgent state. It +// requires that TestAgent had connect enabled in it's config. If ca is nil, a +// new CA is created. +// +// It returns the CARoot passed or created. +// +// Note that we have to use an interface for the TestAgent.RPC method since we +// can't introduce an import cycle by importing `agent.TestAgent` here directly. +// It also means this will work in a few other places we mock that method. +func TestCAConfigSet(t testing.T, a TestAgentRPC, + ca *structs.CARoot) *structs.CARoot { + return testCAConfigSet(t, a, ca, DefaultPrivateKeyType, DefaultPrivateKeyBits) +} + +// TestCAConfigSetWithKeyType is similar to TestCAConfigSet, except that it +// takes two additional arguments to override the default private key type and size. +func TestCAConfigSetWithKeyType(t testing.T, a TestAgentRPC, + ca *structs.CARoot, keyType string, keyBits int) *structs.CARoot { + return testCAConfigSet(t, a, ca, keyType, keyBits) +} diff --git a/agent/connect/testing_ca_test.go b/agent/connect/testing_ca_test.go index 193e532c37..e2414b1010 100644 --- a/agent/connect/testing_ca_test.go +++ b/agent/connect/testing_ca_test.go @@ -1,6 +1,7 @@ package connect import ( + "fmt" "io/ioutil" "os" "os/exec" @@ -14,12 +15,21 @@ import ( var hasOpenSSL bool func init() { + goodParams = []KeyConfig{ + {keyType: "rsa", keyBits: 2048}, + {keyType: "rsa", keyBits: 4096}, + {keyType: "ec", keyBits: 224}, + {keyType: "ec", keyBits: 256}, + {keyType: "ec", keyBits: 384}, + {keyType: "ec", keyBits: 521}, + } + _, err := exec.LookPath("openssl") hasOpenSSL = err == nil } // Test that the TestCA and TestLeaf functions generate valid certificates. -func TestTestCAAndLeaf(t *testing.T) { +func testCAAndLeaf(t *testing.T, keyType string, keyBits int) { if !hasOpenSSL { t.Skip("openssl not found") return @@ -28,7 +38,7 @@ func TestTestCAAndLeaf(t *testing.T) { assert := assert.New(t) // Create the certs - ca := TestCA(t, nil) + ca := TestCAWithKeyType(t, nil, keyType, keyBits) leaf, _ := TestLeaf(t, "web", ca) // Create a temporary directory for storing the certs @@ -51,7 +61,7 @@ func TestTestCAAndLeaf(t *testing.T) { } // Test cross-signing. -func TestTestCAAndLeaf_xc(t *testing.T) { +func testCAAndLeaf_xc(t *testing.T, keyType string, keyBits int) { if !hasOpenSSL { t.Skip("openssl not found") return @@ -60,8 +70,8 @@ func TestTestCAAndLeaf_xc(t *testing.T) { assert := assert.New(t) // Create the certs - ca1 := TestCA(t, nil) - ca2 := TestCA(t, ca1) + ca1 := TestCAWithKeyType(t, nil, keyType, keyBits) + ca2 := TestCAWithKeyType(t, ca1, keyType, keyBits) leaf1, _ := TestLeaf(t, "web", ca1) leaf2, _ := TestLeaf(t, "web", ca2) @@ -98,3 +108,23 @@ func TestTestCAAndLeaf_xc(t *testing.T) { assert.Nil(err) } } + +func TestTestCAAndLeaf(t *testing.T) { + t.Parallel() + for _, params := range goodParams { + t.Run(fmt.Sprintf("TestTestCAAndLeaf-%s-%d", params.keyType, params.keyBits), + func(t *testing.T) { + testCAAndLeaf(t, params.keyType, params.keyBits) + }) + } +} + +func TestTestCAAndLeaf_xc(t *testing.T) { + t.Parallel() + for _, params := range goodParams { + t.Run(fmt.Sprintf("TestTestCAAndLeaf_xc-%s-%d", params.keyType, params.keyBits), + func(t *testing.T) { + testCAAndLeaf_xc(t, params.keyType, params.keyBits) + }) + } +} diff --git a/agent/connect_ca_endpoint_test.go b/agent/connect_ca_endpoint_test.go index d6a52a7266..e979efeb7a 100644 --- a/agent/connect_ca_endpoint_test.go +++ b/agent/connect_ca_endpoint_test.go @@ -73,6 +73,8 @@ func TestConnectCAConfig(t *testing.T) { RotationPeriod: 90 * 24 * time.Hour, } expected.LeafCertTTL = 72 * time.Hour + expected.PrivateKeyType = connect.DefaultPrivateKeyType + expected.PrivateKeyBits = connect.DefaultPrivateKeyBits // Get the initial config. { @@ -107,6 +109,7 @@ func TestConnectCAConfig(t *testing.T) { // The config should be updated now. { expected.RotationPeriod = time.Hour + req, _ := http.NewRequest("GET", "/v1/connect/ca/configuration", nil) resp := httptest.NewRecorder() obj, err := a.srv.ConnectCAConfiguration(resp, req) diff --git a/agent/consul/auto_encrypt.go b/agent/consul/auto_encrypt.go index 3acf15a614..6cc87154b5 100644 --- a/agent/consul/auto_encrypt.go +++ b/agent/consul/auto_encrypt.go @@ -47,8 +47,13 @@ func (c *Client) RequestAutoEncryptCerts(servers []string, defaultPort int, toke Agent: string(c.config.NodeName), } + conf, err := c.config.CAConfig.GetCommonConfig() + if err != nil { + return errFn(err) + } + // Create a new private key - pk, pkPEM, err := connect.GeneratePrivateKey() + pk, pkPEM, err := connect.GeneratePrivateKeyWithConfig(conf.PrivateKeyType, conf.PrivateKeyBits) if err != nil { return errFn(err) } diff --git a/agent/consul/connect_ca_endpoint.go b/agent/consul/connect_ca_endpoint.go index d2af4de839..980f9351ba 100644 --- a/agent/consul/connect_ca_endpoint.go +++ b/agent/consul/connect_ca_endpoint.go @@ -359,6 +359,8 @@ func (s *ConnectCA) Roots( IntermediateCerts: r.IntermediateCerts, RaftIndex: r.RaftIndex, Active: r.Active, + PrivateKeyType: r.PrivateKeyType, + PrivateKeyBits: r.PrivateKeyBits, } if r.Active { diff --git a/agent/consul/leader.go b/agent/consul/leader.go index a791cb9488..d2b8e9543e 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/consul/agent/metadata" @@ -17,9 +17,9 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/types" - memdb "github.com/hashicorp/go-memdb" - uuid "github.com/hashicorp/go-uuid" - version "github.com/hashicorp/go-version" + "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-version" "github.com/hashicorp/raft" "github.com/hashicorp/serf/serf" "golang.org/x/time/rate" diff --git a/agent/consul/leader_connect.go b/agent/consul/leader_connect.go index 61978ab88b..bda98abe75 100644 --- a/agent/consul/leader_connect.go +++ b/agent/consul/leader_connect.go @@ -215,6 +215,13 @@ func (s *Server) initializeRootCA(provider ca.Provider, conf *structs.CAConfigur return err } + commonConfig, err := conf.GetCommonConfig() + if err != nil { + return err + } + rootCA.PrivateKeyType = commonConfig.PrivateKeyType + rootCA.PrivateKeyBits = commonConfig.PrivateKeyBits + // Check if the CA root is already initialized and exit if it is, // adding on any existing intermediate certs since they aren't directly // tied to the provider. diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 090f1b89c8..2e47162bd8 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -102,6 +102,10 @@ type CARoot struct { // active root. RotatedOutAt time.Time `json:"-"` + // Type of private key used to create the CA cert. + PrivateKeyType string + PrivateKeyBits int + RaftIndex } @@ -277,6 +281,9 @@ type CommonCAProviderConfig struct { // immediately in the RPC goroutine. This is 0 by default and CSRMaxPerSecond // is used. This is ignored if CSRMaxPerSecond is non-zero. CSRMaxConcurrent int + + PrivateKeyType string + PrivateKeyBits int } func (c CommonCAProviderConfig) Validate() error { @@ -292,6 +299,19 @@ func (c CommonCAProviderConfig) Validate() error { return fmt.Errorf("leaf cert TTL must be less than 1 year") } + switch c.PrivateKeyType { + case "ec": + if c.PrivateKeyBits != 224 && c.PrivateKeyBits != 256 && c.PrivateKeyBits != 384 && c.PrivateKeyBits != 521 { + return fmt.Errorf("ECDSA key length must be one of (224, 256, 384, 521) bits") + } + case "rsa": + if c.PrivateKeyBits != 2048 && c.PrivateKeyBits != 4096 { + return fmt.Errorf("RSA key length must be 2048 or 4096 bits") + } + default: + return fmt.Errorf("private key type must be either 'ecdsa' or 'rsa'") + } + return nil } diff --git a/connect/certgen/certgen.go b/connect/certgen/certgen.go index 89c4245761..3f5cae0e7f 100644 --- a/connect/certgen/certgen.go +++ b/connect/certgen/certgen.go @@ -44,9 +44,15 @@ func main() { var numCAs = 2 var services = []string{"web", "db", "cache"} var outDir string + var keyType string = "ec" + var keyBits int = 256 flag.StringVar(&outDir, "out-dir", "", "REQUIRED: the dir to write certificates to") + flag.StringVar(&keyType, "key-type", "ec", + "Type of private key to create (ec, rsa)") + flag.IntVar(&keyBits, "key-bits", 256, + "Size of private key to create, in bits") flag.Parse() if outDir == "" { @@ -57,7 +63,7 @@ func main() { // Create CA certs var prevCA *structs.CARoot for i := 1; i <= numCAs; i++ { - ca := connect.TestCA(&testing.RuntimeT{}, prevCA) + ca := connect.TestCAWithKeyType(&testing.RuntimeT{}, prevCA, keyType, keyBits) prefix := fmt.Sprintf("%s/ca%d-ca", outDir, i) writeFile(prefix+".cert.pem", ca.RootCert) writeFile(prefix+".key.pem", ca.SigningKey) diff --git a/connect/proxy/listener_test.go b/connect/proxy/listener_test.go index 3e7a683ae4..358bb191eb 100644 --- a/connect/proxy/listener_test.go +++ b/connect/proxy/listener_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/hashicorp/consul/connect" "log" "net" "os" @@ -15,7 +16,6 @@ import ( "github.com/stretchr/testify/require" agConnect "github.com/hashicorp/consul/agent/connect" - "github.com/hashicorp/consul/connect" "github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/sdk/freeport" ) diff --git a/tlsutil/generate.go b/tlsutil/generate.go index 5379fb4de6..b164dca1b6 100644 --- a/tlsutil/generate.go +++ b/tlsutil/generate.go @@ -4,13 +4,13 @@ import ( "bytes" "crypto" "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" + "github.com/hashicorp/consul/agent/connect" "math/big" "net" "strings" @@ -29,23 +29,7 @@ func GenerateSerialNumber() (*big.Int, error) { // GeneratePrivateKey generates a new ecdsa private key func GeneratePrivateKey() (crypto.Signer, string, error) { - pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, "", fmt.Errorf("error generating private key: %s", err) - } - - bs, err := x509.MarshalECPrivateKey(pk) - if err != nil { - return nil, "", fmt.Errorf("error generating private key: %s", err) - } - - var buf bytes.Buffer - err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs}) - if err != nil { - return nil, "", fmt.Errorf("error encoding private key: %s", err) - } - - return pk, buf.String(), nil + return connect.GeneratePrivateKey() } // GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)