blob: 490732b74811e5d9f03aac7291e927b24b49dac3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 chiron
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/apache/dubbo-kubernetes/pkg/util/ptr"
"os"
"time"
cert "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
clientset "k8s.io/client-go/kubernetes"
"github.com/apache/dubbo-kubernetes/dubbod/security/pkg/pki/util"
dubbolog "github.com/apache/dubbo-kubernetes/pkg/log"
)
var log = dubbolog.RegisterScope("chiron", "chiron debugging")
const (
// The size of a private key for a leaf certificate.
keySize = 2048
)
var certWatchTimeout = 60 * time.Second
// GenKeyCertK8sCA : Generates a key pair and gets public certificate signed by K8s_CA
// Options are meant to sign DNS certs
// 1. Generate a CSR
// 2. Call SignCSRK8s to finish rest of the flow
func GenKeyCertK8sCA(client clientset.Interface, dnsName, caFilePath string, signerName string, approveCsr bool, requestedLifetime time.Duration) ([]byte, []byte, []byte, error) {
// 1. Generate a CSR
options := util.CertOptions{
Host: dnsName,
RSAKeySize: keySize,
IsDualUse: false,
PKCS8Key: false,
}
csrPEM, keyPEM, err := util.GenCSR(options)
if err != nil {
log.Errorf("CSR generation error (%v)", err)
return nil, nil, nil, err
}
usages := []cert.KeyUsage{
cert.UsageDigitalSignature,
cert.UsageKeyEncipherment,
cert.UsageServerAuth,
}
if signerName == "" {
return nil, nil, nil, fmt.Errorf("signerName is required for Kubernetes CA")
}
certChain, caCert, err := SignCSRK8s(client, csrPEM, signerName, usages, dnsName, caFilePath, approveCsr, true, requestedLifetime)
return certChain, keyPEM, caCert, err
}
// SignCSRK8s generates a certificate from CSR using the K8s CA
// 1. Submit a CSR
// 2. Approve a CSR
// 3. Read the signed certificate
// 4. Clean up the artifacts (e.g., delete CSR)
func SignCSRK8s(client clientset.Interface, csrData []byte, signerName string, usages []cert.KeyUsage, dnsName, caFilePath string, approveCsr, appendCaCert bool, requestedLifetime time.Duration) ([]byte, []byte, error) {
// 1. Submit the CSR
csr, err := submitCSR(client, csrData, signerName, usages, requestedLifetime)
if err != nil {
return nil, nil, err
}
log.Debugf("CSR (%v) has been created", csr.Name)
// clean up certificate request after deletion
defer func() {
_ = cleanupCSR(client, csr)
}()
// 2. Approve the CSR
if approveCsr {
approvalMessage := fmt.Sprintf("CSR (%s) for the certificate (%s) is approved", csr.Name, dnsName)
err = approveCSR(client, csr, approvalMessage)
if err != nil {
return nil, nil, fmt.Errorf("failed to approve CSR request: %v", err)
}
log.Debugf("CSR (%v) is approved", csr.Name)
}
// 3. Read the signed certificate
certChain, caCert, err := readSignedCertificate(client, csr, certWatchTimeout, caFilePath, appendCaCert)
if err != nil {
return nil, nil, err
}
// If there is a failure of cleaning up CSR, the error is returned.
return certChain, caCert, err
}
// Read CA certificate and check whether it is a valid certificate.
func readCACert(caCertPath string) ([]byte, error) {
caCert, err := os.ReadFile(caCertPath)
if err != nil {
log.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err)
return nil, fmt.Errorf("failed to read CA cert, cert. path: %v, error: %v", caCertPath, err)
}
b, _ := pem.Decode(caCert)
if b == nil {
return nil, fmt.Errorf("could not decode pem")
}
if b.Type != "CERTIFICATE" {
return nil, fmt.Errorf("ca certificate contains wrong type: %v", b.Type)
}
if _, err := x509.ParseCertificate(b.Bytes); err != nil {
return nil, fmt.Errorf("ca certificate parsing returns an error: %v", err)
}
return caCert, nil
}
func submitCSR(client clientset.Interface, csrData []byte, signerName string, usages []cert.KeyUsage, requestedLifetime time.Duration) (*cert.CertificateSigningRequest, error) {
log.Debugf("create CSR for signer %v", signerName)
csr := &cert.CertificateSigningRequest{
// Username, UID, Groups will be injected by API server.
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
ObjectMeta: metav1.ObjectMeta{
GenerateName: "csr-workload-",
},
Spec: cert.CertificateSigningRequestSpec{
Request: csrData,
Usages: usages,
SignerName: signerName,
},
}
if requestedLifetime != time.Duration(0) {
csr.Spec.ExpirationSeconds = ptr.Of(int32(requestedLifetime.Seconds()))
}
resp, err := client.CertificatesV1().CertificateSigningRequests().Create(context.Background(), csr, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("failed to create CSR: %v", err)
}
return resp, nil
}
func approveCSR(client clientset.Interface, csr *cert.CertificateSigningRequest, approvalMessage string) error {
csr.Status.Conditions = append(csr.Status.Conditions, cert.CertificateSigningRequestCondition{
Type: cert.CertificateApproved,
Reason: approvalMessage,
Message: approvalMessage,
Status: corev1.ConditionTrue,
})
_, err := client.CertificatesV1().CertificateSigningRequests().UpdateApproval(context.TODO(), csr.Name, csr, metav1.UpdateOptions{})
if err != nil {
log.Errorf("failed to approve CSR (%v): %v", csr.Name, err)
return err
}
return nil
}
// Read the signed certificate
// verify and append CA certificate to certChain if appendCaCert is true
func readSignedCertificate(client clientset.Interface, csr *cert.CertificateSigningRequest, watchTimeout time.Duration, caCertPath string, appendCaCert bool) ([]byte, []byte, error) {
// First try to read the signed CSR through a watching mechanism
certPEM, err := readSignedCsr(client, csr.Name, watchTimeout)
if err != nil {
return nil, nil, err
}
if len(certPEM) == 0 {
return nil, nil, fmt.Errorf("no certificate returned for the CSR: %q", csr.Name)
}
certsParsed, _, err := util.ParsePemEncodedCertificateChain(certPEM)
if err != nil {
return nil, nil, fmt.Errorf("decoding certificate failed")
}
if !appendCaCert || caCertPath == "" {
return certPEM, nil, nil
}
caCert, err := readCACert(caCertPath)
if err != nil {
return nil, nil, fmt.Errorf("error when retrieving CA cert: (%v)", err)
}
// Verify the certificate chain before returning the certificate
roots := x509.NewCertPool()
if roots == nil {
return nil, nil, fmt.Errorf("failed to create cert pool")
}
if ok := roots.AppendCertsFromPEM(caCert); !ok {
return nil, nil, fmt.Errorf("failed to append CA certificate")
}
intermediates := x509.NewCertPool()
if len(certsParsed) > 1 {
for _, cert := range certsParsed[1:] {
intermediates.AddCert(cert)
}
}
_, err = certsParsed[0].Verify(x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to verify the certificate chain: %v", err)
}
return append(certPEM, caCert...), caCert, nil
}
// Return signed CSR through a watcher. If no CSR is read, return nil.
func readSignedCsr(client clientset.Interface, csr string, watchTimeout time.Duration) ([]byte, error) {
selector := fields.OneTermEqualSelector("metadata.name", csr).String()
// Setup a List+Watch, like informers do
// A simple Watch will fail if the cert is signed too quickly
l, _ := client.CertificatesV1().CertificateSigningRequests().List(context.Background(), metav1.ListOptions{
FieldSelector: selector,
})
if l != nil && len(l.Items) > 0 {
reqSigned := l.Items[0]
if reqSigned.Status.Certificate != nil {
return reqSigned.Status.Certificate, nil
}
}
var rv string
if l != nil {
rv = l.ResourceVersion
}
watcher, err := client.CertificatesV1().CertificateSigningRequests().Watch(context.Background(), metav1.ListOptions{
ResourceVersion: rv,
FieldSelector: selector,
})
if err != nil {
return nil, fmt.Errorf("failed to watch CSR %v", csr)
}
defer watcher.Stop()
// Set a timeout
timer := time.After(watchTimeout)
for {
select {
case r := <-watcher.ResultChan():
reqSigned := r.Object.(*cert.CertificateSigningRequest)
if reqSigned.Status.Certificate != nil {
return reqSigned.Status.Certificate, nil
}
case <-timer:
return nil, fmt.Errorf("timeout when watching CSR %v", csr)
}
}
}
// Clean up the CSR
func cleanupCSR(client clientset.Interface, csr *cert.CertificateSigningRequest) error {
err := client.CertificatesV1().CertificateSigningRequests().Delete(context.TODO(), csr.Name, metav1.DeleteOptions{})
if err != nil {
log.Errorf("failed to delete CSR (%v): %v", csr.Name, err)
} else {
log.Debugf("deleted CSR: %v", csr.Name)
}
return err
}