500 lines
15 KiB
Go
500 lines
15 KiB
Go
/*
|
||
Copyright 2023 The Karmada 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 certs
|
||
|
||
import (
|
||
"crypto"
|
||
"crypto/ecdsa"
|
||
"crypto/elliptic"
|
||
cryptorand "crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/x509"
|
||
"crypto/x509/pkix"
|
||
"encoding/pem"
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"math/big"
|
||
"net"
|
||
"time"
|
||
|
||
"k8s.io/apimachinery/pkg/util/sets"
|
||
"k8s.io/apimachinery/pkg/util/validation"
|
||
certutil "k8s.io/client-go/util/cert"
|
||
"k8s.io/client-go/util/keyutil"
|
||
netutils "k8s.io/utils/net"
|
||
|
||
operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
|
||
"github.com/karmada-io/karmada/operator/pkg/constants"
|
||
"github.com/karmada-io/karmada/operator/pkg/util"
|
||
)
|
||
|
||
const (
|
||
// CertificateBlockType is a possible value for pem.Block.Type.
|
||
CertificateBlockType = "CERTIFICATE"
|
||
rsaKeySize = 3072
|
||
keyExtension = ".key"
|
||
certExtension = ".crt"
|
||
)
|
||
|
||
// AltNamesMutatorConfig is a config to AltNamesMutator. It includes necessary
|
||
// configs to AltNamesMutator.
|
||
type AltNamesMutatorConfig struct {
|
||
Name string
|
||
Namespace string
|
||
ControlplaneAddress string
|
||
Components *operatorv1alpha1.KarmadaComponents
|
||
}
|
||
|
||
type altNamesMutatorFunc func(*AltNamesMutatorConfig, *CertConfig) error
|
||
|
||
// CertConfig represents a config to generate certificate by karmada.
|
||
type CertConfig struct {
|
||
Name string
|
||
CAName string
|
||
NotAfter *time.Time
|
||
PublicKeyAlgorithm x509.PublicKeyAlgorithm // TODO: All public key of karmada cert use the RSA algorithm by default
|
||
Config certutil.Config
|
||
AltNamesMutatorFunc altNamesMutatorFunc
|
||
}
|
||
|
||
func (config *CertConfig) defaultPublicKeyAlgorithm() {
|
||
if config.PublicKeyAlgorithm == x509.UnknownPublicKeyAlgorithm {
|
||
config.PublicKeyAlgorithm = x509.RSA
|
||
}
|
||
}
|
||
|
||
func (config *CertConfig) defaultNotAfter() {
|
||
if config.NotAfter == nil {
|
||
notAfter := time.Now().Add(constants.CertificateValidity).UTC()
|
||
config.NotAfter = ¬After
|
||
}
|
||
}
|
||
|
||
// GetDefaultCertList returns all of karmada certConfigs, it include karmada, front and etcd.
|
||
func GetDefaultCertList() []*CertConfig {
|
||
return []*CertConfig{
|
||
// karmada cert config.
|
||
KarmadaCertRootCA(),
|
||
KarmadaCertAdmin(),
|
||
KarmadaCertApiserver(),
|
||
// front proxy cert config.
|
||
KarmadaCertFrontProxyCA(),
|
||
KarmadaCertFrontProxyClient(),
|
||
// ETCD cert config.
|
||
KarmadaCertEtcdCA(),
|
||
KarmadaCertEtcdServer(),
|
||
KarmadaCertEtcdClient(),
|
||
}
|
||
}
|
||
|
||
// KarmadaCertRootCA returns karmada ca cert config.
|
||
func KarmadaCertRootCA() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.CaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "karmada",
|
||
},
|
||
}
|
||
}
|
||
|
||
// KarmadaCertAdmin returns karmada client cert config.
|
||
func KarmadaCertAdmin() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.KarmadaCertAndKeyName,
|
||
CAName: constants.CaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "system:admin",
|
||
Organization: []string{"system:masters"},
|
||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||
},
|
||
AltNamesMutatorFunc: makeAltNamesMutator(apiServerAltNamesMutator),
|
||
}
|
||
}
|
||
|
||
// KarmadaCertApiserver returns karmada apiserver cert config.
|
||
func KarmadaCertApiserver() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.ApiserverCertAndKeyName,
|
||
CAName: constants.CaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "karmada-apiserver",
|
||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||
},
|
||
AltNamesMutatorFunc: makeAltNamesMutator(apiServerAltNamesMutator),
|
||
}
|
||
}
|
||
|
||
// KarmadaCertClient returns karmada client cert config.
|
||
func KarmadaCertClient() *CertConfig {
|
||
return &CertConfig{
|
||
Name: "karmada-client",
|
||
CAName: constants.CaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "system:admin",
|
||
Organization: []string{"system:masters"},
|
||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||
},
|
||
AltNamesMutatorFunc: makeAltNamesMutator(apiServerAltNamesMutator),
|
||
}
|
||
}
|
||
|
||
// KarmadaCertFrontProxyCA returns karmada front proxy cert config.
|
||
func KarmadaCertFrontProxyCA() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.FrontProxyCaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "front-proxy-ca",
|
||
},
|
||
}
|
||
}
|
||
|
||
// KarmadaCertFrontProxyClient returns karmada front proxy client cert config.
|
||
func KarmadaCertFrontProxyClient() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.FrontProxyClientCertAndKeyName,
|
||
CAName: constants.FrontProxyCaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "front-proxy-client",
|
||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||
},
|
||
}
|
||
}
|
||
|
||
// KarmadaCertEtcdCA returns karmada front proxy client cert config.
|
||
func KarmadaCertEtcdCA() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.EtcdCaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "karmada-etcd-ca",
|
||
},
|
||
}
|
||
}
|
||
|
||
// KarmadaCertEtcdServer returns etcd server cert config.
|
||
func KarmadaCertEtcdServer() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.EtcdServerCertAndKeyName,
|
||
CAName: constants.EtcdCaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "karmada-etcd-server",
|
||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||
},
|
||
AltNamesMutatorFunc: makeAltNamesMutator(etcdServerAltNamesMutator),
|
||
}
|
||
}
|
||
|
||
// KarmadaCertEtcdClient returns etcd client cert config.
|
||
func KarmadaCertEtcdClient() *CertConfig {
|
||
return &CertConfig{
|
||
Name: constants.EtcdClientCertAndKeyName,
|
||
CAName: constants.EtcdCaCertAndKeyName,
|
||
Config: certutil.Config{
|
||
CommonName: "karmada-etcd-client",
|
||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||
},
|
||
}
|
||
}
|
||
|
||
// KarmadaCert is karmada certificate, it includes certificate basic message.
|
||
// we can directly get the byte array of certificate key and cert from the object.
|
||
type KarmadaCert struct {
|
||
pairName string
|
||
caName string
|
||
cert []byte
|
||
key []byte
|
||
}
|
||
|
||
// CertData returns certificate cert data.
|
||
func (cert *KarmadaCert) CertData() []byte {
|
||
return cert.cert
|
||
}
|
||
|
||
// KeyData returns certificate key data.
|
||
func (cert *KarmadaCert) KeyData() []byte {
|
||
return cert.key
|
||
}
|
||
|
||
// CertName returns cert file name. its default suffix is ".crt".
|
||
func (cert *KarmadaCert) CertName() string {
|
||
pair := cert.pairName
|
||
if len(pair) == 0 {
|
||
pair = "cert"
|
||
}
|
||
return pair + certExtension
|
||
}
|
||
|
||
// KeyName returns cert key file name. its default suffix is ".key".
|
||
func (cert *KarmadaCert) KeyName() string {
|
||
pair := cert.pairName
|
||
if len(pair) == 0 {
|
||
pair = "cert"
|
||
}
|
||
return pair + keyExtension
|
||
}
|
||
|
||
// GeneratePrivateKey generates cert key with default size if 1024. it supports
|
||
// ECDSA and RAS algorithm.
|
||
func GeneratePrivateKey(keyType x509.PublicKeyAlgorithm) (crypto.Signer, error) {
|
||
if keyType == x509.ECDSA {
|
||
return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
|
||
}
|
||
|
||
return rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
|
||
}
|
||
|
||
// NewCertificateAuthority creates new certificate and private key for the certificate authority
|
||
func NewCertificateAuthority(cc *CertConfig) (*KarmadaCert, error) {
|
||
cc.defaultPublicKeyAlgorithm()
|
||
|
||
key, err := GeneratePrivateKey(cc.PublicKeyAlgorithm)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("unable to create private key while generating CA certificate, err: %w", err)
|
||
}
|
||
|
||
cert, err := certutil.NewSelfSignedCACert(cc.Config, key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("unable to create self-signed CA certificate, err: %w", err)
|
||
}
|
||
|
||
encoded, err := keyutil.MarshalPrivateKeyToPEM(key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("unable to marshal private key to PEM, err: %w", err)
|
||
}
|
||
|
||
return &KarmadaCert{
|
||
pairName: cc.Name,
|
||
caName: cc.CAName,
|
||
cert: EncodeCertPEM(cert),
|
||
key: encoded,
|
||
}, nil
|
||
}
|
||
|
||
// CreateCertAndKeyFilesWithCA loads the given certificate authority from disk, then generates and writes out the given certificate and key.
|
||
// The certSpec and caCertSpec should both be one of the variables from this package.
|
||
func CreateCertAndKeyFilesWithCA(cc *CertConfig, caCertData, caKeyData []byte) (*KarmadaCert, error) {
|
||
if len(cc.Config.Usages) == 0 {
|
||
return nil, fmt.Errorf("must specify at least one ExtKeyUsage")
|
||
}
|
||
|
||
cc.defaultNotAfter()
|
||
cc.defaultPublicKeyAlgorithm()
|
||
|
||
key, err := GeneratePrivateKey(cc.PublicKeyAlgorithm)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("unable to create private key, err: %w", err)
|
||
}
|
||
|
||
caCerts, err := certutil.ParseCertsPEM(caCertData)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
caKey, err := ParsePrivateKeyPEM(caKeyData)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Safely pick the first one because the sender's certificate must come first in the list.
|
||
// For details, see: https://www.rfc-editor.org/rfc/rfc4346#section-7.4.2
|
||
caCert := caCerts[0]
|
||
|
||
cert, err := NewSignedCert(cc, key, caCert, caKey, false)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
encoded, err := keyutil.MarshalPrivateKeyToPEM(key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("unable to marshal private key to PEM, err: %w", err)
|
||
}
|
||
|
||
return &KarmadaCert{
|
||
pairName: cc.Name,
|
||
caName: cc.CAName,
|
||
cert: EncodeCertPEM(cert),
|
||
key: encoded,
|
||
}, nil
|
||
}
|
||
|
||
// NewSignedCert creates a signed certificate using the given CA certificate and key
|
||
func NewSignedCert(cc *CertConfig, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, isCA bool) (*x509.Certificate, error) {
|
||
serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(cc.Config.CommonName) == 0 {
|
||
return nil, fmt.Errorf("must specify a CommonName")
|
||
}
|
||
|
||
keyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature
|
||
if isCA {
|
||
keyUsage |= x509.KeyUsageCertSign
|
||
}
|
||
|
||
RemoveDuplicateAltNames(&cc.Config.AltNames)
|
||
|
||
certTmpl := x509.Certificate{
|
||
Subject: pkix.Name{
|
||
CommonName: cc.Config.CommonName,
|
||
Organization: cc.Config.Organization,
|
||
},
|
||
DNSNames: cc.Config.AltNames.DNSNames,
|
||
IPAddresses: cc.Config.AltNames.IPs,
|
||
SerialNumber: serial,
|
||
NotBefore: caCert.NotBefore,
|
||
NotAfter: cc.NotAfter.UTC(),
|
||
KeyUsage: keyUsage,
|
||
ExtKeyUsage: cc.Config.Usages,
|
||
BasicConstraintsValid: true,
|
||
IsCA: isCA,
|
||
}
|
||
certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &certTmpl, caCert, key.Public(), caKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return x509.ParseCertificate(certDERBytes)
|
||
}
|
||
|
||
// RemoveDuplicateAltNames removes duplicate items in altNames.
|
||
func RemoveDuplicateAltNames(altNames *certutil.AltNames) {
|
||
if altNames == nil {
|
||
return
|
||
}
|
||
|
||
if altNames.DNSNames != nil {
|
||
altNames.DNSNames = sets.NewString(altNames.DNSNames...).List()
|
||
}
|
||
|
||
ipsKeys := make(map[string]struct{})
|
||
var ips []net.IP
|
||
for _, one := range altNames.IPs {
|
||
if _, ok := ipsKeys[one.String()]; !ok {
|
||
ipsKeys[one.String()] = struct{}{}
|
||
ips = append(ips, one)
|
||
}
|
||
}
|
||
altNames.IPs = ips
|
||
}
|
||
|
||
func appendSANsToAltNames(altNames *certutil.AltNames, SANs []string) {
|
||
for _, altname := range SANs {
|
||
if ip := netutils.ParseIPSloppy(altname); ip != nil {
|
||
altNames.IPs = append(altNames.IPs, ip)
|
||
} else if len(validation.IsDNS1123Subdomain(altname)) == 0 {
|
||
altNames.DNSNames = append(altNames.DNSNames, altname)
|
||
} else if len(validation.IsWildcardDNS1123Subdomain(altname)) == 0 {
|
||
altNames.DNSNames = append(altNames.DNSNames, altname)
|
||
}
|
||
}
|
||
}
|
||
|
||
// EncodeCertPEM returns PEM-encoded certificate data
|
||
func EncodeCertPEM(cert *x509.Certificate) []byte {
|
||
block := pem.Block{
|
||
Type: CertificateBlockType,
|
||
Bytes: cert.Raw,
|
||
}
|
||
return pem.EncodeToMemory(&block)
|
||
}
|
||
|
||
// ParsePrivateKeyPEM parses crypto.Signer from byte array. the key
|
||
// must be encryption by ECDSA and RAS.
|
||
func ParsePrivateKeyPEM(keyData []byte) (crypto.Signer, error) {
|
||
caPrivateKey, err := keyutil.ParsePrivateKeyPEM(keyData)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Allow RSA and ECDSA formats only
|
||
var key crypto.Signer
|
||
switch k := caPrivateKey.(type) {
|
||
case *rsa.PrivateKey:
|
||
key = k
|
||
case *ecdsa.PrivateKey:
|
||
key = k
|
||
default:
|
||
return nil, errors.New("the private key is neither in RSA nor ECDSA format")
|
||
}
|
||
|
||
return key, nil
|
||
}
|
||
|
||
func makeAltNamesMutator(f func(cfg *AltNamesMutatorConfig) (*certutil.AltNames, error)) altNamesMutatorFunc {
|
||
return func(cfg *AltNamesMutatorConfig, cc *CertConfig) error {
|
||
altNames, err := f(cfg)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
cc.Config.AltNames = *altNames
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func etcdServerAltNamesMutator(cfg *AltNamesMutatorConfig) (*certutil.AltNames, error) {
|
||
etcdClientServiceDNS := fmt.Sprintf("%s.%s.svc.cluster.local", util.KarmadaEtcdClientName(cfg.Name), cfg.Namespace)
|
||
etcdPeerServiceDNS := fmt.Sprintf("*.%s.%s.svc.cluster.local", util.KarmadaEtcdName(cfg.Name), cfg.Namespace)
|
||
|
||
altNames := &certutil.AltNames{
|
||
DNSNames: []string{"localhost", etcdClientServiceDNS, etcdPeerServiceDNS},
|
||
IPs: []net.IP{net.IPv4(127, 0, 0, 1)},
|
||
}
|
||
|
||
if cfg.Components.Etcd.Local != nil {
|
||
appendSANsToAltNames(altNames, cfg.Components.Etcd.Local.ServerCertSANs)
|
||
}
|
||
|
||
return altNames, nil
|
||
}
|
||
|
||
func apiServerAltNamesMutator(cfg *AltNamesMutatorConfig) (*certutil.AltNames, error) {
|
||
altNames := &certutil.AltNames{
|
||
DNSNames: []string{
|
||
"localhost",
|
||
"kubernetes",
|
||
"kubernetes.default",
|
||
"kubernetes.default.svc",
|
||
fmt.Sprintf("*.%s.svc.cluster.local", constants.KarmadaSystemNamespace),
|
||
fmt.Sprintf("*.%s.svc", constants.KarmadaSystemNamespace),
|
||
},
|
||
IPs: []net.IP{
|
||
net.IPv4(127, 0, 0, 1),
|
||
},
|
||
}
|
||
|
||
// When deploying a karmada under a namespace other than 'karmada-system', like 'test', there are two scenarios below:
|
||
// 1.When karmada-apiserver access APIService, the cert of 'karmada-demo-aggregated-apiserver' will be verified to see
|
||
// if its altNames contains 'karmada-demo-aggregated-apiserver.karmada-system.svc';
|
||
// 2.When karmada-apiserver access webhook, the cert of 'karmada-demo-webhook' will be verified to see
|
||
// if its altNames contains 'karmada-demo-webhook.test.svc'.
|
||
// Therefore, the certificate's altNames should contain both 'karmada-system.svc.cluster.local' and 'test.svc.cluster.local'.
|
||
if cfg.Namespace != constants.KarmadaSystemNamespace {
|
||
appendSANsToAltNames(altNames, []string{fmt.Sprintf("*.%s.svc.cluster.local", cfg.Namespace),
|
||
fmt.Sprintf("*.%s.svc", cfg.Namespace)})
|
||
}
|
||
|
||
if len(cfg.Components.KarmadaAPIServer.CertSANs) > 0 {
|
||
appendSANsToAltNames(altNames, cfg.Components.KarmadaAPIServer.CertSANs)
|
||
}
|
||
if len(cfg.ControlplaneAddress) > 0 {
|
||
appendSANsToAltNames(altNames, []string{cfg.ControlplaneAddress})
|
||
}
|
||
|
||
return altNames, nil
|
||
}
|