Tizen_4.0 base
[platform/upstream/docker-engine.git] / vendor / github.com / docker / swarmkit / manager / controlapi / ca_rotation.go
1 package controlapi
2
3 import (
4         "bytes"
5         "context"
6         "crypto/tls"
7         "crypto/x509"
8         "errors"
9         "net"
10         "net/url"
11         "time"
12
13         "google.golang.org/grpc"
14         "google.golang.org/grpc/codes"
15
16         "github.com/cloudflare/cfssl/helpers"
17         "github.com/docker/swarmkit/api"
18         "github.com/docker/swarmkit/ca"
19         "github.com/docker/swarmkit/log"
20 )
21
22 var minRootExpiration = 1 * helpers.OneYear
23
24 // determines whether an api.RootCA, api.RootRotation, or api.CAConfig has a signing key (local signer)
25 func hasSigningKey(a interface{}) bool {
26         switch b := a.(type) {
27         case *api.RootCA:
28                 return len(b.CAKey) > 0
29         case *api.RootRotation:
30                 return b != nil && len(b.CAKey) > 0
31         case *api.CAConfig:
32                 return len(b.SigningCACert) > 0 && len(b.SigningCAKey) > 0
33         default:
34                 panic("needsExternalCAs should be called something of type *api.RootCA, *api.RootRotation, or *api.CAConfig")
35         }
36 }
37
38 // Creates a cross-signed intermediate and new api.RootRotation object.
39 // This function assumes that the root cert and key and the external CAs have already been validated.
40 func newRootRotationObject(ctx context.Context, securityConfig *ca.SecurityConfig, apiRootCA *api.RootCA, newCARootCA ca.RootCA, extCAs []*api.ExternalCA, version uint64) (*api.RootCA, error) {
41         var (
42                 rootCert, rootKey, crossSignedCert []byte
43                 newRootHasSigner                   bool
44                 err                                error
45         )
46
47         rootCert = newCARootCA.Certs
48         if s, err := newCARootCA.Signer(); err == nil {
49                 rootCert, rootKey = s.Cert, s.Key
50                 newRootHasSigner = true
51         }
52
53         // we have to sign with the original signer, not whatever is in the SecurityConfig's RootCA (which may have an intermediate signer, if
54         // a root rotation is already in progress)
55         switch {
56         case hasSigningKey(apiRootCA):
57                 var oldRootCA ca.RootCA
58                 oldRootCA, err = ca.NewRootCA(apiRootCA.CACert, apiRootCA.CACert, apiRootCA.CAKey, ca.DefaultNodeCertExpiration, nil)
59                 if err == nil {
60                         crossSignedCert, err = oldRootCA.CrossSignCACertificate(rootCert)
61                 }
62         case !newRootHasSigner: // the original CA and the new CA both require external CAs
63                 return nil, grpc.Errorf(codes.InvalidArgument, "rotating from one external CA to a different external CA is not supported")
64         default:
65                 // We need the same credentials but to connect to the original URLs (in case we are in the middle of a root rotation already)
66                 externalCA := securityConfig.ExternalCA().Copy()
67                 var urls []string
68                 for _, c := range extCAs {
69                         if c.Protocol == api.ExternalCA_CAProtocolCFSSL {
70                                 urls = append(urls, c.URL)
71                         }
72                 }
73                 if len(urls) == 0 {
74                         return nil, grpc.Errorf(codes.InvalidArgument,
75                                 "must provide an external CA for the current external root CA to generate a cross-signed certificate")
76                 }
77                 externalCA.UpdateURLs(urls...)
78                 crossSignedCert, err = externalCA.CrossSignRootCA(ctx, newCARootCA)
79         }
80
81         if err != nil {
82                 log.G(ctx).WithError(err).Error("unable to generate a cross-signed certificate for root rotation")
83                 return nil, grpc.Errorf(codes.Internal, "unable to generate a cross-signed certificate for root rotation")
84         }
85
86         copied := apiRootCA.Copy()
87         copied.RootRotation = &api.RootRotation{
88                 CACert:            rootCert,
89                 CAKey:             rootKey,
90                 CrossSignedCACert: ca.NormalizePEMs(crossSignedCert),
91         }
92         copied.LastForcedRotation = version
93         return copied, nil
94 }
95
96 // Checks that a CA URL is connectable using the credentials we have and that its server certificate is signed by the
97 // root CA that we expect.  This uses a TCP dialer rather than an HTTP client; because we have custom TLS configuration,
98 // if we wanted to use an HTTP client we'd have to create a new transport for every connection.  The docs specify that
99 // Transports cache connections for future re-use, which could cause many open connections.
100 func validateExternalCAURL(dialer *net.Dialer, tlsOpts *tls.Config, caURL string) error {
101         parsed, err := url.Parse(caURL)
102         if err != nil {
103                 return err
104         }
105         if parsed.Scheme != "https" {
106                 return errors.New("invalid HTTP scheme")
107         }
108         host, port, err := net.SplitHostPort(parsed.Host)
109         if err != nil {
110                 // It either has no port or is otherwise invalid (e.g. too many colons).  If it's otherwise invalid the dialer
111                 // will error later, so just assume it's no port and set the port to the default HTTPS port.
112                 host = parsed.Host
113                 port = "443"
114         }
115
116         conn, err := tls.DialWithDialer(dialer, "tcp", net.JoinHostPort(host, port), tlsOpts)
117         if conn != nil {
118                 conn.Close()
119         }
120         return err
121 }
122
123 // Validates that there is at least 1 reachable, valid external CA for the given CA certificate.  Returns true if there is, false otherwise.
124 // Requires that the wanted cert is already normalized.
125 func validateHasAtLeastOneExternalCA(ctx context.Context, externalCAs map[string][]*api.ExternalCA, securityConfig *ca.SecurityConfig,
126         wantedCert []byte, desc string) ([]*api.ExternalCA, error) {
127         specific, ok := externalCAs[string(wantedCert)]
128         if ok {
129                 pool := x509.NewCertPool()
130                 pool.AppendCertsFromPEM(wantedCert)
131                 dialer := net.Dialer{Timeout: 5 * time.Second}
132                 opts := tls.Config{
133                         RootCAs:      pool,
134                         Certificates: securityConfig.ClientTLSCreds.Config().Certificates,
135                 }
136                 for i, ca := range specific {
137                         if ca.Protocol == api.ExternalCA_CAProtocolCFSSL {
138                                 if err := validateExternalCAURL(&dialer, &opts, ca.URL); err != nil {
139                                         log.G(ctx).WithError(err).Warnf("external CA # %d is unreachable or invalid", i+1)
140                                 } else {
141                                         return specific, nil
142                                 }
143                         }
144                 }
145         }
146         return nil, grpc.Errorf(codes.InvalidArgument, "there must be at least one valid, reachable external CA corresponding to the %s CA certificate", desc)
147 }
148
149 // validates that the list of external CAs have valid certs associated with them, and produce a mapping of subject/pubkey:external
150 // for later validation of required external CAs
151 func getNormalizedExtCAs(caConfig *api.CAConfig, normalizedCurrentRootCACert []byte) (map[string][]*api.ExternalCA, error) {
152         extCAs := make(map[string][]*api.ExternalCA)
153
154         for _, extCA := range caConfig.ExternalCAs {
155                 associatedCert := normalizedCurrentRootCACert
156                 // if no associated cert is provided, assume it's the current root cert
157                 if len(extCA.CACert) > 0 {
158                         associatedCert = ca.NormalizePEMs(extCA.CACert)
159                 }
160                 certKey := string(associatedCert)
161                 extCAs[certKey] = append(extCAs[certKey], extCA)
162         }
163
164         return extCAs, nil
165 }
166
167 // validateAndUpdateCA validates a cluster's desired CA configuration spec, and returns a RootCA value on success representing
168 // current RootCA as it should be.  Validation logic and return values are as follows:
169 // 1. Validates that the contents are complete - e.g. a signing key is not provided without a signing cert, and that external
170 //    CAs are not removed if they are needed.  Otherwise, returns an error.
171 // 2. If no desired signing cert or key are provided, then either:
172 //    - we are happy with the current CA configuration (force rotation value has not changed), and we return the current RootCA
173 //      object as is
174 //    - we want to generate a new internal CA cert and key (force rotation value has changed), and we return the updated RootCA
175 //      object
176 // 3. Signing cert and key have been provided: validate that these match (the cert and key match). Otherwise, return an error.
177 // 4. Return the updated RootCA object according to the following criteria:
178 //    - If the desired cert is the same as the current CA cert then abort any outstanding rotations. The current signing key
179 //      is replaced with the desired signing key (this could lets us switch between external->internal or internal->external
180 //      without an actual CA rotation, which is not needed because any leaf cert issued with one CA cert can be validated using
181 //       the second CA certificate).
182 //    - If the desired cert is the same as the current to-be-rotated-to CA cert then a new root rotation is not needed. The
183 //      current to-be-rotated-to signing key is replaced with the desired signing key (this could lets us switch between
184 //      external->internal or internal->external without an actual CA rotation, which is not needed because any leaf cert
185 //      issued with one CA cert can be validated using the second CA certificate).
186 //    - Otherwise, start a new root rotation using the desired signing cert and desired signing key as the root rotation
187 //      signing cert and key.  If a root rotation is already in progress, just replace it and start over.
188 func validateCAConfig(ctx context.Context, securityConfig *ca.SecurityConfig, cluster *api.Cluster) (*api.RootCA, error) {
189         newConfig := cluster.Spec.CAConfig.Copy()
190         newConfig.SigningCACert = ca.NormalizePEMs(newConfig.SigningCACert) // ensure this is normalized before we use it
191
192         if len(newConfig.SigningCAKey) > 0 && len(newConfig.SigningCACert) == 0 {
193                 return nil, grpc.Errorf(codes.InvalidArgument, "if a signing CA key is provided, the signing CA cert must also be provided")
194         }
195
196         normalizedRootCA := ca.NormalizePEMs(cluster.RootCA.CACert)
197         extCAs, err := getNormalizedExtCAs(newConfig, normalizedRootCA) // validate that the list of external CAs is not malformed
198         if err != nil {
199                 return nil, err
200         }
201
202         var oldCertExtCAs []*api.ExternalCA
203         if !hasSigningKey(&cluster.RootCA) {
204                 oldCertExtCAs, err = validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, normalizedRootCA, "current")
205                 if err != nil {
206                         return nil, err
207                 }
208         }
209
210         // if the desired CA cert and key are not set, then we are happy with the current root CA configuration, unless
211         // the ForceRotate version has changed
212         if len(newConfig.SigningCACert) == 0 {
213                 if cluster.RootCA.LastForcedRotation != newConfig.ForceRotate {
214                         newRootCA, err := ca.CreateRootCA(ca.DefaultRootCN)
215                         if err != nil {
216                                 return nil, grpc.Errorf(codes.Internal, err.Error())
217                         }
218                         return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
219                 }
220
221                 // we also need to make sure that if the current root rotation requires an external CA, those external CAs are
222                 // still valid
223                 if cluster.RootCA.RootRotation != nil && !hasSigningKey(cluster.RootCA.RootRotation) {
224                         _, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, ca.NormalizePEMs(cluster.RootCA.RootRotation.CACert), "next")
225                         if err != nil {
226                                 return nil, err
227                         }
228                 }
229
230                 return &cluster.RootCA, nil // no change, return as is
231         }
232
233         // A desired cert and maybe key were provided - we need to make sure the cert and key (if provided) match.
234         var signingCert []byte
235         if hasSigningKey(newConfig) {
236                 signingCert = newConfig.SigningCACert
237         }
238         newRootCA, err := ca.NewRootCA(newConfig.SigningCACert, signingCert, newConfig.SigningCAKey, ca.DefaultNodeCertExpiration, nil)
239         if err != nil {
240                 return nil, grpc.Errorf(codes.InvalidArgument, err.Error())
241         }
242
243         if len(newRootCA.Pool.Subjects()) != 1 {
244                 return nil, grpc.Errorf(codes.InvalidArgument, "the desired CA certificate cannot contain multiple certificates")
245         }
246
247         parsedCert, err := helpers.ParseCertificatePEM(newConfig.SigningCACert)
248         if err != nil {
249                 return nil, grpc.Errorf(codes.InvalidArgument, "could not parse the desired CA certificate")
250         }
251
252         // The new certificate's expiry must be at least one year away
253         if parsedCert.NotAfter.Before(time.Now().Add(minRootExpiration)) {
254                 return nil, grpc.Errorf(codes.InvalidArgument, "CA certificate expires too soon")
255         }
256
257         if !hasSigningKey(newConfig) {
258                 if _, err := validateHasAtLeastOneExternalCA(ctx, extCAs, securityConfig, newConfig.SigningCACert, "desired"); err != nil {
259                         return nil, err
260                 }
261         }
262
263         // check if we can abort any existing root rotations
264         if bytes.Equal(normalizedRootCA, newConfig.SigningCACert) {
265                 copied := cluster.RootCA.Copy()
266                 copied.CAKey = newConfig.SigningCAKey
267                 copied.RootRotation = nil
268                 copied.LastForcedRotation = newConfig.ForceRotate
269                 return copied, nil
270         }
271
272         // check if this is the same desired cert as an existing root rotation
273         if r := cluster.RootCA.RootRotation; r != nil && bytes.Equal(ca.NormalizePEMs(r.CACert), newConfig.SigningCACert) {
274                 copied := cluster.RootCA.Copy()
275                 copied.RootRotation.CAKey = newConfig.SigningCAKey
276                 copied.LastForcedRotation = newConfig.ForceRotate
277                 return copied, nil
278         }
279
280         // ok, everything's different; we have to begin a new root rotation which means generating a new cross-signed cert
281         return newRootRotationObject(ctx, securityConfig, &cluster.RootCA, newRootCA, oldCertExtCAs, newConfig.ForceRotate)
282 }