-
Notifications
You must be signed in to change notification settings - Fork 7.6k
/
ca.go
506 lines (439 loc) · 17.2 KB
/
ca.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ca
import (
"context"
"crypto/elliptic"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"time"
v1 "k8s.io/api/core/v1"
apierror "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"istio.io/istio/pkg/backoff"
"istio.io/istio/pkg/log"
"istio.io/istio/security/pkg/cmd"
caerror "istio.io/istio/security/pkg/pki/error"
"istio.io/istio/security/pkg/pki/util"
certutil "istio.io/istio/security/pkg/util"
)
const (
// istioCASecretType is the Istio secret annotation type.
istioCASecretType = "istio.io/ca-root"
// CACertFile is the CA certificate chain file.
CACertFile = "ca-cert.pem"
// CAPrivateKeyFile is the private key file of CA.
CAPrivateKeyFile = "ca-key.pem"
// CASecret stores the key/cert of self-signed CA for persistency purpose.
CASecret = "istio-ca-secret"
// CertChainFile is the ID/name for the certificate chain file.
CertChainFile = "cert-chain.pem"
// PrivateKeyFile is the ID/name for the private key file.
PrivateKeyFile = "key.pem"
// RootCertFile is the ID/name for the CA root certificate file.
RootCertFile = "root-cert.pem"
// TLSSecretCACertFile is the CA certificate file name as it exists in tls type k8s secret.
TLSSecretCACertFile = "tls.crt"
// TLSSecretCAPrivateKeyFile is the CA certificate key file name as it exists in tls type k8s secret.
TLSSecretCAPrivateKeyFile = "tls.key"
// TLSSecretRootCertFile is the root cert file name as it exists in tls type k8s secret.
TLSSecretRootCertFile = "ca.crt"
// The standard key size to use when generating an RSA private key
rsaKeySize = 2048
// CACertsSecret stores the plugin CA certificates, in external istiod scenario, the secret can be in the config cluster.
CACertsSecret = "cacerts"
// IstioGenerated is the key indicating the secret is generated by Istio.
IstioGenerated = "istio-generated"
)
// SigningCAFileBundle locations of the files used for the signing CA
type SigningCAFileBundle struct {
RootCertFile string
CertChainFiles []string
SigningCertFile string
SigningKeyFile string
}
var pkiCaLog = log.RegisterScope("pkica", "Citadel CA log")
// caTypes is the enum for the CA type.
type caTypes int
type CertOpts struct {
// SubjectIDs are used for building the SAN extension for the certificate.
SubjectIDs []string
// TTL is the requested lifetime (Time to live) to be applied in the certificate.
TTL time.Duration
// ForCA indicates whether the signed certificate if for CA.
// If true, the signed certificate is a CA certificate, otherwise, it is a workload certificate.
ForCA bool
// Cert Signer info
CertSigner string
}
const (
// selfSignedCA means the Istio CA uses a self signed certificate.
selfSignedCA caTypes = iota
// pluggedCertCA means the Istio CA uses a operator-specified key/cert.
pluggedCertCA
)
// IstioCAOptions holds the configurations for creating an Istio CA.
type IstioCAOptions struct {
CAType caTypes
DefaultCertTTL time.Duration
MaxCertTTL time.Duration
CARSAKeySize int
KeyCertBundle *util.KeyCertBundle
// Config for creating self-signed root cert rotator.
RotatorConfig *SelfSignedCARootCertRotatorConfig
// OnRootCertUpdate is the cb which can only be called by self-signed root cert rotator
OnRootCertUpdate func() error
}
type RootCertUpdateFunc func() error
// NewSelfSignedIstioCAOptions returns a new IstioCAOptions instance using self-signed certificate.
func NewSelfSignedIstioCAOptions(ctx context.Context,
rootCertGracePeriodPercentile int, caCertTTL, rootCertCheckInverval, defaultCertTTL,
maxCertTTL time.Duration, org string, useCacertsSecretName, dualUse bool, namespace string, client corev1.CoreV1Interface,
rootCertFile string, enableJitter bool, caRSAKeySize int,
) (caOpts *IstioCAOptions, err error) {
caOpts = &IstioCAOptions{
CAType: selfSignedCA,
DefaultCertTTL: defaultCertTTL,
MaxCertTTL: maxCertTTL,
RotatorConfig: &SelfSignedCARootCertRotatorConfig{
CheckInterval: rootCertCheckInverval,
caCertTTL: caCertTTL,
retryInterval: cmd.ReadSigningCertRetryInterval,
retryMax: cmd.ReadSigningCertRetryMax,
certInspector: certutil.NewCertUtil(rootCertGracePeriodPercentile),
caStorageNamespace: namespace,
dualUse: dualUse,
org: org,
rootCertFile: rootCertFile,
enableJitter: enableJitter,
client: client,
},
}
// always use ``istio-ca-secret` in priority, otherwise fall back to `cacerts`
var caCertName string
b := backoff.NewExponentialBackOff(backoff.DefaultOption())
err = b.RetryWithContext(ctx, func() error {
caCertName = CASecret
// 1. fetch `istio-ca-secret` in priority
err := loadSelfSignedCaSecret(client, namespace, caCertName, rootCertFile, caOpts)
if err == nil {
return nil
} else if apierror.IsNotFound(err) {
// 2. if `istio-ca-secret` not exist and use cacerts enabled, fallback to fetch `cacerts`
if useCacertsSecretName {
caCertName = CACertsSecret
err := loadSelfSignedCaSecret(client, namespace, caCertName, rootCertFile, caOpts)
if err == nil {
return nil
} else if apierror.IsNotFound(err) { // if neither `istio-ca-secret` nor `cacerts` exists, we create a `cacerts`
// continue to create `cacerts`
} else {
return err
}
}
// 3. if use cacerts disabled, create `istio-ca-secret`, otherwise create `cacerts`.
pkiCaLog.Infof("CASecret %s not found, will create one", caCertName)
options := util.CertOptions{
TTL: caCertTTL,
Org: org,
IsCA: true,
IsSelfSigned: true,
RSAKeySize: caRSAKeySize,
IsDualUse: dualUse,
}
pemCert, pemKey, ckErr := util.GenCertKeyFromOptions(options)
if ckErr != nil {
pkiCaLog.Warnf("unable to generate CA cert and key for self-signed CA (%v)", ckErr)
return fmt.Errorf("unable to generate CA cert and key for self-signed CA (%v)", ckErr)
}
rootCerts, err := util.AppendRootCerts(pemCert, rootCertFile)
if err != nil {
pkiCaLog.Warnf("failed to append root certificates (%v)", err)
return fmt.Errorf("failed to append root certificates (%v)", err)
}
if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(pemCert, pemKey, nil, rootCerts); err != nil {
pkiCaLog.Warnf("failed to create CA KeyCertBundle (%v)", err)
return fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
}
// Write the key/cert back to secret, so they will be persistent when CA restarts.
secret := BuildSecret(caCertName, namespace, nil, nil, pemCert, pemCert, pemKey, istioCASecretType)
_, err = client.Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
if err != nil {
pkiCaLog.Warnf("Failed to create secret %s (%v)", caCertName, err)
return err
}
pkiCaLog.Infof("Using self-generated public key: %v", string(rootCerts))
return nil
}
return err
})
pkiCaLog.Infof("Set secret name for self-signed CA cert rotator to %s", caCertName)
caOpts.RotatorConfig.secretName = caCertName
return caOpts, err
}
func loadSelfSignedCaSecret(client corev1.CoreV1Interface, namespace string, caCertName string, rootCertFile string, caOpts *IstioCAOptions) error {
caSecret, err := client.Secrets(namespace).Get(context.TODO(), caCertName, metav1.GetOptions{})
if err == nil {
pkiCaLog.Infof("Load signing key and cert from existing secret %s/%s", caSecret.Namespace, caSecret.Name)
rootCerts, err := util.AppendRootCerts(caSecret.Data[CACertFile], rootCertFile)
if err != nil {
return fmt.Errorf("failed to append root certificates (%v)", err)
}
if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(caSecret.Data[CACertFile],
caSecret.Data[CAPrivateKeyFile], nil, rootCerts); err != nil {
return fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
}
pkiCaLog.Infof("Using existing public key: %v", string(rootCerts))
}
return err
}
// NewSelfSignedDebugIstioCAOptions returns a new IstioCAOptions instance using self-signed certificate produced by in-memory CA,
// which runs without K8s, and no local ca key file presented.
func NewSelfSignedDebugIstioCAOptions(rootCertFile string, caCertTTL, defaultCertTTL, maxCertTTL time.Duration,
org string, caRSAKeySize int,
) (caOpts *IstioCAOptions, err error) {
caOpts = &IstioCAOptions{
CAType: selfSignedCA,
DefaultCertTTL: defaultCertTTL,
MaxCertTTL: maxCertTTL,
CARSAKeySize: caRSAKeySize,
}
options := util.CertOptions{
TTL: caCertTTL,
Org: org,
IsCA: true,
IsSelfSigned: true,
RSAKeySize: caRSAKeySize,
IsDualUse: true, // hardcoded to true for K8S as well
}
pemCert, pemKey, ckErr := util.GenCertKeyFromOptions(options)
if ckErr != nil {
return nil, fmt.Errorf("unable to generate CA cert and key for self-signed CA (%v)", ckErr)
}
rootCerts, err := util.AppendRootCerts(pemCert, rootCertFile)
if err != nil {
return nil, fmt.Errorf("failed to append root certificates (%v)", err)
}
if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromPem(pemCert, pemKey, nil, rootCerts); err != nil {
return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
}
return caOpts, nil
}
// NewPluggedCertIstioCAOptions returns a new IstioCAOptions instance using given certificate.
func NewPluggedCertIstioCAOptions(fileBundle SigningCAFileBundle,
defaultCertTTL, maxCertTTL time.Duration, caRSAKeySize int,
) (caOpts *IstioCAOptions, err error) {
caOpts = &IstioCAOptions{
CAType: pluggedCertCA,
DefaultCertTTL: defaultCertTTL,
MaxCertTTL: maxCertTTL,
CARSAKeySize: caRSAKeySize,
}
if caOpts.KeyCertBundle, err = util.NewVerifiedKeyCertBundleFromFile(
fileBundle.SigningCertFile, fileBundle.SigningKeyFile, fileBundle.CertChainFiles, fileBundle.RootCertFile); err != nil {
return nil, fmt.Errorf("failed to create CA KeyCertBundle (%v)", err)
}
// Validate that the passed in signing cert can be used as CA.
// The check can't be done inside `KeyCertBundle`, since bundle could also be used to
// validate workload certificates (i.e., where the leaf certificate is not a CA).
b, err := os.ReadFile(fileBundle.SigningCertFile)
if err != nil {
return nil, err
}
block, _ := pem.Decode(b)
if block == nil {
return nil, fmt.Errorf("invalid PEM encoded certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse X.509 certificate")
}
if !cert.IsCA {
return nil, fmt.Errorf("certificate is not authorized to sign other certificates")
}
return caOpts, nil
}
// BuildSecret returns a secret struct, contents of which are filled with parameters passed in.
// Adds the "istio-generated" key if the secret name is `cacerts`.
func BuildSecret(scrtName, namespace string, certChain, privateKey, rootCert, caCert, caPrivateKey []byte, secretType v1.SecretType) *v1.Secret {
secret := &v1.Secret{
Data: map[string][]byte{
CertChainFile: certChain,
PrivateKeyFile: privateKey,
RootCertFile: rootCert,
CACertFile: caCert,
CAPrivateKeyFile: caPrivateKey,
},
ObjectMeta: metav1.ObjectMeta{
Name: scrtName,
Namespace: namespace,
},
Type: secretType,
}
if scrtName == CACertsSecret {
secret.Data[IstioGenerated] = []byte("")
}
return secret
}
// IstioCA generates keys and certificates for Istio identities.
type IstioCA struct {
defaultCertTTL time.Duration
maxCertTTL time.Duration
caRSAKeySize int
keyCertBundle *util.KeyCertBundle
// rootCertRotator periodically rotates self-signed root cert for CA. It is nil
// if CA is not self-signed CA.
rootCertRotator *SelfSignedCARootCertRotator
}
// NewIstioCA returns a new IstioCA instance.
func NewIstioCA(opts *IstioCAOptions) (*IstioCA, error) {
ca := &IstioCA{
maxCertTTL: opts.MaxCertTTL,
keyCertBundle: opts.KeyCertBundle,
caRSAKeySize: opts.CARSAKeySize,
}
if opts.CAType == selfSignedCA && opts.RotatorConfig != nil && opts.RotatorConfig.CheckInterval > time.Duration(0) {
ca.rootCertRotator = NewSelfSignedCARootCertRotator(opts.RotatorConfig, ca, opts.OnRootCertUpdate)
}
// if CA cert becomes invalid before workload cert it's going to cause workload cert to be invalid too,
// however citatel won't rotate if that happens, this function will prevent that using cert chain TTL as
// the workload TTL
defaultCertTTL, err := ca.minTTL(opts.DefaultCertTTL)
if err != nil {
return ca, fmt.Errorf("failed to get default cert TTL %s", err.Error())
}
ca.defaultCertTTL = defaultCertTTL
return ca, nil
}
func (ca *IstioCA) Run(stopChan chan struct{}) {
if ca.rootCertRotator != nil {
// Start root cert rotator in a separate goroutine.
go ca.rootCertRotator.Run(stopChan)
}
}
// Sign takes a PEM-encoded CSR and cert opts, and returns a signed certificate.
func (ca *IstioCA) Sign(csrPEM []byte, certOpts CertOpts) (
[]byte, error,
) {
return ca.sign(csrPEM, certOpts.SubjectIDs, certOpts.TTL, true, certOpts.ForCA)
}
// SignWithCertChain is similar to Sign but returns the leaf cert and the entire cert chain.
func (ca *IstioCA) SignWithCertChain(csrPEM []byte, certOpts CertOpts) (
[]string, error,
) {
cert, err := ca.signWithCertChain(csrPEM, certOpts.SubjectIDs, certOpts.TTL, true, certOpts.ForCA)
if err != nil {
return nil, err
}
return []string{string(cert)}, nil
}
// GetCAKeyCertBundle returns the KeyCertBundle for the CA.
func (ca *IstioCA) GetCAKeyCertBundle() *util.KeyCertBundle {
return ca.keyCertBundle
}
// GenKeyCert generates a certificate signed by the CA,
// returns the certificate chain and the private key.
func (ca *IstioCA) GenKeyCert(hostnames []string, certTTL time.Duration, checkLifetime bool) ([]byte, []byte, error) {
opts := util.CertOptions{
RSAKeySize: rsaKeySize,
}
// use the type of private key the CA uses to generate an intermediate CA of that type (e.g. CA cert using RSA will
// cause intermediate CAs using RSA to be generated)
_, signingKey, _, _ := ca.keyCertBundle.GetAll()
curve, err := util.GetEllipticCurve(signingKey)
if err == nil {
opts.ECSigAlg = util.EcdsaSigAlg
switch curve {
case elliptic.P384():
opts.ECCCurve = util.P384Curve
default:
opts.ECCCurve = util.P256Curve
}
}
csrPEM, privPEM, err := util.GenCSR(opts)
if err != nil {
return nil, nil, err
}
certPEM, err := ca.signWithCertChain(csrPEM, hostnames, certTTL, checkLifetime, false)
if err != nil {
return nil, nil, err
}
return certPEM, privPEM, nil
}
func (ca *IstioCA) minTTL(defaultCertTTL time.Duration) (time.Duration, error) {
certChainPem := ca.keyCertBundle.GetCertChainPem()
if len(certChainPem) == 0 {
return defaultCertTTL, nil
}
certChainExpiration, err := util.TimeBeforeCertExpires(certChainPem, time.Now())
if err != nil {
return 0, fmt.Errorf("failed to get cert chain TTL %s", err.Error())
}
if certChainExpiration.Seconds() <= 0 {
return 0, fmt.Errorf("cert chain has expired")
}
if defaultCertTTL.Seconds() > certChainExpiration.Seconds() {
return certChainExpiration, nil
}
return defaultCertTTL, nil
}
func (ca *IstioCA) sign(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, checkLifetime, forCA bool) ([]byte, error) {
signingCert, signingKey, _, _ := ca.keyCertBundle.GetAll()
if signingCert == nil {
return nil, caerror.NewError(caerror.CANotReady, fmt.Errorf("Istio CA is not ready")) // nolint
}
csr, err := util.ParsePemEncodedCSR(csrPEM)
if err != nil {
return nil, caerror.NewError(caerror.CSRError, err)
}
if err := csr.CheckSignature(); err != nil {
return nil, caerror.NewError(caerror.CSRError, err)
}
lifetime := requestedLifetime
// If the requested requestedLifetime is non-positive, apply the default TTL.
if requestedLifetime.Seconds() <= 0 {
lifetime = ca.defaultCertTTL
}
// If checkLifetime is set and the requested TTL is greater than maxCertTTL, return an error
if checkLifetime && requestedLifetime.Seconds() > ca.maxCertTTL.Seconds() {
return nil, caerror.NewError(caerror.TTLError, fmt.Errorf(
"requested TTL %s is greater than the max allowed TTL %s", requestedLifetime, ca.maxCertTTL))
}
certBytes, err := util.GenCertFromCSR(csr, signingCert, csr.PublicKey, *signingKey, subjectIDs, lifetime, forCA)
if err != nil {
return nil, caerror.NewError(caerror.CertGenError, err)
}
block := &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
}
cert := pem.EncodeToMemory(block)
return cert, nil
}
func (ca *IstioCA) signWithCertChain(csrPEM []byte, subjectIDs []string, requestedLifetime time.Duration, lifetimeCheck,
forCA bool,
) ([]byte, error) {
cert, err := ca.sign(csrPEM, subjectIDs, requestedLifetime, lifetimeCheck, forCA)
if err != nil {
return nil, err
}
chainPem := ca.GetCAKeyCertBundle().GetCertChainPem()
if len(chainPem) > 0 {
cert = append(cert, chainPem...)
}
return cert, nil
}