| // 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. |
| |
| // Provides utility methods to generate X.509 certificates with different |
| // options. This implementation is Largely inspired from |
| // https://golang.org/src/crypto/tls/generate_cert.go. |
| |
| package util |
| |
| import ( |
| "crypto" |
| "crypto/ecdsa" |
| "crypto/ed25519" |
| "crypto/elliptic" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/pem" |
| "errors" |
| "fmt" |
| "math/big" |
| "os" |
| "strings" |
| "time" |
| ) |
| |
| import ( |
| "istio.io/pkg/log" |
| ) |
| |
| // SupportedECSignatureAlgorithms are the types of EC Signature Algorithms |
| // to be used in key generation (e.g. ECDSA or ED2551) |
| type SupportedECSignatureAlgorithms string |
| |
| const ( |
| // only ECDSA using P256 is currently supported |
| EcdsaSigAlg SupportedECSignatureAlgorithms = "ECDSA" |
| ) |
| |
| // CertOptions contains options for generating a new certificate. |
| type CertOptions struct { |
| // Comma-separated hostnames and IPs to generate a certificate for. |
| // This can also be set to the identity running the workload, |
| // like kubernetes service account. |
| Host string |
| |
| // The NotBefore field of the issued certificate. |
| NotBefore time.Time |
| |
| // TTL of the certificate. NotAfter - NotBefore. |
| TTL time.Duration |
| |
| // Signer certificate. |
| SignerCert *x509.Certificate |
| |
| // Signer private key. |
| SignerPriv crypto.PrivateKey |
| |
| // Signer private key (PEM encoded). |
| SignerPrivPem []byte |
| |
| // Organization for this certificate. |
| Org string |
| |
| // The size of RSA private key to be generated. |
| RSAKeySize int |
| |
| // Whether this certificate is used as signing cert for CA. |
| IsCA bool |
| |
| // Whether this certificate is self-signed. |
| IsSelfSigned bool |
| |
| // Whether this certificate is for a client. |
| IsClient bool |
| |
| // Whether this certificate is for a server. |
| IsServer bool |
| |
| // Whether this certificate is for dual-use clients (SAN+CN). |
| IsDualUse bool |
| |
| // If true, the private key is encoded with PKCS#8. |
| PKCS8Key bool |
| |
| // The type of Elliptical Signature algorithm to use |
| // when generating private keys. Currently only ECDSA is supported. |
| // If empty, RSA is used, otherwise ECC is used. |
| ECSigAlg SupportedECSignatureAlgorithms |
| |
| // Subjective Alternative Name values. |
| DNSNames string |
| } |
| |
| // GenCertKeyFromOptions generates a X.509 certificate and a private key with the given options. |
| func GenCertKeyFromOptions(options CertOptions) (pemCert []byte, pemKey []byte, err error) { |
| // Generate the appropriate private&public key pair based on options. |
| // The public key will be bound to the certificate generated below. The |
| // private key will be used to sign this certificate in the self-signed |
| // case, otherwise the certificate is signed by the signer private key |
| // as specified in the CertOptions. |
| if options.ECSigAlg != "" { |
| var ecPriv *ecdsa.PrivateKey |
| |
| switch options.ECSigAlg { |
| case EcdsaSigAlg: |
| ecPriv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| return nil, nil, fmt.Errorf("cert generation fails at EC key generation (%v)", err) |
| } |
| default: |
| return nil, nil, errors.New("cert generation fails due to unsupported EC signature algorithm") |
| } |
| return genCert(options, ecPriv, &ecPriv.PublicKey) |
| } |
| |
| if options.RSAKeySize < minimumRsaKeySize { |
| return nil, nil, fmt.Errorf("requested key size does not meet the minimum requied size of %d (requested: %d)", minimumRsaKeySize, options.RSAKeySize) |
| } |
| rsaPriv, err := rsa.GenerateKey(rand.Reader, options.RSAKeySize) |
| if err != nil { |
| return nil, nil, fmt.Errorf("cert generation fails at RSA key generation (%v)", err) |
| } |
| return genCert(options, rsaPriv, &rsaPriv.PublicKey) |
| } |
| |
| func genCert(options CertOptions, priv interface{}, key interface{}) ([]byte, []byte, error) { |
| template, err := genCertTemplateFromOptions(options) |
| if err != nil { |
| return nil, nil, fmt.Errorf("cert generation fails at cert template creation (%v)", err) |
| } |
| signerCert, signerKey := template, crypto.PrivateKey(priv) |
| if !options.IsSelfSigned { |
| signerCert, signerKey = options.SignerCert, options.SignerPriv |
| } |
| certBytes, err := x509.CreateCertificate(rand.Reader, template, signerCert, key, signerKey) |
| if err != nil { |
| return nil, nil, fmt.Errorf("cert generation fails at X509 cert creation (%v)", err) |
| } |
| |
| pemCert, pemKey, err := encodePem(false, certBytes, priv, options.PKCS8Key) |
| return pemCert, pemKey, err |
| } |
| |
| func publicKey(priv interface{}) interface{} { |
| switch k := priv.(type) { |
| case *rsa.PrivateKey: |
| return &k.PublicKey |
| case *ecdsa.PrivateKey: |
| return &k.PublicKey |
| case ed25519.PrivateKey: |
| return k.Public().(ed25519.PublicKey) |
| default: |
| return nil |
| } |
| } |
| |
| // GenRootCertFromExistingKey generates a X.509 certificate using existing |
| // CA private key. Only called by a self-signed Citadel. |
| func GenRootCertFromExistingKey(options CertOptions) (pemCert []byte, pemKey []byte, err error) { |
| if !options.IsSelfSigned || len(options.SignerPrivPem) == 0 { |
| return nil, nil, fmt.Errorf("skip cert " + |
| "generation. Citadel is not in self-signed mode or CA private key is not " + |
| "available") |
| } |
| |
| template, err := genCertTemplateFromOptions(options) |
| if err != nil { |
| return nil, nil, fmt.Errorf("cert generation fails at cert template creation (%v)", err) |
| } |
| caPrivateKey, err := ParsePemEncodedKey(options.SignerPrivPem) |
| if err != nil { |
| return nil, nil, fmt.Errorf("unrecogniazed CA "+ |
| "private key, skip root cert rotation: %s", err.Error()) |
| } |
| certBytes, err := x509.CreateCertificate(rand.Reader, template, template, publicKey(caPrivateKey), caPrivateKey) |
| if err != nil { |
| return nil, nil, fmt.Errorf("cert generation fails at X509 cert creation (%v)", err) |
| } |
| |
| pemCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) |
| return pemCert, options.SignerPrivPem, nil |
| } |
| |
| // GetCertOptionsFromExistingCert parses cert and generates a CertOptions |
| // that contains information about the cert. This is the reverse operation of |
| // genCertTemplateFromOptions(), and only called by a self-signed Citadel. |
| func GetCertOptionsFromExistingCert(certBytes []byte) (opts CertOptions, err error) { |
| cert, certErr := ParsePemEncodedCertificate(certBytes) |
| if certErr != nil { |
| return opts, certErr |
| } |
| |
| orgs := cert.Subject.Organization |
| if len(orgs) > 0 { |
| opts.Org = orgs[0] |
| } |
| // TODO(JimmyCYJ): parse other fields from certificate, e.g. CommonName. |
| return opts, nil |
| } |
| |
| // MergeCertOptions merges deltaOpts into defaultOpts and returns the merged |
| // CertOptions. Only called by a self-signed Citadel. |
| func MergeCertOptions(defaultOpts, deltaOpts CertOptions) CertOptions { |
| if len(deltaOpts.Org) > 0 { |
| defaultOpts.Org = deltaOpts.Org |
| } |
| // TODO(JimmyCYJ): merge other fields, e.g. Host, IsDualUse, etc. |
| return defaultOpts |
| } |
| |
| // GenCertFromCSR generates a X.509 certificate with the given CSR. |
| func GenCertFromCSR(csr *x509.CertificateRequest, signingCert *x509.Certificate, publicKey interface{}, |
| signingKey crypto.PrivateKey, subjectIDs []string, ttl time.Duration, isCA bool) (cert []byte, err error) { |
| tmpl, err := genCertTemplateFromCSR(csr, subjectIDs, ttl, isCA) |
| if err != nil { |
| return nil, err |
| } |
| return x509.CreateCertificate(rand.Reader, tmpl, signingCert, publicKey, signingKey) |
| } |
| |
| // LoadSignerCredsFromFiles loads the signer cert&key from the given files. |
| // |
| // signerCertFile: cert file name |
| // signerPrivFile: private key file name |
| func LoadSignerCredsFromFiles(signerCertFile string, signerPrivFile string) (*x509.Certificate, crypto.PrivateKey, error) { |
| signerCertBytes, err := os.ReadFile(signerCertFile) |
| if err != nil { |
| return nil, nil, fmt.Errorf("certificate file reading failure (%v)", err) |
| } |
| |
| signerPrivBytes, err := os.ReadFile(signerPrivFile) |
| if err != nil { |
| return nil, nil, fmt.Errorf("private key file reading failure (%v)", err) |
| } |
| |
| cert, err := ParsePemEncodedCertificate(signerCertBytes) |
| if err != nil { |
| return nil, nil, fmt.Errorf("pem encoded cert parsing failure (%v)", err) |
| } |
| |
| key, err := ParsePemEncodedKey(signerPrivBytes) |
| if err != nil { |
| return nil, nil, fmt.Errorf("pem encoded key parsing failure (%v)", err) |
| } |
| |
| return cert, key, nil |
| } |
| |
| // ClockSkewGracePeriod defines the period of time a certificate will be valid before its creation. |
| // This is meant to handle cases where we have clock skew between the CA and workloads. |
| const ClockSkewGracePeriod = time.Minute * 2 |
| |
| // genCertTemplateFromCSR generates a certificate template with the given CSR. |
| // The NotBefore value of the cert is set to current time. |
| func genCertTemplateFromCSR(csr *x509.CertificateRequest, subjectIDs []string, ttl time.Duration, isCA bool) ( |
| *x509.Certificate, error) { |
| subjectIDsInString := strings.Join(subjectIDs, ",") |
| var keyUsage x509.KeyUsage |
| extKeyUsages := []x509.ExtKeyUsage{} |
| if isCA { |
| // If the cert is a CA cert, the private key is allowed to sign other certificates. |
| keyUsage = x509.KeyUsageCertSign |
| } else { |
| // Otherwise the private key is allowed for digital signature and key encipherment. |
| keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment |
| // For now, we do not differentiate non-CA certs to be used on client auth or server auth. |
| extKeyUsages = append(extKeyUsages, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth) |
| } |
| |
| // Build cert extensions with the subjectIDs. |
| ext, err := BuildSubjectAltNameExtension(subjectIDsInString) |
| if err != nil { |
| return nil, err |
| } |
| exts := []pkix.Extension{*ext} |
| |
| subject := pkix.Name{} |
| // Dual use mode if common name in CSR is not empty. |
| // In this case, set CN as determined by DualUseCommonName(subjectIDsInString). |
| if len(csr.Subject.CommonName) != 0 { |
| if cn, err := DualUseCommonName(subjectIDsInString); err != nil { |
| // log and continue |
| log.Errorf("dual-use failed for cert template - omitting CN (%v)", err) |
| } else { |
| subject.CommonName = cn |
| } |
| } |
| |
| now := time.Now() |
| |
| serialNum, err := genSerialNum() |
| if err != nil { |
| return nil, err |
| } |
| // SignatureAlgorithm will use the default algorithm. |
| // See https://golang.org/src/crypto/x509/x509.go?s=5131:5158#L1965 . |
| return &x509.Certificate{ |
| SerialNumber: serialNum, |
| Subject: subject, |
| NotBefore: now.Add(-ClockSkewGracePeriod), |
| NotAfter: now.Add(ttl), |
| KeyUsage: keyUsage, |
| ExtKeyUsage: extKeyUsages, |
| IsCA: isCA, |
| BasicConstraintsValid: true, |
| ExtraExtensions: exts, |
| }, nil |
| } |
| |
| // genCertTemplateFromoptions generates a certificate template with the given options. |
| func genCertTemplateFromOptions(options CertOptions) (*x509.Certificate, error) { |
| var keyUsage x509.KeyUsage |
| if options.IsCA { |
| // If the cert is a CA cert, the private key is allowed to sign other certificates. |
| keyUsage = x509.KeyUsageCertSign |
| } else { |
| // Otherwise the private key is allowed for digital signature and key encipherment. |
| keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment |
| } |
| |
| extKeyUsages := []x509.ExtKeyUsage{} |
| if options.IsServer { |
| extKeyUsages = append(extKeyUsages, x509.ExtKeyUsageServerAuth) |
| } |
| if options.IsClient { |
| extKeyUsages = append(extKeyUsages, x509.ExtKeyUsageClientAuth) |
| } |
| |
| notBefore := time.Now() |
| if !options.NotBefore.IsZero() { |
| notBefore = options.NotBefore |
| } |
| |
| serialNum, err := genSerialNum() |
| if err != nil { |
| return nil, err |
| } |
| |
| subject := pkix.Name{ |
| Organization: []string{options.Org}, |
| } |
| |
| exts := []pkix.Extension{} |
| if h := options.Host; len(h) > 0 { |
| s, err := BuildSubjectAltNameExtension(h) |
| if err != nil { |
| return nil, err |
| } |
| if options.IsDualUse { |
| cn, err := DualUseCommonName(h) |
| if err != nil { |
| // log and continue |
| log.Errorf("dual-use failed for cert template - omitting CN (%v)", err) |
| } else { |
| subject.CommonName = cn |
| } |
| } |
| exts = []pkix.Extension{*s} |
| } |
| |
| dnsNames := strings.Split(options.DNSNames, ",") |
| if len(dnsNames[0]) == 0 { |
| dnsNames = nil |
| } |
| |
| return &x509.Certificate{ |
| SerialNumber: serialNum, |
| Subject: subject, |
| NotBefore: notBefore, |
| NotAfter: notBefore.Add(options.TTL), |
| KeyUsage: keyUsage, |
| ExtKeyUsage: extKeyUsages, |
| IsCA: options.IsCA, |
| BasicConstraintsValid: true, |
| ExtraExtensions: exts, |
| DNSNames: dnsNames, |
| }, nil |
| } |
| |
| func genSerialNum() (*big.Int, error) { |
| serialNumLimit := new(big.Int).Lsh(big.NewInt(1), 128) |
| serialNum, err := rand.Int(rand.Reader, serialNumLimit) |
| if err != nil { |
| return nil, fmt.Errorf("serial number generation failure (%v)", err) |
| } |
| return serialNum, nil |
| } |
| |
| func encodePem(isCSR bool, csrOrCert []byte, priv interface{}, pkcs8 bool) ( |
| csrOrCertPem []byte, privPem []byte, err error) { |
| encodeMsg := "CERTIFICATE" |
| if isCSR { |
| encodeMsg = "CERTIFICATE REQUEST" |
| } |
| csrOrCertPem = pem.EncodeToMemory(&pem.Block{Type: encodeMsg, Bytes: csrOrCert}) |
| |
| var encodedKey []byte |
| if pkcs8 { |
| if encodedKey, err = x509.MarshalPKCS8PrivateKey(priv); err != nil { |
| return nil, nil, err |
| } |
| privPem = pem.EncodeToMemory(&pem.Block{Type: blockTypePKCS8PrivateKey, Bytes: encodedKey}) |
| } else { |
| switch k := priv.(type) { |
| case *rsa.PrivateKey: |
| encodedKey = x509.MarshalPKCS1PrivateKey(k) |
| privPem = pem.EncodeToMemory(&pem.Block{Type: blockTypeRSAPrivateKey, Bytes: encodedKey}) |
| case *ecdsa.PrivateKey: |
| encodedKey, err = x509.MarshalECPrivateKey(k) |
| if err != nil { |
| return nil, nil, err |
| } |
| privPem = pem.EncodeToMemory(&pem.Block{Type: blockTypeECPrivateKey, Bytes: encodedKey}) |
| } |
| } |
| err = nil |
| return |
| } |