Skip to content

Commit

Permalink
Add get-consul-client-ca command (#211)
Browse files Browse the repository at this point in the history
* Add get-consul-client-ca command

When auto-encrypt is enabled, we need to retrieve
Consul client CA from the Consul servers.

This command calls the '/agent/connect/ca/roots' endpoint,
finds the currently active root CA, and writes it
to the provided output file location.
  • Loading branch information
ishustava authored Mar 30, 2020
1 parent 33afd63 commit 82e34ba
Show file tree
Hide file tree
Showing 9 changed files with 922 additions and 194 deletions.
5 changes: 5 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

cmdACLInit "github.com/hashicorp/consul-k8s/subcommand/acl-init"
cmdDeleteCompletedJob "github.com/hashicorp/consul-k8s/subcommand/delete-completed-job"
cmdGetConsulClientCA "github.com/hashicorp/consul-k8s/subcommand/get-consul-client-ca"
cmdInjectConnect "github.com/hashicorp/consul-k8s/subcommand/inject-connect"
cmdLifecycleSidecar "github.com/hashicorp/consul-k8s/subcommand/lifecycle-sidecar"
cmdServerACLInit "github.com/hashicorp/consul-k8s/subcommand/server-acl-init"
Expand Down Expand Up @@ -45,6 +46,10 @@ func init() {
return &cmdDeleteCompletedJob.Command{UI: ui}, nil
},

"get-consul-client-ca": func() (cli.Command, error) {
return &cmdGetConsulClientCA.Command{UI: ui}, nil
},

"version": func() (cli.Command, error) {
return &cmdVersion.Command{UI: ui, Version: version.GetHumanVersion()}, nil
},
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/hashicorp/consul v1.7.1
github.com/hashicorp/consul/api v1.4.0
github.com/hashicorp/consul/sdk v0.4.0
github.com/hashicorp/go-discover v0.0.0-20191202160150-7ec2cfbda7a2
github.com/hashicorp/go-hclog v0.12.0
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/golang-lru v0.5.3 // indirect
Expand Down
175 changes: 8 additions & 167 deletions helper/cert/source_gen.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
package cert

import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"strings"
"sync"
"time"
)
Expand All @@ -40,7 +29,7 @@ type GenSource struct {
ExpiryWithin time.Duration

mu sync.Mutex
caCert []byte
caCert string
caCertTemplate *x509.Certificate
caSigner crypto.Signer
}
Expand All @@ -57,15 +46,14 @@ func (s *GenSource) Certificate(ctx context.Context, last *Bundle) (Bundle, erro
return result, err
}
}

// Set the CA cert
result.CACert = s.caCert
result.CACert = []byte(s.caCert)

// If we have a prior cert, we wait for getting near to the expiry
// (within 30 minutes arbitrarily chosen).
if last != nil {
// We have a prior certificate, let's parse it to get the expiry
cert, err := parseCert(last.Cert)
cert, err := ParseCert(last.Cert)
if err != nil {
return result, err
}
Expand All @@ -88,7 +76,7 @@ func (s *GenSource) Certificate(ctx context.Context, last *Bundle) (Bundle, erro
}

// Generate cert, set it on the result, and return
cert, key, err := s.generateCert()
cert, key, err := GenerateCert(s.Name+" Service", s.expiry(), s.caCertTemplate, s.caSigner, s.Hosts)
if err == nil {
result.Cert = []byte(cert)
result.Key = []byte(key)
Expand All @@ -114,162 +102,15 @@ func (s *GenSource) expiryWithin() time.Duration {
return time.Duration(float64(s.expiry()) * 0.10)
}

func (s *GenSource) generateCert() (string, string, error) {
// Create the private key we'll use for this leaf cert.
signer, keyPEM, err := s.privateKey()
if err != nil {
return "", "", err
}

// The serial number for the cert
sn, err := serialNumber()
if err != nil {
return "", "", err
}

// Create the leaf cert
template := x509.Certificate{
SerialNumber: sn,
Subject: pkix.Name{CommonName: s.Name + " Service"},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
NotAfter: time.Now().Add(s.expiry()),
NotBefore: time.Now().Add(-1 * time.Minute),
}
for _, h := range s.Hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}

bs, err := x509.CreateCertificate(
rand.Reader, &template, s.caCertTemplate, signer.Public(), s.caSigner)
if err != nil {
return "", "", err
}
var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
return "", "", err
}

return buf.String(), keyPEM, nil
}

func (s *GenSource) generateCA() error {
// Create the private key we'll use for this CA cert.
signer, _, err := s.privateKey()
// generate the CA
signer, _, caCertPem, caCertTemplate, err := GenerateCA(s.Name + " CA")
if err != nil {
return err
}
s.caSigner = signer

// The serial number for the cert
sn, err := serialNumber()
if err != nil {
return err
}

signerKeyId, err := keyId(signer.Public())
if err != nil {
return err
}

// Create the CA cert
template := x509.Certificate{
SerialNumber: sn,
Subject: pkix.Name{CommonName: s.Name + " CA"},
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IsCA: true,
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
NotBefore: time.Now().Add(-1 * time.Minute),
AuthorityKeyId: signerKeyId,
SubjectKeyId: signerKeyId,
}

bs, err := x509.CreateCertificate(
rand.Reader, &template, &template, signer.Public(), signer)
if err != nil {
return err
}

var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
if err != nil {
return err
}

s.caCert = buf.Bytes()
s.caCertTemplate = &template
s.caCert = caCertPem
s.caCertTemplate = caCertTemplate

return nil
}

// privateKey returns a new ECDSA-based private key. Both a crypto.Signer
// and the key in PEM format are returned.
func (s *GenSource) privateKey() (crypto.Signer, string, error) {
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, "", err
}

bs, err := x509.MarshalECPrivateKey(pk)
if err != nil {
return nil, "", err
}

var buf bytes.Buffer
err = pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: bs})
if err != nil {
return nil, "", err
}

return pk, buf.String(), nil
}

// serialNumber generates a new random serial number.
func serialNumber() (*big.Int, error) {
return rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
}

// keyId returns a x509 KeyId from the given signing key. The key must be
// an *ecdsa.PublicKey currently, but may support more types in the future.
func keyId(raw interface{}) ([]byte, error) {
switch raw.(type) {
case *ecdsa.PublicKey:
default:
return nil, fmt.Errorf("invalid key type: %T", raw)
}

// This is not standard; RFC allows any unique identifier as long as they
// match in subject/authority chains but suggests specific hashing of DER
// bytes of public key including DER tags.
bs, err := x509.MarshalPKIXPublicKey(raw)
if err != nil {
return nil, err
}

// String formatted
kID := sha256.Sum256(bs)
return []byte(strings.Replace(fmt.Sprintf("% x", kID), " ", ":", -1)), nil
}

// parseCert parses the x509 certificate from a PEM-encoded value.
func parseCert(pemValue []byte) (*x509.Certificate, error) {
// The _ result below is not an error but the remaining PEM bytes.
block, _ := pem.Decode(pemValue)
if block == nil {
return nil, fmt.Errorf("no PEM-encoded data found")
}

if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type")
}

return x509.ParseCertificate(block.Bytes)
}
2 changes: 1 addition & 1 deletion helper/cert/source_gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func testBundleVerify(t *testing.T, bundle *Bundle) {
cmd := exec.Command(
"openssl", "verify", "-verbose", "-CAfile", "ca.pem", "leaf.pem")
cmd.Dir = td
output, err := cmd.Output()
output, err := cmd.CombinedOutput()
t.Log(string(output))
require.NoError(err)
}
Loading

0 comments on commit 82e34ba

Please sign in to comment.