2016-06-07 14:28:28 -07:00
|
|
|
package ca
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto"
|
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/pem"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
cfcsr "github.com/cloudflare/cfssl/csr"
|
|
|
|
"github.com/cloudflare/cfssl/helpers"
|
|
|
|
"github.com/cloudflare/cfssl/initca"
|
|
|
|
cflog "github.com/cloudflare/cfssl/log"
|
|
|
|
cfsigner "github.com/cloudflare/cfssl/signer"
|
|
|
|
"github.com/cloudflare/cfssl/signer/local"
|
|
|
|
"github.com/docker/distribution/digest"
|
|
|
|
"github.com/docker/go-events"
|
|
|
|
"github.com/docker/swarmkit/api"
|
|
|
|
"github.com/docker/swarmkit/identity"
|
|
|
|
"github.com/docker/swarmkit/ioutils"
|
2016-08-22 22:30:01 -07:00
|
|
|
"github.com/docker/swarmkit/remotes"
|
2016-09-26 23:48:16 -07:00
|
|
|
"github.com/pkg/errors"
|
2016-06-07 14:28:28 -07:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"google.golang.org/grpc/credentials"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Security Strength Equivalence
|
|
|
|
//-----------------------------------
|
2016-08-17 22:43:33 -07:00
|
|
|
//| ECC | DH/DSA/RSA |
|
|
|
|
//| 256 | 3072 |
|
|
|
|
//| 384 | 7680 |
|
2016-06-07 14:28:28 -07:00
|
|
|
//-----------------------------------
|
|
|
|
|
|
|
|
// RootKeySize is the default size of the root CA key
|
2016-08-17 22:43:33 -07:00
|
|
|
// It would be ideal for the root key to use P-384, but in P-384 is not optimized in go yet :(
|
|
|
|
RootKeySize = 256
|
2016-06-07 14:28:28 -07:00
|
|
|
// RootKeyAlgo defines the default algorithm for the root CA Key
|
|
|
|
RootKeyAlgo = "ecdsa"
|
|
|
|
// PassphraseENVVar defines the environment variable to look for the
|
|
|
|
// root CA private key material encryption key
|
|
|
|
PassphraseENVVar = "SWARM_ROOT_CA_PASSPHRASE"
|
|
|
|
// PassphraseENVVarPrev defines the alternate environment variable to look for the
|
|
|
|
// root CA private key material encryption key. It can be used for seamless
|
|
|
|
// KEK rotations.
|
|
|
|
PassphraseENVVarPrev = "SWARM_ROOT_CA_PASSPHRASE_PREV"
|
|
|
|
// RootCAExpiration represents the expiration for the root CA in seconds (20 years)
|
|
|
|
RootCAExpiration = "630720000s"
|
|
|
|
// DefaultNodeCertExpiration represents the default expiration for node certificates (3 months)
|
|
|
|
DefaultNodeCertExpiration = 2160 * time.Hour
|
2016-08-09 11:49:39 -07:00
|
|
|
// CertBackdate represents the amount of time each certificate is backdated to try to avoid
|
|
|
|
// clock drift issues.
|
|
|
|
CertBackdate = 1 * time.Hour
|
2016-06-07 14:28:28 -07:00
|
|
|
// CertLowerRotationRange represents the minimum fraction of time that we will wait when randomly
|
|
|
|
// choosing our next certificate rotation
|
|
|
|
CertLowerRotationRange = 0.5
|
|
|
|
// CertUpperRotationRange represents the maximum fraction of time that we will wait when randomly
|
|
|
|
// choosing our next certificate rotation
|
|
|
|
CertUpperRotationRange = 0.8
|
2016-08-09 11:49:39 -07:00
|
|
|
// MinNodeCertExpiration represents the minimum expiration for node certificates
|
|
|
|
MinNodeCertExpiration = 1 * time.Hour
|
2016-06-07 14:28:28 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
// ErrNoLocalRootCA is an error type used to indicate that the local root CA
|
|
|
|
// certificate file does not exist.
|
|
|
|
var ErrNoLocalRootCA = errors.New("local root CA certificate does not exist")
|
|
|
|
|
|
|
|
// ErrNoValidSigner is an error type used to indicate that our RootCA doesn't have the ability to
|
|
|
|
// sign certificates.
|
|
|
|
var ErrNoValidSigner = errors.New("no valid signer found")
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
cflog.Level = 5
|
|
|
|
}
|
|
|
|
|
|
|
|
// CertPaths is a helper struct that keeps track of the paths of a
|
|
|
|
// Cert and corresponding Key
|
|
|
|
type CertPaths struct {
|
|
|
|
Cert, Key string
|
|
|
|
}
|
|
|
|
|
|
|
|
// RootCA is the representation of everything we need to sign certificates
|
|
|
|
type RootCA struct {
|
|
|
|
// Key will only be used by the original manager to put the private
|
|
|
|
// key-material in raft, no signing operations depend on it.
|
|
|
|
Key []byte
|
|
|
|
// Cert includes the PEM encoded Certificate for the Root CA
|
|
|
|
Cert []byte
|
|
|
|
Pool *x509.CertPool
|
|
|
|
// Digest of the serialized bytes of the certificate
|
|
|
|
Digest digest.Digest
|
|
|
|
// This signer will be nil if the node doesn't have the appropriate key material
|
|
|
|
Signer cfsigner.Signer
|
|
|
|
}
|
|
|
|
|
|
|
|
// CanSign ensures that the signer has all three necessary elements needed to operate
|
|
|
|
func (rca *RootCA) CanSign() bool {
|
|
|
|
if rca.Cert == nil || rca.Pool == nil || rca.Signer == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// IssueAndSaveNewCertificates generates a new key-pair, signs it with the local root-ca, and returns a
|
|
|
|
// tls certificate
|
|
|
|
func (rca *RootCA) IssueAndSaveNewCertificates(paths CertPaths, cn, ou, org string) (*tls.Certificate, error) {
|
|
|
|
csr, key, err := GenerateAndWriteNewKey(paths)
|
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Wrap(err, "error when generating new node certs")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if !rca.CanSign() {
|
|
|
|
return nil, ErrNoValidSigner
|
|
|
|
}
|
|
|
|
|
|
|
|
// Obtain a signed Certificate
|
2016-06-15 22:41:30 -07:00
|
|
|
certChain, err := rca.ParseValidateAndSignCSR(csr, cn, ou, org)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Wrap(err, "failed to sign node certificate")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure directory exists
|
|
|
|
err = os.MkdirAll(filepath.Dir(paths.Cert), 0755)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the chain to disk
|
2016-06-15 22:41:30 -07:00
|
|
|
if err := ioutils.AtomicWriteFile(paths.Cert, certChain, 0644); err != nil {
|
2016-06-07 14:28:28 -07:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a valid TLSKeyPair out of the PEM encoded private key and certificate
|
2016-06-15 22:41:30 -07:00
|
|
|
tlsKeyPair, err := tls.X509KeyPair(certChain, key)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &tlsKeyPair, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RequestAndSaveNewCertificates gets new certificates issued, either by signing them locally if a signer is
|
|
|
|
// available, or by requesting them from the remote server at remoteAddr.
|
2016-09-13 09:28:01 -07:00
|
|
|
func (rca *RootCA) RequestAndSaveNewCertificates(ctx context.Context, paths CertPaths, token string, remotes remotes.Remotes, transport credentials.TransportCredentials, nodeInfo chan<- api.IssueNodeCertificateResponse) (*tls.Certificate, error) {
|
2016-06-07 14:28:28 -07:00
|
|
|
// Create a new key/pair and CSR for the new manager
|
|
|
|
// Write the new CSR and the new key to a temporary location so we can survive crashes on rotation
|
|
|
|
tempPaths := genTempPaths(paths)
|
|
|
|
csr, key, err := GenerateAndWriteNewKey(tempPaths)
|
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Wrap(err, "error when generating new node certs")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get the remote manager to issue a CA signed certificate for this node
|
2016-06-30 13:34:48 -07:00
|
|
|
// Retry up to 5 times in case the manager we first try to contact isn't
|
|
|
|
// responding properly (for example, it may have just been demoted).
|
|
|
|
var signedCert []byte
|
|
|
|
for i := 0; i != 5; i++ {
|
2016-08-22 22:30:01 -07:00
|
|
|
signedCert, err = GetRemoteSignedCertificate(ctx, csr, token, rca.Pool, remotes, transport, nodeInfo)
|
2016-06-30 13:34:48 -07:00
|
|
|
if err == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Доверяй, но проверяй.
|
|
|
|
// Before we overwrite our local certificate, let's make sure the server gave us one that is valid
|
|
|
|
// Create an X509Cert so we can .Verify()
|
|
|
|
certBlock, _ := pem.Decode(signedCert)
|
|
|
|
if certBlock == nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("failed to parse certificate PEM")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
X509Cert, err := x509.ParseCertificate(certBlock.Bytes)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// Include our current root pool
|
|
|
|
opts := x509.VerifyOptions{
|
|
|
|
Roots: rca.Pool,
|
|
|
|
}
|
|
|
|
// Check to see if this certificate was signed by our CA, and isn't expired
|
|
|
|
if _, err := X509Cert.Verify(opts); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
// Create a valid TLSKeyPair out of the PEM encoded private key and certificate
|
|
|
|
tlsKeyPair, err := tls.X509KeyPair(signedCert, key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-06-07 14:28:28 -07:00
|
|
|
// Ensure directory exists
|
|
|
|
err = os.MkdirAll(filepath.Dir(paths.Cert), 0755)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the chain to disk
|
|
|
|
if err := ioutils.AtomicWriteFile(paths.Cert, signedCert, 0644); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Move the new key to the final location
|
|
|
|
if err := os.Rename(tempPaths.Key, paths.Key); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &tlsKeyPair, nil
|
|
|
|
}
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
// PrepareCSR creates a CFSSL Sign Request based on the given raw CSR and
|
|
|
|
// overrides the Subject and Hosts with the given extra args.
|
|
|
|
func PrepareCSR(csrBytes []byte, cn, ou, org string) cfsigner.SignRequest {
|
|
|
|
// All managers get added the subject-alt-name of CA, so they can be
|
|
|
|
// used for cert issuance.
|
2016-08-11 16:41:44 -07:00
|
|
|
hosts := []string{ou, cn}
|
2016-06-07 14:28:28 -07:00
|
|
|
if ou == ManagerRole {
|
|
|
|
hosts = append(hosts, CARole)
|
|
|
|
}
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
return cfsigner.SignRequest{
|
2016-06-07 14:28:28 -07:00
|
|
|
Request: string(csrBytes),
|
|
|
|
// OU is used for Authentication of the node type. The CN has the random
|
|
|
|
// node ID.
|
|
|
|
Subject: &cfsigner.Subject{CN: cn, Names: []cfcsr.Name{{OU: ou, O: org}}},
|
|
|
|
// Adding ou as DNS alt name, so clients can connect to ManagerRole and CARole
|
|
|
|
Hosts: hosts,
|
2016-06-30 13:34:48 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseValidateAndSignCSR returns a signed certificate from a particular rootCA and a CSR.
|
|
|
|
func (rca *RootCA) ParseValidateAndSignCSR(csrBytes []byte, cn, ou, org string) ([]byte, error) {
|
|
|
|
if !rca.CanSign() {
|
|
|
|
return nil, ErrNoValidSigner
|
|
|
|
}
|
|
|
|
|
|
|
|
signRequest := PrepareCSR(csrBytes, cn, ou, org)
|
|
|
|
|
|
|
|
cert, err := rca.Signer.Sign(signRequest)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Wrap(err, "failed to sign node certificate")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
return rca.AppendFirstRootPEM(cert)
|
|
|
|
}
|
|
|
|
|
|
|
|
// AppendFirstRootPEM appends the first certificate from this RootCA's cert
|
|
|
|
// bundle to the given cert bundle (which should already be encoded as a series
|
|
|
|
// of PEM-encoded certificate blocks).
|
|
|
|
func (rca *RootCA) AppendFirstRootPEM(cert []byte) ([]byte, error) {
|
2016-06-15 22:41:30 -07:00
|
|
|
// Append the first root CA Cert to the certificate, to create a valid chain
|
|
|
|
// Get the first Root CA Cert on the bundle
|
|
|
|
firstRootCA, _, err := helpers.ParseOneCertificateFromPEM(rca.Cert)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(firstRootCA) < 1 {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("no valid Root CA certificates found")
|
2016-06-15 22:41:30 -07:00
|
|
|
}
|
|
|
|
// Convert the first root CA back to PEM
|
|
|
|
firstRootCAPEM := helpers.EncodeCertificatePEM(firstRootCA[0])
|
|
|
|
if firstRootCAPEM == nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("error while encoding the Root CA certificate")
|
2016-06-15 22:41:30 -07:00
|
|
|
}
|
|
|
|
// Append this Root CA to the certificate to make [Cert PEM]\n[Root PEM][EOF]
|
|
|
|
certChain := append(cert, firstRootCAPEM...)
|
|
|
|
|
|
|
|
return certChain, nil
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
2016-06-15 22:41:30 -07:00
|
|
|
// NewRootCA creates a new RootCA object from unparsed PEM cert bundle and key byte
|
2016-06-07 14:28:28 -07:00
|
|
|
// slices. key may be nil, and in this case NewRootCA will return a RootCA
|
|
|
|
// without a signer.
|
2016-06-15 22:41:30 -07:00
|
|
|
func NewRootCA(certBytes, keyBytes []byte, certExpiry time.Duration) (RootCA, error) {
|
|
|
|
// Parse all the certificates in the cert bundle
|
|
|
|
parsedCerts, err := helpers.ParseCertificatesPEM(certBytes)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
2016-06-15 22:41:30 -07:00
|
|
|
// Check to see if we have at least one valid cert
|
|
|
|
if len(parsedCerts) < 1 {
|
2016-09-26 23:48:16 -07:00
|
|
|
return RootCA{}, errors.New("no valid Root CA certificates found")
|
2016-06-15 22:41:30 -07:00
|
|
|
}
|
2016-06-07 14:28:28 -07:00
|
|
|
|
2016-06-15 22:41:30 -07:00
|
|
|
// Create a Pool with all of the certificates found
|
2016-06-07 14:28:28 -07:00
|
|
|
pool := x509.NewCertPool()
|
2016-06-15 22:41:30 -07:00
|
|
|
for _, cert := range parsedCerts {
|
|
|
|
// Check to see if all of the certificates are valid, self-signed root CA certs
|
|
|
|
if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return RootCA{}, errors.Wrap(err, "error while validating Root CA Certificate")
|
2016-06-15 22:41:30 -07:00
|
|
|
}
|
|
|
|
pool.AddCert(cert)
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
2016-06-15 22:41:30 -07:00
|
|
|
// Calculate the digest for our Root CA bundle
|
|
|
|
digest := digest.FromBytes(certBytes)
|
|
|
|
|
|
|
|
if len(keyBytes) == 0 {
|
2016-06-07 14:28:28 -07:00
|
|
|
// This RootCA does not have a valid signer.
|
2016-06-15 22:41:30 -07:00
|
|
|
return RootCA{Cert: certBytes, Digest: digest, Pool: pool}, nil
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
passphraseStr string
|
|
|
|
passphrase, passphrasePrev []byte
|
|
|
|
priv crypto.Signer
|
|
|
|
)
|
|
|
|
|
|
|
|
// Attempt two distinct passphrases, so we can do a hitless passphrase rotation
|
|
|
|
if passphraseStr = os.Getenv(PassphraseENVVar); passphraseStr != "" {
|
|
|
|
passphrase = []byte(passphraseStr)
|
|
|
|
}
|
|
|
|
|
|
|
|
if p := os.Getenv(PassphraseENVVarPrev); p != "" {
|
|
|
|
passphrasePrev = []byte(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attempt to decrypt the current private-key with the passphrases provided
|
2016-06-15 22:41:30 -07:00
|
|
|
priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrase)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
2016-06-15 22:41:30 -07:00
|
|
|
priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrasePrev)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return RootCA{}, errors.Wrap(err, "malformed private key")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-15 22:41:30 -07:00
|
|
|
// We will always use the first certificate inside of the root bundle as the active one
|
|
|
|
if err := ensureCertKeyMatch(parsedCerts[0], priv.Public()); err != nil {
|
2016-06-07 14:28:28 -07:00
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
2016-06-15 22:41:30 -07:00
|
|
|
signer, err := local.NewSigner(priv, parsedCerts[0], cfsigner.DefaultSigAlgo(priv), SigningPolicy(certExpiry))
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the key was loaded from disk unencrypted, but there is a passphrase set,
|
|
|
|
// ensure it is encrypted, so it doesn't hit raft in plain-text
|
2016-06-15 22:41:30 -07:00
|
|
|
keyBlock, _ := pem.Decode(keyBytes)
|
2016-06-07 14:28:28 -07:00
|
|
|
if keyBlock == nil {
|
|
|
|
// This RootCA does not have a valid signer.
|
2016-06-15 22:41:30 -07:00
|
|
|
return RootCA{Cert: certBytes, Digest: digest, Pool: pool}, nil
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
if passphraseStr != "" && !x509.IsEncryptedPEMBlock(keyBlock) {
|
2016-06-15 22:41:30 -07:00
|
|
|
keyBytes, err = EncryptECPrivateKey(keyBytes, passphraseStr)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-15 22:41:30 -07:00
|
|
|
return RootCA{Signer: signer, Key: keyBytes, Digest: digest, Cert: certBytes, Pool: pool}, nil
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
func ensureCertKeyMatch(cert *x509.Certificate, key crypto.PublicKey) error {
|
|
|
|
switch certPub := cert.PublicKey.(type) {
|
|
|
|
// TODO: Handle RSA keys.
|
|
|
|
case *ecdsa.PublicKey:
|
|
|
|
ecKey, ok := key.(*ecdsa.PublicKey)
|
|
|
|
if ok && certPub.X.Cmp(ecKey.X) == 0 && certPub.Y.Cmp(ecKey.Y) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
default:
|
2016-09-26 23:48:16 -07:00
|
|
|
return errors.New("unknown or unsupported certificate public key algorithm")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
2016-09-26 23:48:16 -07:00
|
|
|
return errors.New("certificate key mismatch")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetLocalRootCA validates if the contents of the file are a valid self-signed
|
|
|
|
// CA certificate, and returns the PEM-encoded Certificate if so
|
|
|
|
func GetLocalRootCA(baseDir string) (RootCA, error) {
|
|
|
|
paths := NewConfigPaths(baseDir)
|
|
|
|
|
|
|
|
// Check if we have a Certificate file
|
|
|
|
cert, err := ioutil.ReadFile(paths.RootCA.Cert)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
err = ErrNoLocalRootCA
|
|
|
|
}
|
|
|
|
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := ioutil.ReadFile(paths.RootCA.Key)
|
|
|
|
if err != nil {
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
// There may not be a local key. It's okay to pass in a nil
|
|
|
|
// key. We'll get a root CA without a signer.
|
|
|
|
key = nil
|
|
|
|
}
|
|
|
|
|
2016-09-13 09:28:01 -07:00
|
|
|
return NewRootCA(cert, key, DefaultNodeCertExpiration)
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetRemoteCA returns the remote endpoint's CA certificate
|
2016-08-22 22:30:01 -07:00
|
|
|
func GetRemoteCA(ctx context.Context, d digest.Digest, r remotes.Remotes) (RootCA, error) {
|
2016-06-07 14:28:28 -07:00
|
|
|
// This TLS Config is intentionally using InsecureSkipVerify. Either we're
|
|
|
|
// doing TOFU, in which case we don't validate the remote CA, or we're using
|
|
|
|
// a user supplied hash to check the integrity of the CA certificate.
|
|
|
|
insecureCreds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})
|
|
|
|
opts := []grpc.DialOption{
|
|
|
|
grpc.WithTransportCredentials(insecureCreds),
|
2016-08-22 22:30:01 -07:00
|
|
|
grpc.WithTimeout(5 * time.Second),
|
|
|
|
grpc.WithBackoffMaxDelay(5 * time.Second),
|
|
|
|
}
|
2016-06-07 14:28:28 -07:00
|
|
|
|
2016-08-22 22:30:01 -07:00
|
|
|
peer, err := r.Select()
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
2016-08-22 22:30:01 -07:00
|
|
|
conn, err := grpc.Dial(peer.Addr, opts...)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
client := api.NewCAClient(conn)
|
2016-08-22 22:30:01 -07:00
|
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
r.Observe(peer, -remotes.DefaultObservationWeight)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
r.Observe(peer, remotes.DefaultObservationWeight)
|
|
|
|
}()
|
2016-06-07 14:28:28 -07:00
|
|
|
response, err := client.GetRootCACertificate(ctx, &api.GetRootCACertificateRequest{})
|
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if d != "" {
|
|
|
|
verifier, err := digest.NewDigestVerifier(d)
|
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return RootCA{}, errors.Wrap(err, "unexpected error getting digest verifier")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
io.Copy(verifier, bytes.NewReader(response.Certificate))
|
|
|
|
|
|
|
|
if !verifier.Verified() {
|
2016-09-26 23:48:16 -07:00
|
|
|
return RootCA{}, errors.Errorf("remote CA does not match fingerprint. Expected: %s", d.Hex())
|
2016-06-07 14:28:28 -07:00
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check the validity of the remote Cert
|
|
|
|
_, err = helpers.ParseCertificatePEM(response.Certificate)
|
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a Pool with our RootCACertificate
|
|
|
|
pool := x509.NewCertPool()
|
|
|
|
if !pool.AppendCertsFromPEM(response.Certificate) {
|
2016-09-26 23:48:16 -07:00
|
|
|
return RootCA{}, errors.New("failed to append certificate to cert pool")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
2016-07-20 17:34:33 -07:00
|
|
|
return RootCA{Cert: response.Certificate, Digest: digest.FromBytes(response.Certificate), Pool: pool}, nil
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// CreateAndWriteRootCA creates a Certificate authority for a new Swarm Cluster, potentially
|
|
|
|
// overwriting any existing CAs.
|
|
|
|
func CreateAndWriteRootCA(rootCN string, paths CertPaths) (RootCA, error) {
|
|
|
|
// Create a simple CSR for the CA using the default CA validator and policy
|
|
|
|
req := cfcsr.CertificateRequest{
|
|
|
|
CN: rootCN,
|
|
|
|
KeyRequest: &cfcsr.BasicKeyRequest{A: RootKeyAlgo, S: RootKeySize},
|
|
|
|
CA: &cfcsr.CAConfig{Expiry: RootCAExpiration},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate the CA and get the certificate and private key
|
|
|
|
cert, _, key, err := initca.New(&req)
|
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure directory exists
|
|
|
|
err = os.MkdirAll(filepath.Dir(paths.Cert), 0755)
|
|
|
|
if err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the Private Key and Certificate to disk, using decent permissions
|
|
|
|
if err := ioutils.AtomicWriteFile(paths.Cert, cert, 0644); err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
if err := ioutils.AtomicWriteFile(paths.Key, key, 0600); err != nil {
|
|
|
|
return RootCA{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return NewRootCA(cert, key, DefaultNodeCertExpiration)
|
|
|
|
}
|
|
|
|
|
|
|
|
// BootstrapCluster receives a directory and creates both new Root CA key material
|
|
|
|
// and a ManagerRole key/certificate pair to be used by the initial cluster manager
|
|
|
|
func BootstrapCluster(baseCertDir string) error {
|
|
|
|
paths := NewConfigPaths(baseCertDir)
|
|
|
|
|
|
|
|
rootCA, err := CreateAndWriteRootCA(rootCN, paths.RootCA)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-06-14 17:07:14 -07:00
|
|
|
nodeID := identity.NewID()
|
2016-06-07 14:28:28 -07:00
|
|
|
newOrg := identity.NewID()
|
|
|
|
_, err = GenerateAndSignNewTLSCert(rootCA, nodeID, ManagerRole, newOrg, paths.Node)
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GenerateAndSignNewTLSCert creates a new keypair, signs the certificate using signer,
|
|
|
|
// and saves the certificate and key to disk. This method is used to bootstrap the first
|
|
|
|
// manager TLS certificates.
|
|
|
|
func GenerateAndSignNewTLSCert(rootCA RootCA, cn, ou, org string, paths CertPaths) (*tls.Certificate, error) {
|
|
|
|
// Generate and new keypair and CSR
|
|
|
|
csr, key, err := generateNewCSR()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Obtain a signed Certificate
|
2016-06-15 22:41:30 -07:00
|
|
|
certChain, err := rootCA.ParseValidateAndSignCSR(csr, cn, ou, org)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.Wrap(err, "failed to sign node certificate")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure directory exists
|
|
|
|
err = os.MkdirAll(filepath.Dir(paths.Cert), 0755)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write both the chain and key to disk
|
|
|
|
if err := ioutils.AtomicWriteFile(paths.Cert, certChain, 0644); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := ioutils.AtomicWriteFile(paths.Key, key, 0600); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load a valid tls.Certificate from the chain and the key
|
|
|
|
serverCert, err := tls.X509KeyPair(certChain, key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &serverCert, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GenerateAndWriteNewKey generates a new pub/priv key pair, writes it to disk
|
|
|
|
// and returns the CSR and the private key material
|
|
|
|
func GenerateAndWriteNewKey(paths CertPaths) (csr, key []byte, err error) {
|
|
|
|
// Generate a new key pair
|
|
|
|
csr, key, err = generateNewCSR()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure directory exists
|
|
|
|
err = os.MkdirAll(filepath.Dir(paths.Key), 0755)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = ioutils.AtomicWriteFile(paths.Key, key, 0600); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-08-22 22:30:01 -07:00
|
|
|
// GetRemoteSignedCertificate submits a CSR to a remote CA server address,
|
|
|
|
// and that is part of a CA identified by a specific certificate pool.
|
2016-09-13 09:28:01 -07:00
|
|
|
func GetRemoteSignedCertificate(ctx context.Context, csr []byte, token string, rootCAPool *x509.CertPool, r remotes.Remotes, creds credentials.TransportCredentials, nodeInfo chan<- api.IssueNodeCertificateResponse) ([]byte, error) {
|
2016-06-07 14:28:28 -07:00
|
|
|
if rootCAPool == nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("valid root CA pool required")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if creds == nil {
|
|
|
|
// This is our only non-MTLS request, and it happens when we are boostraping our TLS certs
|
|
|
|
// We're using CARole as server name, so an external CA doesn't also have to have ManagerRole in the cert SANs
|
|
|
|
creds = credentials.NewTLS(&tls.Config{ServerName: CARole, RootCAs: rootCAPool})
|
|
|
|
}
|
|
|
|
|
2016-08-22 22:30:01 -07:00
|
|
|
peer, err := r.Select()
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-08-22 22:30:01 -07:00
|
|
|
opts := []grpc.DialOption{
|
|
|
|
grpc.WithTransportCredentials(creds),
|
|
|
|
grpc.WithTimeout(5 * time.Second),
|
|
|
|
grpc.WithBackoffMaxDelay(5 * time.Second),
|
|
|
|
}
|
|
|
|
|
|
|
|
conn, err := grpc.Dial(peer.Addr, opts...)
|
2016-06-07 14:28:28 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
2016-06-30 13:34:48 -07:00
|
|
|
// Create a CAClient to retrieve a new Certificate
|
2016-06-07 14:28:28 -07:00
|
|
|
caClient := api.NewNodeCAClient(conn)
|
|
|
|
|
|
|
|
// Send the Request and retrieve the request token
|
2016-07-20 17:34:33 -07:00
|
|
|
issueRequest := &api.IssueNodeCertificateRequest{CSR: csr, Token: token}
|
2016-06-07 14:28:28 -07:00
|
|
|
issueResponse, err := caClient.IssueNodeCertificate(ctx, issueRequest)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send back the NodeID on the nodeInfo, so the caller can know what ID was assigned by the CA
|
|
|
|
if nodeInfo != nil {
|
2016-06-15 22:41:30 -07:00
|
|
|
nodeInfo <- *issueResponse
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
2016-06-15 22:41:30 -07:00
|
|
|
statusRequest := &api.NodeCertificateStatusRequest{NodeID: issueResponse.NodeID}
|
2016-06-07 14:28:28 -07:00
|
|
|
expBackoff := events.NewExponentialBackoff(events.ExponentialBackoffConfig{
|
|
|
|
Base: time.Second,
|
|
|
|
Factor: time.Second,
|
|
|
|
Max: 30 * time.Second,
|
|
|
|
})
|
|
|
|
|
|
|
|
// Exponential backoff with Max of 30 seconds to wait for a new retry
|
|
|
|
for {
|
|
|
|
// Send the Request and retrieve the certificate
|
2016-08-22 22:30:01 -07:00
|
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
|
|
defer cancel()
|
2016-06-07 14:28:28 -07:00
|
|
|
statusResponse, err := caClient.NodeCertificateStatus(ctx, statusRequest)
|
|
|
|
if err != nil {
|
2016-08-22 22:30:01 -07:00
|
|
|
r.Observe(peer, -remotes.DefaultObservationWeight)
|
2016-06-07 14:28:28 -07:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the certificate was issued, return
|
|
|
|
if statusResponse.Status.State == api.IssuanceStateIssued {
|
|
|
|
if statusResponse.Certificate == nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("no certificate in CertificateStatus response")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
2016-06-30 13:34:48 -07:00
|
|
|
|
|
|
|
// The certificate in the response must match the CSR
|
|
|
|
// we submitted. If we are getting a response for a
|
|
|
|
// certificate that was previously issued, we need to
|
|
|
|
// retry until the certificate gets updated per our
|
|
|
|
// current request.
|
|
|
|
if bytes.Equal(statusResponse.Certificate.CSR, csr) {
|
2016-08-22 22:30:01 -07:00
|
|
|
r.Observe(peer, remotes.DefaultObservationWeight)
|
2016-06-30 13:34:48 -07:00
|
|
|
return statusResponse.Certificate.Certificate, nil
|
|
|
|
}
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// If we're still pending, the issuance failed, or the state is unknown
|
|
|
|
// let's continue trying.
|
|
|
|
expBackoff.Failure(nil, nil)
|
|
|
|
time.Sleep(expBackoff.Proceed(nil))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// readCertExpiration returns the number of months left for certificate expiration
|
|
|
|
func readCertExpiration(paths CertPaths) (time.Duration, error) {
|
|
|
|
// Read the Cert
|
|
|
|
cert, err := ioutil.ReadFile(paths.Cert)
|
|
|
|
if err != nil {
|
|
|
|
return time.Hour, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create an x509 certificate out of the contents on disk
|
|
|
|
certBlock, _ := pem.Decode([]byte(cert))
|
|
|
|
if certBlock == nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return time.Hour, errors.New("failed to decode certificate block")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
X509Cert, err := x509.ParseCertificate(certBlock.Bytes)
|
|
|
|
if err != nil {
|
|
|
|
return time.Hour, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return X509Cert.NotAfter.Sub(time.Now()), nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func saveRootCA(rootCA RootCA, paths CertPaths) error {
|
|
|
|
// Make sure the necessary dirs exist and they are writable
|
|
|
|
err := os.MkdirAll(filepath.Dir(paths.Cert), 0755)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the root certificate got returned successfully, save the rootCA to disk.
|
|
|
|
return ioutils.AtomicWriteFile(paths.Cert, rootCA.Cert, 0644)
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateNewCSR() (csr, key []byte, err error) {
|
|
|
|
req := &cfcsr.CertificateRequest{
|
|
|
|
KeyRequest: cfcsr.NewBasicKeyRequest(),
|
|
|
|
}
|
|
|
|
|
|
|
|
csr, key, err = cfcsr.ParseRequest(req)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// EncryptECPrivateKey receives a PEM encoded private key and returns an encrypted
|
|
|
|
// AES256 version using a passphrase
|
|
|
|
// TODO: Make this method generic to handle RSA keys
|
|
|
|
func EncryptECPrivateKey(key []byte, passphraseStr string) ([]byte, error) {
|
|
|
|
passphrase := []byte(passphraseStr)
|
|
|
|
cipherType := x509.PEMCipherAES256
|
|
|
|
|
|
|
|
keyBlock, _ := pem.Decode(key)
|
|
|
|
if keyBlock == nil {
|
|
|
|
// This RootCA does not have a valid signer.
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("error while decoding PEM key")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
encryptedPEMBlock, err := x509.EncryptPEMBlock(rand.Reader,
|
|
|
|
"EC PRIVATE KEY",
|
|
|
|
keyBlock.Bytes,
|
|
|
|
passphrase,
|
|
|
|
cipherType)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if encryptedPEMBlock.Headers == nil {
|
2016-09-26 23:48:16 -07:00
|
|
|
return nil, errors.New("unable to encrypt key - invalid PEM file produced")
|
2016-06-07 14:28:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return pem.EncodeToMemory(encryptedPEMBlock), nil
|
|
|
|
}
|