agent/consul: basic sign endpoint not tested yet

This commit is contained in:
Mitchell Hashimoto 2018-03-19 14:36:17 -07:00
parent 6550ff9492
commit a360c5cca4
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
7 changed files with 188 additions and 56 deletions

View File

@ -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) {

View File

@ -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)
}
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))
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -1,3 +0,0 @@
// Package connect contains utilities and helpers for working with the
// Connect feature of Consul.
package connect