From a360c5cca41e7561ff2d3d6ad07835cc8e090e32 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 Mar 2018 14:36:17 -0700 Subject: [PATCH] agent/consul: basic sign endpoint not tested yet --- agent/connect/ca.go | 15 +++ {connect => agent/connect}/testing.go | 56 +++++++++++- {connect => agent/connect}/testing_test.go | 0 agent/consul/connect_ca_endpoint.go | 101 +++++++++++++++++++++ agent/structs/connect_ca.go | 21 ++++- connect/ca.go | 48 ---------- connect/connect.go | 3 - 7 files changed, 188 insertions(+), 56 deletions(-) rename {connect => agent/connect}/testing.go (83%) rename {connect => agent/connect}/testing_test.go (100%) delete mode 100644 connect/ca.go delete mode 100644 connect/connect.go diff --git a/agent/connect/ca.go b/agent/connect/ca.go index 90c484529..a0a65ece6 100644 --- a/agent/connect/ca.go +++ b/agent/connect/ca.go @@ -40,6 +40,21 @@ func ParseSigner(pemValue string) (crypto.Signer, error) { } } +// ParseCSR parses a CSR from a PEM-encoded value. The certificate request +// must be the the first block in the PEM value. +func ParseCSR(pemValue string) (*x509.CertificateRequest, error) { + block, _ := pem.Decode([]byte(pemValue)) + if block == nil { + return nil, fmt.Errorf("no PEM-encoded data found") + } + + if block.Type != "CERTIFICATE REQUEST" { + return nil, fmt.Errorf("first PEM-block should be CERTIFICATE REQUEST type") + } + + return x509.ParseCertificateRequest(block.Bytes) +} + // KeyId returns a x509 KeyId from the given signing key. The key must be // an *ecdsa.PublicKey, but is an interface type to support crypto.Signer. func KeyId(raw interface{}) ([]byte, error) { diff --git a/connect/testing.go b/agent/connect/testing.go similarity index 83% rename from connect/testing.go rename to agent/connect/testing.go index 78008270a..96b13dcf5 100644 --- a/connect/testing.go +++ b/agent/connect/testing.go @@ -11,6 +11,7 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" + "math/big" "net/url" "sync/atomic" "time" @@ -45,7 +46,7 @@ func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot { signer := testPrivateKey(t, &result) // The serial number for the cert - sn, err := SerialNumber() + sn, err := testSerialNumber() if err != nil { t.Fatalf("error generating serial number: %s", err) } @@ -124,7 +125,11 @@ func TestCA(t testing.T, xc *structs.CARoot) *structs.CARoot { // the given CA Root. func TestLeaf(t testing.T, service string, root *structs.CARoot) string { // Parse the CA cert and signing key from the root - caCert, err := ParseCert(root.RootCert) + cert := root.SigningCert + if cert == "" { + cert = root.RootCert + } + caCert, err := ParseCert(cert) if err != nil { t.Fatalf("error parsing CA cert: %s", err) } @@ -133,8 +138,16 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string { t.Fatalf("error parsing signing key: %s", err) } + // Build the SPIFFE ID + spiffeId := &SpiffeIDService{ + Host: fmt.Sprintf("%s.consul", testClusterID), + Namespace: "default", + Datacenter: "dc01", + Service: service, + } + // The serial number for the cert - sn, err := SerialNumber() + sn, err := testSerialNumber() if err != nil { t.Fatalf("error generating serial number: %s", err) } @@ -143,6 +156,7 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string { template := x509.Certificate{ SerialNumber: sn, Subject: pkix.Name{CommonName: service}, + URIs: []*url.URL{spiffeId.URI()}, SignatureAlgorithm: x509.ECDSAWithSHA256, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement, @@ -171,6 +185,30 @@ func TestLeaf(t testing.T, service string, root *structs.CARoot) string { return buf.String() } +// TestCSR returns a CSR to sign the given service. +func TestCSR(t testing.T, id SpiffeID) string { + template := &x509.CertificateRequest{ + URIs: []*url.URL{id.URI()}, + } + + // Create the private key we'll use + signer := testPrivateKey(t, nil) + + // Create the CSR itself + bs, err := x509.CreateCertificateRequest(rand.Reader, template, signer) + if err != nil { + t.Fatalf("error creating CSR: %s", err) + } + + var buf bytes.Buffer + err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bs}) + if err != nil { + t.Fatalf("error encoding CSR: %s", err) + } + + return buf.String() +} + // testKeyID returns a KeyID from the given public key. The "raw" must be // an *ecdsa.PublicKey, but is an interface type to suppot crypto.Signer.Public // values. @@ -221,10 +259,20 @@ func testPrivateKey(t testing.T, ca *structs.CARoot) crypto.Signer { if err != nil { t.Fatalf("error encoding private key: %s", err) } - ca.SigningKey = buf.String() + if ca != nil { + ca.SigningKey = buf.String() + } // Memoize the key testMemoizePK.Store(pk) return pk } + +// testSerialNumber generates a serial number suitable for a certificate. +// For testing, this just sets it to a random number. +// +// This function is taken directly from the Vault implementation. +func testSerialNumber() (*big.Int, error) { + return rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) +} diff --git a/connect/testing_test.go b/agent/connect/testing_test.go similarity index 100% rename from connect/testing_test.go rename to agent/connect/testing_test.go diff --git a/agent/consul/connect_ca_endpoint.go b/agent/consul/connect_ca_endpoint.go index 3f35ad79f..f07fbd90f 100644 --- a/agent/consul/connect_ca_endpoint.go +++ b/agent/consul/connect_ca_endpoint.go @@ -1,6 +1,16 @@ package consul import ( + "bytes" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "time" + + "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/go-memdb" @@ -53,3 +63,94 @@ func (s *ConnectCA) Roots( }, ) } + +// Sign signs a certificate for a service. +// +// NOTE(mitchellh): There is a LOT missing from this. I do next to zero +// validation of the incoming CSR, the way the cert is signed probably +// isn't right, we're not using enough of the CSR fields, etc. +func (s *ConnectCA) Sign( + args *structs.CASignRequest, + reply *structs.IndexedCARoots) error { + // Parse the CSR + csr, err := connect.ParseCSR(args.CSR) + if err != nil { + return err + } + + // Parse the SPIFFE ID + spiffeId, err := connect.ParseSpiffeID(csr.URIs[0]) + if err != nil { + return err + } + serviceId, ok := spiffeId.(*connect.SpiffeIDService) + if !ok { + return fmt.Errorf("SPIFFE ID in CSR must be a service ID") + } + + var root *structs.CARoot + + // Determine the signing certificate. It is the set signing cert + // unless that is empty, in which case it is identically to the public + // cert. + certPem := root.SigningCert + if certPem == "" { + certPem = root.RootCert + } + + // Parse the CA cert and signing key from the root + caCert, err := connect.ParseCert(certPem) + if err != nil { + return fmt.Errorf("error parsing CA cert: %s", err) + } + signer, err := connect.ParseSigner(root.SigningKey) + if err != nil { + return fmt.Errorf("error parsing signing key: %s", err) + } + + // The serial number for the cert. NOTE(mitchellh): in the final + // implementation this should be monotonically increasing based on + // some raft state. + sn, err := rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) + if err != nil { + return fmt.Errorf("error generating serial number: %s", err) + } + + // Create the keyId for the cert from the signing public key. + keyId, err := connect.KeyId(signer.Public()) + if err != nil { + return err + } + + // Cert template for generation + template := x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{CommonName: serviceId.Service}, + URIs: csr.URIs, + SignatureAlgorithm: x509.ECDSAWithSHA256, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + }, + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + NotBefore: time.Now(), + AuthorityKeyId: keyId, + SubjectKeyId: keyId, + } + + // Create the certificate, PEM encode it and return that value. + var buf bytes.Buffer + bs, err := x509.CreateCertificate( + rand.Reader, &template, caCert, signer.Public(), signer) + if err != nil { + return fmt.Errorf("error generating certificate: %s", err) + } + err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) + if err != nil { + return fmt.Errorf("error encoding private key: %s", err) + } + + return nil +} diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 46725dcc7..992fce85a 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -28,7 +28,8 @@ type CARoot struct { RootCert string // SigningCert is the PEM-encoded signing certificate and SigningKey - // is the PEM-encoded private key for the signing certificate. + // is the PEM-encoded private key for the signing certificate. These + // may actually be empty if the CA plugin in use manages these for us. SigningCert string SigningKey string @@ -37,3 +38,21 @@ type CARoot struct { // CARoots is a list of CARoot structures. type CARoots []*CARoot + +// CASignRequest is the request for signing a service certificate. +type CASignRequest struct { + // Datacenter is the target for this request. + Datacenter string + + // CSR is the PEM-encoded CSR. + CSR string + + // WriteRequest is a common struct containing ACL tokens and other + // write-related common elements for requests. + WriteRequest +} + +// RequestDatacenter returns the datacenter for a given request. +func (q *CASignRequest) RequestDatacenter() string { + return q.Datacenter +} diff --git a/connect/ca.go b/connect/ca.go deleted file mode 100644 index e9ada4953..000000000 --- a/connect/ca.go +++ /dev/null @@ -1,48 +0,0 @@ -package connect - -import ( - "crypto" - "crypto/rand" - "crypto/x509" - "encoding/pem" - "fmt" - "math/big" -) - -// ParseCert parses the x509 certificate from a PEM-encoded value. -func ParseCert(pemValue string) (*x509.Certificate, error) { - block, _ := pem.Decode([]byte(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) -} - -// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key -// is expected to be the first block in the PEM value. -func ParseSigner(pemValue string) (crypto.Signer, error) { - block, _ := pem.Decode([]byte(pemValue)) - if block == nil { - return nil, fmt.Errorf("no PEM-encoded data found") - } - - switch block.Type { - case "EC PRIVATE KEY": - return x509.ParseECPrivateKey(block.Bytes) - - default: - return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type) - } -} - -// SerialNumber generates a serial number suitable for a certificate. -// -// This function is taken directly from the Vault implementation. -func SerialNumber() (*big.Int, error) { - return rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil)) -} diff --git a/connect/connect.go b/connect/connect.go deleted file mode 100644 index b2ad85f71..000000000 --- a/connect/connect.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package connect contains utilities and helpers for working with the -// Connect feature of Consul. -package connect