2019-05-09 19:36:03 +00:00
/ *
Copyright 2014 The Kubernetes 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 cert
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"math"
"math/big"
"net"
2022-06-29 08:38:36 +00:00
"os"
2020-04-26 05:59:52 +00:00
"path/filepath"
2022-06-29 08:38:36 +00:00
"strconv"
2019-05-09 19:36:03 +00:00
"strings"
"time"
2019-10-08 21:56:00 +00:00
"github.com/sirupsen/logrus"
2019-05-09 19:36:03 +00:00
)
const (
rsaKeySize = 2048
duration365d = time . Hour * 24 * 365
)
2021-12-21 22:38:04 +00:00
var ErrStaticCert = errors . New ( "cannot renew static certificate" )
2020-08-11 00:06:11 +00:00
2021-12-21 22:38:04 +00:00
// Config contains the basic fields required for creating a certificate.
2019-05-09 19:36:03 +00:00
type Config struct {
CommonName string
Organization [ ] string
AltNames AltNames
Usages [ ] x509 . ExtKeyUsage
2021-12-21 22:38:04 +00:00
ExpiresAt time . Duration
2019-05-09 19:36:03 +00:00
}
// AltNames contains the domain names and IP addresses that will be added
// to the API Server's x509 certificate SubAltNames field. The values will
// be passed directly to the x509.Certificate object.
type AltNames struct {
DNSNames [ ] string
IPs [ ] net . IP
}
// NewPrivateKey creates an RSA private key
func NewPrivateKey ( ) ( * rsa . PrivateKey , error ) {
return rsa . GenerateKey ( cryptorand . Reader , rsaKeySize )
}
// NewSelfSignedCACert creates a CA certificate
func NewSelfSignedCACert ( cfg Config , key crypto . Signer ) ( * x509 . Certificate , error ) {
now := time . Now ( )
tmpl := x509 . Certificate {
SerialNumber : new ( big . Int ) . SetInt64 ( 0 ) ,
Subject : pkix . Name {
CommonName : cfg . CommonName ,
Organization : cfg . Organization ,
} ,
NotBefore : now . UTC ( ) ,
NotAfter : now . Add ( duration365d * 10 ) . UTC ( ) ,
KeyUsage : x509 . KeyUsageKeyEncipherment | x509 . KeyUsageDigitalSignature | x509 . KeyUsageCertSign ,
BasicConstraintsValid : true ,
IsCA : true ,
}
certDERBytes , err := x509 . CreateCertificate ( cryptorand . Reader , & tmpl , & tmpl , key . Public ( ) , key )
if err != nil {
return nil , err
}
2021-11-18 01:43:37 +00:00
logrus . Infof ( "generated self-signed CA certificate %s: notBefore=%s notAfter=%s" ,
tmpl . Subject , tmpl . NotBefore , tmpl . NotAfter )
2019-05-09 19:36:03 +00:00
return x509 . ParseCertificate ( certDERBytes )
}
2021-12-21 22:38:04 +00:00
// NewSignedCert creates a signed certificate using the given CA certificate and key based
// on the given configuration.
2019-05-09 19:36:03 +00:00
func NewSignedCert ( cfg Config , key crypto . Signer , caCert * x509 . Certificate , caKey crypto . Signer ) ( * x509 . Certificate , error ) {
serial , err := rand . Int ( rand . Reader , new ( big . Int ) . SetInt64 ( math . MaxInt64 ) )
if err != nil {
return nil , err
}
if len ( cfg . CommonName ) == 0 {
return nil , errors . New ( "must specify a CommonName" )
}
if len ( cfg . Usages ) == 0 {
return nil , errors . New ( "must specify at least one ExtKeyUsage" )
}
2022-06-29 08:38:36 +00:00
expiresAt := duration365d
2021-12-21 22:38:04 +00:00
if cfg . ExpiresAt > 0 {
expiresAt = time . Duration ( cfg . ExpiresAt )
} else {
2022-06-29 08:38:36 +00:00
envExpirationDays := os . Getenv ( "CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS" )
if envExpirationDays != "" {
if envExpirationDaysInt , err := strconv . Atoi ( envExpirationDays ) ; err != nil {
logrus . Infof ( "[NewSignedCert] expiration days from ENV (%s) could not be converted to int (falling back to default value: %d)" , envExpirationDays , expiresAt )
} else {
expiresAt = time . Hour * 24 * time . Duration ( envExpirationDaysInt )
}
}
2021-12-21 22:38:04 +00:00
}
2019-05-09 19:36:03 +00:00
certTmpl := x509 . Certificate {
Subject : pkix . Name {
CommonName : cfg . CommonName ,
Organization : cfg . Organization ,
} ,
DNSNames : cfg . AltNames . DNSNames ,
IPAddresses : cfg . AltNames . IPs ,
SerialNumber : serial ,
NotBefore : caCert . NotBefore ,
2021-12-21 22:38:04 +00:00
NotAfter : time . Now ( ) . Add ( expiresAt ) . UTC ( ) ,
2019-05-09 19:36:03 +00:00
KeyUsage : x509 . KeyUsageKeyEncipherment | x509 . KeyUsageDigitalSignature ,
ExtKeyUsage : cfg . Usages ,
}
certDERBytes , err := x509 . CreateCertificate ( cryptorand . Reader , & certTmpl , caCert , key . Public ( ) , caKey )
if err != nil {
return nil , err
}
2020-08-11 00:06:11 +00:00
parsedCert , err := x509 . ParseCertificate ( certDERBytes )
if err == nil {
logrus . Infof ( "certificate %s signed by %s: notBefore=%s notAfter=%s" ,
parsedCert . Subject , caCert . Subject , parsedCert . NotBefore , parsedCert . NotAfter )
}
return parsedCert , err
2019-05-09 19:36:03 +00:00
}
// MakeEllipticPrivateKeyPEM creates an ECDSA private key
func MakeEllipticPrivateKeyPEM ( ) ( [ ] byte , error ) {
privateKey , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , cryptorand . Reader )
if err != nil {
return nil , err
}
derBytes , err := x509 . MarshalECPrivateKey ( privateKey )
if err != nil {
return nil , err
}
privateKeyPemBlock := & pem . Block {
Type : ECPrivateKeyBlockType ,
Bytes : derBytes ,
}
return pem . EncodeToMemory ( privateKeyPemBlock ) , nil
}
// GenerateSelfSignedCertKey creates a self-signed certificate and key for the given host.
// Host may be an IP or a DNS name
// You may also specify additional subject alt names (either ip or dns names) for the certificate.
func GenerateSelfSignedCertKey ( host string , alternateIPs [ ] net . IP , alternateDNS [ ] string ) ( [ ] byte , [ ] byte , error ) {
return GenerateSelfSignedCertKeyWithFixtures ( host , alternateIPs , alternateDNS , "" )
}
// GenerateSelfSignedCertKeyWithFixtures creates a self-signed certificate and key for the given host.
// Host may be an IP or a DNS name. You may also specify additional subject alt names (either ip or dns names)
// for the certificate.
//
// If fixtureDirectory is non-empty, it is a directory path which can contain pre-generated certs. The format is:
// <host>_<ip>-<ip>_<alternateDNS>-<alternateDNS>.crt
// <host>_<ip>-<ip>_<alternateDNS>-<alternateDNS>.key
// Certs/keys not existing in that directory are created.
func GenerateSelfSignedCertKeyWithFixtures ( host string , alternateIPs [ ] net . IP , alternateDNS [ ] string , fixtureDirectory string ) ( [ ] byte , [ ] byte , error ) {
validFrom := time . Now ( ) . Add ( - time . Hour ) // valid an hour earlier to avoid flakes due to clock skew
maxAge := time . Hour * 24 * 365 // one year self-signed certs
baseName := fmt . Sprintf ( "%s_%s_%s" , host , strings . Join ( ipsToStrings ( alternateIPs ) , "-" ) , strings . Join ( alternateDNS , "-" ) )
2020-04-26 05:59:52 +00:00
certFixturePath := filepath . Join ( fixtureDirectory , baseName + ".crt" )
keyFixturePath := filepath . Join ( fixtureDirectory , baseName + ".key" )
2019-05-09 19:36:03 +00:00
if len ( fixtureDirectory ) > 0 {
cert , err := ioutil . ReadFile ( certFixturePath )
if err == nil {
key , err := ioutil . ReadFile ( keyFixturePath )
if err == nil {
return cert , key , nil
}
return nil , nil , fmt . Errorf ( "cert %s can be read, but key %s cannot: %v" , certFixturePath , keyFixturePath , err )
}
maxAge = 100 * time . Hour * 24 * 365 // 100 years fixtures
}
caKey , err := rsa . GenerateKey ( cryptorand . Reader , 2048 )
if err != nil {
return nil , nil , err
}
caTemplate := x509 . Certificate {
SerialNumber : big . NewInt ( 1 ) ,
Subject : pkix . Name {
CommonName : fmt . Sprintf ( "%s-ca@%d" , host , time . Now ( ) . Unix ( ) ) ,
} ,
NotBefore : validFrom ,
NotAfter : validFrom . Add ( maxAge ) ,
KeyUsage : x509 . KeyUsageKeyEncipherment | x509 . KeyUsageDigitalSignature | x509 . KeyUsageCertSign ,
BasicConstraintsValid : true ,
IsCA : true ,
}
caDERBytes , err := x509 . CreateCertificate ( cryptorand . Reader , & caTemplate , & caTemplate , & caKey . PublicKey , caKey )
if err != nil {
return nil , nil , err
}
caCertificate , err := x509 . ParseCertificate ( caDERBytes )
if err != nil {
return nil , nil , err
}
priv , err := rsa . GenerateKey ( cryptorand . Reader , 2048 )
if err != nil {
return nil , nil , err
}
template := x509 . Certificate {
SerialNumber : big . NewInt ( 2 ) ,
Subject : pkix . Name {
CommonName : fmt . Sprintf ( "%s@%d" , host , time . Now ( ) . Unix ( ) ) ,
} ,
NotBefore : validFrom ,
NotAfter : validFrom . Add ( maxAge ) ,
KeyUsage : x509 . KeyUsageKeyEncipherment | x509 . KeyUsageDigitalSignature ,
ExtKeyUsage : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageServerAuth } ,
BasicConstraintsValid : true ,
}
if ip := net . ParseIP ( host ) ; ip != nil {
template . IPAddresses = append ( template . IPAddresses , ip )
} else {
template . DNSNames = append ( template . DNSNames , host )
}
template . IPAddresses = append ( template . IPAddresses , alternateIPs ... )
template . DNSNames = append ( template . DNSNames , alternateDNS ... )
derBytes , err := x509 . CreateCertificate ( cryptorand . Reader , & template , caCertificate , & priv . PublicKey , caKey )
if err != nil {
return nil , nil , err
}
// Generate cert, followed by ca
certBuffer := bytes . Buffer { }
if err := pem . Encode ( & certBuffer , & pem . Block { Type : CertificateBlockType , Bytes : derBytes } ) ; err != nil {
return nil , nil , err
}
if err := pem . Encode ( & certBuffer , & pem . Block { Type : CertificateBlockType , Bytes : caDERBytes } ) ; err != nil {
return nil , nil , err
}
// Generate key
keyBuffer := bytes . Buffer { }
if err := pem . Encode ( & keyBuffer , & pem . Block { Type : RSAPrivateKeyBlockType , Bytes : x509 . MarshalPKCS1PrivateKey ( priv ) } ) ; err != nil {
return nil , nil , err
}
if len ( fixtureDirectory ) > 0 {
if err := ioutil . WriteFile ( certFixturePath , certBuffer . Bytes ( ) , 0644 ) ; err != nil {
return nil , nil , fmt . Errorf ( "failed to write cert fixture to %s: %v" , certFixturePath , err )
}
if err := ioutil . WriteFile ( keyFixturePath , keyBuffer . Bytes ( ) , 0644 ) ; err != nil {
return nil , nil , fmt . Errorf ( "failed to write key fixture to %s: %v" , certFixturePath , err )
}
}
return certBuffer . Bytes ( ) , keyBuffer . Bytes ( ) , nil
}
func ipsToStrings ( ips [ ] net . IP ) [ ] string {
ss := make ( [ ] string , 0 , len ( ips ) )
for _ , ip := range ips {
ss = append ( ss , ip . String ( ) )
}
return ss
}
2019-10-08 21:56:00 +00:00
// IsCertExpired checks if the certificate about to expire
2020-08-11 00:06:11 +00:00
func IsCertExpired ( cert * x509 . Certificate , days int ) bool {
2019-10-08 21:56:00 +00:00
expirationDate := cert . NotAfter
2020-08-11 00:06:11 +00:00
diffDays := time . Until ( expirationDate ) . Hours ( ) / 24.0
if diffDays <= float64 ( days ) {
logrus . Infof ( "certificate %s will expire in %f days at %s" , cert . Subject , diffDays , cert . NotAfter )
2019-10-08 21:56:00 +00:00
return true
}
return false
}