|
|
|
@ -1,22 +1,13 @@
|
|
|
|
|
package consul |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"bytes" |
|
|
|
|
"crypto/rand" |
|
|
|
|
"crypto/x509" |
|
|
|
|
"crypto/x509/pkix" |
|
|
|
|
"encoding/pem" |
|
|
|
|
"fmt" |
|
|
|
|
"math/big" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"github.com/hashicorp/consul/acl" |
|
|
|
|
"github.com/hashicorp/consul/agent/connect" |
|
|
|
|
"github.com/hashicorp/consul/agent/consul/state" |
|
|
|
|
"github.com/hashicorp/consul/agent/structs" |
|
|
|
|
"github.com/hashicorp/go-memdb" |
|
|
|
|
"github.com/hashicorp/go-uuid" |
|
|
|
|
"github.com/mitchellh/go-testing-interface" |
|
|
|
|
"github.com/mitchellh/mapstructure" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
// ConnectCA manages the Connect CA.
|
|
|
|
@ -25,81 +16,54 @@ type ConnectCA struct {
|
|
|
|
|
srv *Server |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ConfigurationSet updates the configuration for the CA.
|
|
|
|
|
//
|
|
|
|
|
// NOTE(mitchellh): This whole implementation is temporary until the real
|
|
|
|
|
// CA plugin work comes in. For now, this is only used to configure a single
|
|
|
|
|
// static CA root.
|
|
|
|
|
func (s *ConnectCA) ConfigurationSet( |
|
|
|
|
args *structs.CAConfiguration, |
|
|
|
|
reply *interface{}) error { |
|
|
|
|
// NOTE(mitchellh): This is the temporary hardcoding of a static CA
|
|
|
|
|
// provider. This will allow us to test agent implementations and so on
|
|
|
|
|
// with an incomplete CA for now.
|
|
|
|
|
if args.Provider != "static" { |
|
|
|
|
return fmt.Errorf("The CA provider can only be 'static' for now") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Config is the configuration allowed for our static provider
|
|
|
|
|
var config struct { |
|
|
|
|
Name string |
|
|
|
|
CertPEM string |
|
|
|
|
PrivateKeyPEM string |
|
|
|
|
Generate bool |
|
|
|
|
} |
|
|
|
|
if err := mapstructure.Decode(args.Config, &config); err != nil { |
|
|
|
|
return fmt.Errorf("error decoding config: %s", err) |
|
|
|
|
// ConfigurationGet returns the configuration for the CA.
|
|
|
|
|
func (s *ConnectCA) ConfigurationGet( |
|
|
|
|
args *structs.DCSpecificRequest, |
|
|
|
|
reply *structs.CAConfiguration) error { |
|
|
|
|
if done, err := s.srv.forward("ConnectCA.ConfigurationGet", args, args, reply); done { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Basic validation so demos aren't super jank
|
|
|
|
|
if config.Name == "" { |
|
|
|
|
return fmt.Errorf("Name must be set") |
|
|
|
|
// This action requires operator read access.
|
|
|
|
|
rule, err := s.srv.resolveToken(args.Token) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
if config.CertPEM == "" || config.PrivateKeyPEM == "" { |
|
|
|
|
if !config.Generate { |
|
|
|
|
return fmt.Errorf( |
|
|
|
|
"CertPEM and PrivateKeyPEM must be set, or Generate must be true") |
|
|
|
|
} |
|
|
|
|
if rule != nil && !rule.OperatorRead() { |
|
|
|
|
return acl.ErrPermissionDenied |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Convenience to auto-generate the cert
|
|
|
|
|
if config.Generate { |
|
|
|
|
ca := connect.TestCA(&testing.RuntimeT{}, nil) |
|
|
|
|
config.CertPEM = ca.RootCert |
|
|
|
|
config.PrivateKeyPEM = ca.SigningKey |
|
|
|
|
state := s.srv.fsm.State() |
|
|
|
|
_, config, err := state.CAConfig() |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
*reply = *config |
|
|
|
|
|
|
|
|
|
// TODO(mitchellh): verify that the private key is valid for the cert
|
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Generate an ID for this
|
|
|
|
|
id, err := uuid.GenerateUUID() |
|
|
|
|
if err != nil { |
|
|
|
|
// ConfigurationSet updates the configuration for the CA.
|
|
|
|
|
func (s *ConnectCA) ConfigurationSet( |
|
|
|
|
args *structs.CARequest, |
|
|
|
|
reply *interface{}) error { |
|
|
|
|
if done, err := s.srv.forward("ConnectCA.ConfigurationSet", args, args, reply); done { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Get the highest index
|
|
|
|
|
state := s.srv.fsm.State() |
|
|
|
|
idx, _, err := state.CARoots(nil) |
|
|
|
|
// This action requires operator read access.
|
|
|
|
|
rule, err := s.srv.resolveToken(args.Token) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
if rule != nil && !rule.OperatorWrite() { |
|
|
|
|
return acl.ErrPermissionDenied |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Commit
|
|
|
|
|
resp, err := s.srv.raftApply(structs.ConnectCARequestType, &structs.CARequest{ |
|
|
|
|
Op: structs.CAOpSet, |
|
|
|
|
Index: idx, |
|
|
|
|
Roots: []*structs.CARoot{ |
|
|
|
|
&structs.CARoot{ |
|
|
|
|
ID: id, |
|
|
|
|
Name: config.Name, |
|
|
|
|
RootCert: config.CertPEM, |
|
|
|
|
SigningKey: config.PrivateKeyPEM, |
|
|
|
|
Active: true, |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
args.Op = structs.CAOpSetConfig |
|
|
|
|
resp, err := s.srv.raftApply(structs.ConnectCARequestType, args) |
|
|
|
|
if err != nil { |
|
|
|
|
s.srv.logger.Printf("[ERR] consul.test: Apply failed %v", err) |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
if respErr, ok := resp.(error); ok { |
|
|
|
@ -157,13 +121,13 @@ 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.IssuedCert) error { |
|
|
|
|
if done, err := s.srv.forward("ConnectCA.Sign", args, args, reply); done { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Parse the CSR
|
|
|
|
|
csr, err := connect.ParseCSR(args.CSR) |
|
|
|
|
if err != nil { |
|
|
|
@ -180,93 +144,15 @@ func (s *ConnectCA) Sign(
|
|
|
|
|
return fmt.Errorf("SPIFFE ID in CSR must be a service ID") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Get the currently active root
|
|
|
|
|
state := s.srv.fsm.State() |
|
|
|
|
_, root, err := state.CARootActive(nil) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
if root == nil { |
|
|
|
|
return fmt.Errorf("no active CA found") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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) |
|
|
|
|
} |
|
|
|
|
// todo(kyhavlov): more validation on the CSR before signing
|
|
|
|
|
|
|
|
|
|
// 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()) |
|
|
|
|
cert, err := s.srv.signConnectCert(serviceId, csr) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Cert template for generation
|
|
|
|
|
template := x509.Certificate{ |
|
|
|
|
SerialNumber: sn, |
|
|
|
|
Subject: pkix.Name{CommonName: serviceId.Service}, |
|
|
|
|
URIs: csr.URIs, |
|
|
|
|
Signature: csr.Signature, |
|
|
|
|
SignatureAlgorithm: csr.SignatureAlgorithm, |
|
|
|
|
PublicKeyAlgorithm: csr.PublicKeyAlgorithm, |
|
|
|
|
PublicKey: csr.PublicKey, |
|
|
|
|
BasicConstraintsValid: true, |
|
|
|
|
KeyUsage: x509.KeyUsageDataEncipherment | |
|
|
|
|
x509.KeyUsageKeyAgreement | |
|
|
|
|
x509.KeyUsageDigitalSignature | |
|
|
|
|
x509.KeyUsageKeyEncipherment, |
|
|
|
|
ExtKeyUsage: []x509.ExtKeyUsage{ |
|
|
|
|
x509.ExtKeyUsageClientAuth, |
|
|
|
|
x509.ExtKeyUsageServerAuth, |
|
|
|
|
}, |
|
|
|
|
NotAfter: time.Now().Add(3 * 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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Set the response
|
|
|
|
|
*reply = structs.IssuedCert{ |
|
|
|
|
SerialNumber: connect.HexString(template.SerialNumber.Bytes()), |
|
|
|
|
CertPEM: buf.String(), |
|
|
|
|
Service: serviceId.Service, |
|
|
|
|
ServiceURI: template.URIs[0].String(), |
|
|
|
|
ValidAfter: template.NotBefore, |
|
|
|
|
ValidBefore: template.NotAfter, |
|
|
|
|
} |
|
|
|
|
*reply = *cert |
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|