pkg/mesh/certs.go (209 lines of code) (raw):
package mesh
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// WIP - consolidate cert signing, not require pilot-agent for proxyless gRPC.
// TODO: rotation
// TODO: save last cert in the chain to roots
// TODO: only use CAS if mesh-env is configured
const (
blockTypeECPrivateKey = "EC PRIVATE KEY"
blockTypeRSAPrivateKey = "RSA PRIVATE KEY" // PKCS#1 private key
blockTypePKCS8PrivateKey = "PRIVATE KEY" // PKCS#8 plain private key
)
// Common setup for cert management.
// After the 'mesh-env' is loaded (from env, k8s, URL) the next step is to init the workload identity.
// This must happen before connecting to XDS - since certs is one of the possible auth methods.
//
// The logic is:
// - (best case) certificates already provisioned by platform. Detects GKE paths (CAS), old Istio, CertManager style
// If workload certs are platform-provisioned: extract trust domain, namespace, name, pod id from cert.
//
// - Detect the WORKLOAD_SERVICE_ACCOUNT, trust domain from JWT or mesh-env
// - Use WORKLOAD_CERT json to load the config for the CSR, create a CSR
// - Call CSRSigner.
// - Save the certificates if running as root or an output dir is set. This will use CAS naming convention.
//
// If envoy + pilot-agent are used, they should be configured to use the cert files.
// This is done by setting "CA_PROVIDER=GoogleGkeWorkloadCertificate" when starting pilot-agent
func (kr *KRun) InitCertificates(ctx context.Context, outDir string) error {
var err error
keyFile := filepath.Join(outDir, privateKey)
chainFile := filepath.Join(outDir, cert)
if outDir != "" {
kp, err := tls.LoadX509KeyPair(chainFile, keyFile)
if err == nil && len(kp.Certificate) > 0 {
kp.Leaf, _ = x509.ParseCertificate(kp.Certificate[0])
exp := kp.Leaf.NotAfter.Sub(time.Now())
if exp > -5 * time.Minute {
kr.X509KeyPair = &kp
log.Println("Existing Cert", "expires", exp)
return nil
}
}
}
if kr.CSRSigner == nil {
return nil
}
// TODO: decode WorkloadCertificateConfig, use EC256 or RSA
privPEM, csr, err := kr.NewCSR("rsa", kr.TrustDomain, "spiffe://"+kr.TrustDomain+"/ns/"+kr.Namespace+"/sa/"+kr.KSA)
if err != nil {
return err
}
chain, err := kr.CSRSigner.CSRSign(ctx, csr, 24*3600)
if err != nil {
return err
}
certChain := strings.Join(chain, "\n")
kp, err := tls.X509KeyPair([]byte(certChain), privPEM)
kr.X509KeyPair = &kp
if err == nil && len(kp.Certificate) > 0 {
kp.Leaf, _ = x509.ParseCertificate(kp.Certificate[0])
if !kp.Leaf.NotAfter.Before(time.Now()) {
r, _ := x509.ParseCertificate(kp.Certificate[len(kp.Certificate) - 1])
log.Println("New Cert", "expires", kp.Leaf.NotAfter, "signer", r.Subject)
}
}
if outDir != "" {
os.MkdirAll(outDir, 0755)
err = ioutil.WriteFile(keyFile, privPEM, 0660)
if err != nil {
return err
}
err = ioutil.WriteFile(chainFile, []byte(certChain), 0660)
if err != nil {
return err
}
if os.Getuid() == 0 {
os.Chown(outDir, 1337, 1337)
os.Chown(keyFile, 1337, 1337)
os.Chown(chainFile, 1337, 1337)
}
}
// The roots are extracted from the mesh env.
return err
}
// InitRoots will find the mesh roots.
//
// - if Zatar or another CSI provider are enabled, we do nothing - Zatar config is the root of trust for everything
// - otherwise the roots are expected to be part of mesh-env. The mesh connector or other tools will
// populate it - ideally from the CSI/Zatar or TrustConfig CRD.
func (kr *KRun) InitRoots(ctx context.Context, outDir string) error {
rootFile := filepath.Join(outDir, WorkloadRootCAs)
if outDir != "" {
rootCertPEM, err := ioutil.ReadFile(rootFile)
if err == nil {
block, rest := pem.Decode(rootCertPEM)
var blockBytes []byte
for block != nil {
blockBytes = append(blockBytes, block.Bytes...)
block, rest = pem.Decode(rest)
}
rootCAs, err := x509.ParseCertificates(blockBytes)
if err != nil {
return err
}
for _, c := range rootCAs {
kr.TrustedCertPool.AddCert(c)
}
return nil
}
}
// File not found - extract it from mesh env, and save it.
// This includes Citadel root (if active in the mesh) or other roots.
roots := ""
for k, v := range kr.MeshEnv {
if strings.HasPrefix(k, "CAROOT") {
roots = roots + "\n" + v
}
}
block, rest := pem.Decode([]byte(roots))
var blockBytes []byte
for block != nil {
blockBytes = append(blockBytes, block.Bytes...)
block, rest = pem.Decode(rest)
}
rootCAs, err := x509.ParseCertificates(blockBytes)
if err != nil {
return err
}
for _, c := range rootCAs {
kr.TrustedCertPool.AddCert(c)
}
if outDir != "" {
os.MkdirAll(outDir, 0660)
err = ioutil.WriteFile(rootFile, []byte(roots), 0644)
if err != nil {
return err
}
}
return nil
}
type CSRSigner interface {
CSRSign(ctx context.Context, csrPEM []byte, certValidTTLInSec int64) ([]string, error)
}
const (
WorkloadCertDir = "./var/run/secrets/workload-spiffe-credentials"
// Different from typical Istio and CertManager key.pem - we can check both
privateKey = "private_key.pem"
// Also different, we'll check all. CertManager uses cert.pem
cert = "certificates.pem"
// This is derived from CA certs plus all TrustAnchors.
// In GKE, it is expected that Citadel roots will be configure using TrustConfig - so they are visible
// to all workloads including TD proxyless GRPC.
//
// Outside of GKE, this is loaded from the mesh.env - the mesh gate is responsible to keep it up to date.
WorkloadRootCAs = "ca_certificates.pem"
)
type WorkloadCertificateConfigSpec struct {
CertificateAuthorityConfig CertificateAuthorityConfig `json:"certificateAuthorityConfig"`
ValidityDurationSeconds int64 `json:"validityDurationSeconds,omitempty"`
RotationWindowPercentage int64 `json:"rotationWindowPercentage,omitempty"`
KeyAlgorithm *KeyAlgorithm `json:"keyAlgorithm,omitempty"`
}
type CertificateAuthorityConfig struct {
MeshCAConfig *MeshCAConfig `json:"meshCAConfig,omitempty"`
CertificateAuthorityServiceConfig *CertificateAuthorityServiceConfig `json:"certificateAuthorityServiceConfig,omitempty"`
}
type MeshCAConfig struct {
}
type CertificateAuthorityServiceConfig struct {
// Format: //privateca.googleapis.com/projects/PROJECT_ID/locations/SUBORDINATE_CA_LOCATION/caPools/SUBORDINATE_CA_POOL_NAME
EndpointURI string `json:"endpointURI"`
}
type KeyAlgorithm struct {
RSA *RSA `json:"rsa,omitempty"`
ECDSA *ECDSA `json:"ecdsa,omitempty"`
}
type RSA struct {
ModulusSize int `json:"modulusSize"`
}
type ECDSA struct {
Curve string `json:"curve"`
}
// TrustConfig is the GKE config - when used outside GKE this is passed in the mesh-env
type TrustConfigSpec struct {
TrustStores []TrustStore `json:"trustStores"`
}
type TrustStore struct {
TrustDomain string `json:"trustDomain"`
TrustAnchors []TrustAnchor `json:"trustAnchors,omitempty"`
}
type TrustAnchor struct {
SPIFFETrustBundleEndpoint string `json:"spiffeTrustBundleEndpoint,omitempty"`
// Format: //privateca.googleapis.com/projects/PROJECT_ID/locations/ROOT_CA_POOL_LOCATION/caPools/ROOT_CA_POOL_NAME
CertificateAuthorityServiceURI string `json:"certificateAuthorityServiceURI,omitempty"`
PEMCertificate string `json:"pemCertificate,omitempty"`
}
func CheckFiles() {
}
//
//
func SaveFiles() {
}
func (a *KRun) NewCSR(kty string, trustDomain, san string) (privPEM []byte, csrPEM []byte, err error) {
var priv crypto.PrivateKey
if kty == "ec256" {
// TODO
}
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048)
priv = rsaKey
csr := GenCSRTemplate(trustDomain, san)
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csr, priv)
encodeMsg := "CERTIFICATE REQUEST"
csrPEM = pem.EncodeToMemory(&pem.Block{Type: encodeMsg, Bytes: csrBytes})
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})
}
//}
return
}
func GenCSRTemplate(trustDomain, san string) *x509.CertificateRequest {
template := &x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{trustDomain},
},
}
// TODO: add the SAN, it is not required, server will fill up
return template
}