Merge pull request #9130 from johngmyers/pki-refactor

Refactor cert issuance code
This commit is contained in:
Kubernetes Prow Robot 2020-06-05 01:43:43 -07:00 committed by GitHub
commit d18e97140e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 572 additions and 666 deletions

View File

@ -35,7 +35,6 @@ go_library(
"import.go",
"import_cluster.go",
"main.go",
"pkix.go",
"replace.go",
"rollingupdate.go",
"rollingupdatecluster.go",

View File

@ -28,6 +28,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
@ -176,8 +177,8 @@ func describeKeypair(keyStore fi.CAStore, item *fi.KeystoreItem, w *bytes.Buffer
}
if cert != nil {
fmt.Fprintf(w, "Subject:\t%s\n", pkixNameToString(&cert.Certificate.Subject))
fmt.Fprintf(w, "Issuer:\t%s\n", pkixNameToString(&cert.Certificate.Issuer))
fmt.Fprintf(w, "Subject:\t%s\n", pki.PkixNameToString(&cert.Certificate.Subject))
fmt.Fprintf(w, "Issuer:\t%s\n", pki.PkixNameToString(&cert.Certificate.Issuer))
fmt.Fprintf(w, "AlternateNames:\t%s\n", strings.Join(alternateNames, ", "))
fmt.Fprintf(w, "CA:\t%v\n", cert.IsCA)
fmt.Fprintf(w, "NotAfter:\t%s\n", cert.Certificate.NotAfter)

View File

@ -1,58 +0,0 @@
/*
Copyright 2019 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 main
import (
"bytes"
"crypto/x509/pkix"
"fmt"
)
func pkixNameToString(name *pkix.Name) string {
seq := name.ToRDNSequence()
var s bytes.Buffer
for _, rdnSet := range seq {
for _, rdn := range rdnSet {
if s.Len() != 0 {
s.WriteString(",")
}
key := ""
t := rdn.Type
if len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 {
switch t[3] {
case 3:
key = "cn"
case 5:
key = "serial"
case 6:
key = "c"
case 7:
key = "l"
case 10:
key = "o"
case 11:
key = "ou"
}
}
if key == "" {
key = t.String()
}
s.WriteString(fmt.Sprintf("%v=%v", key, rdn.Value))
}
}
return s.String()
}

View File

@ -17,12 +17,10 @@ limitations under the License.
package model
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"os"
"path/filepath"
"time"
"k8s.io/klog"
"k8s.io/kops/pkg/pki"
@ -78,26 +76,22 @@ func (b *EtcdManagerTLSBuilder) Build(ctx *fi.ModelBuilderContext) error {
}
func (b *EtcdManagerTLSBuilder) buildKubeAPIServerKeypair() error {
etcdClientsCACertificate, err := b.KeyStore.FindCert("etcd-clients-ca")
if err != nil {
return err
req := &pki.IssueCertRequest{
Signer: "etcd-clients-ca",
Type: "client",
Subject: pkix.Name{
CommonName: "kube-apiserver",
},
MinValidDays: 455,
}
etcdClientsCAPrivateKey, err := b.KeyStore.FindPrivateKey("etcd-clients-ca")
if err != nil {
return err
}
if etcdClientsCACertificate == nil {
klog.Errorf("unable to find etcd-clients-ca certificate, won't build key for apiserver")
return nil
}
if etcdClientsCAPrivateKey == nil {
klog.Errorf("unable to find etcd-clients-ca private key, won't build key for apiserver")
return nil
}
dir := "/etc/kubernetes/pki/kube-apiserver"
name := "etcd-client"
humanName := dir + "/" + name
klog.Infof("signing certificate for %q", humanName)
cert, privateKey, etcdClientsCACertificate, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating directories %q: %v", dir, err)
@ -110,39 +104,12 @@ func (b *EtcdManagerTLSBuilder) buildKubeAPIServerKeypair() error {
}
}
name := "etcd-client"
humanName := dir + "/" + name
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return fmt.Errorf("unable to create private key %q: %v", humanName, err)
}
certTmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: "kube-apiserver",
},
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
klog.Infof("signing certificate for %q", humanName)
cert, err := pki.SignNewCertificate(privateKey, certTmpl, etcdClientsCACertificate.Certificate, etcdClientsCAPrivateKey)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
p := filepath.Join(dir, name)
{
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
{
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
return nil

View File

@ -17,7 +17,6 @@ limitations under the License.
package model
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"path/filepath"
@ -86,7 +85,17 @@ func (b *KubeAPIServerBuilder) addHealthcheckSidecarTasks(c *fi.ModelBuilderCont
})
}
clientKey, clientCert, err := b.buildClientKeypair(id)
req := &pki.IssueCertRequest{
Signer: fi.CertificateId_CA,
Type: "client",
Subject: pkix.Name{
CommonName: id,
},
MinValidDays: 455,
}
klog.Infof("signing certificate for %q", id)
clientCert, clientKey, _, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return err
}
@ -136,68 +145,3 @@ func (b *KubeAPIServerBuilder) addHealthcheckSidecarTasks(c *fi.ModelBuilderCont
return nil
}
func (b *KubeAPIServerBuilder) buildClientKeypair(commonName string) (*pki.PrivateKey, *pki.Certificate, error) {
signerID := fi.CertificateId_CA
var signerKey *pki.PrivateKey
{
k, err := b.KeyStore.FindPrivateKey(signerID)
if err != nil {
return nil, nil, err
}
if k == nil {
return nil, nil, fmt.Errorf("private key %q not found", signerID)
}
signerKey = k
}
var signerCertificate *pki.Certificate
{
cert, err := b.KeyStore.FindCert(signerID)
if err != nil {
return nil, nil, err
}
if cert == nil {
return nil, nil, fmt.Errorf("certificate %q not found", signerID)
}
signerCertificate = cert
}
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return nil, nil, err
}
template := &x509.Certificate{
BasicConstraintsValid: true,
IsCA: false,
Subject: pkix.Name{
CommonName: commonName,
},
}
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
//
// Digital signature allows the certificate to be used to verify
// digital signatures used during TLS negotiation.
template.KeyUsage = template.KeyUsage | x509.KeyUsageDigitalSignature
// KeyEncipherment allows the cert/key pair to be used to encrypt
// keys, including the symmetric keys negotiated during TLS setup
// and used for data transfer.
template.KeyUsage = template.KeyUsage | x509.KeyUsageKeyEncipherment
// ClientAuth allows the cert to be used by a TLS client to
// authenticate itself to the TLS server.
template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
klog.Infof("signing certificate for %q", commonName)
cert, err := pki.SignNewCertificate(privateKey, template, signerCertificate.Certificate, signerKey)
if err != nil {
return nil, nil, fmt.Errorf("error signing certificate for %q: %v", commonName, err)
}
return privateKey, cert, nil
}

View File

@ -18,7 +18,6 @@ package model
import (
"bytes"
"crypto/x509"
"strings"
"testing"
@ -74,10 +73,6 @@ func (k fakeKeyStore) FindKeypair(name string) (*pki.Certificate, *pki.PrivateKe
panic("implement me")
}
func (k fakeKeyStore) CreateKeypair(signer string, name string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) {
panic("implement me")
}
func (k fakeKeyStore) StoreKeypair(id string, cert *pki.Certificate, privateKey *pki.PrivateKey) error {
panic("implement me")
}

View File

@ -17,13 +17,11 @@ limitations under the License.
package model
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"path"
"path/filepath"
"strings"
"time"
"k8s.io/kops/pkg/model/components"
@ -553,54 +551,17 @@ func (b *KubeletBuilder) buildMasterKubeletKubeconfig() (*nodetasks.File, error)
return nil, fmt.Errorf("error getting NodeName: %v", err)
}
caCert, err := b.KeyStore.FindCert(fi.CertificateId_CA)
if err != nil {
return nil, fmt.Errorf("error fetching CA certificate from keystore: %v", err)
}
if caCert == nil {
return nil, fmt.Errorf("unable to find CA certificate %q in keystore", fi.CertificateId_CA)
req := &pki.IssueCertRequest{
Signer: fi.CertificateId_CA,
Type: "client",
Subject: pkix.Name{
CommonName: fmt.Sprintf("system:node:%s", nodeName),
Organization: []string{rbac.NodesGroup},
},
MinValidDays: 455,
}
caKey, err := b.KeyStore.FindPrivateKey(fi.CertificateId_CA)
if err != nil {
return nil, fmt.Errorf("error fetching CA certificate from keystore: %v", err)
}
if caKey == nil {
return nil, fmt.Errorf("unable to find CA key %q in keystore", fi.CertificateId_CA)
}
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return nil, err
}
template := &x509.Certificate{
BasicConstraintsValid: true,
IsCA: false,
}
template.Subject = pkix.Name{
CommonName: fmt.Sprintf("system:node:%s", nodeName),
Organization: []string{rbac.NodesGroup},
}
// https://tools.ietf.org/html/rfc5280#section-4.2.1.3
//
// Digital signature allows the certificate to be used to verify
// digital signatures used during TLS negotiation.
template.KeyUsage = template.KeyUsage | x509.KeyUsageDigitalSignature
// KeyEncipherment allows the cert/key pair to be used to encrypt
// keys, including the symmetric keys negotiated during TLS setup
// and used for data transfer.
template.KeyUsage = template.KeyUsage | x509.KeyUsageKeyEncipherment
// ClientAuth allows the cert to be used by a TLS client to
// authenticate itself to the TLS server.
template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
t := time.Now().UnixNano()
template.SerialNumber = pki.BuildPKISerial(t)
certificate, err := pki.SignNewCertificate(privateKey, template, caCert.Certificate, caKey)
certificate, privateKey, caCert, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return nil, fmt.Errorf("error signing certificate for master kubelet: %v", err)
}

View File

@ -17,12 +17,10 @@ limitations under the License.
package networking
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"os"
"path/filepath"
"time"
"k8s.io/kops/nodeup/pkg/model"
@ -129,17 +127,22 @@ func (b *CiliumBuilder) buildCiliumEtcdSecrets(c *fi.ModelBuilderContext) error
}
}
etcdClientsCACertificate, err := b.KeyStore.FindCert("etcd-clients-ca-cilium")
if err != nil {
return err
req := &pki.IssueCertRequest{
Signer: "etcd-clients-ca-cilium",
Type: "client",
Subject: pkix.Name{
CommonName: "cilium",
},
MinValidDays: 455,
}
etcdClientsCAPrivateKey, err := b.KeyStore.FindPrivateKey("etcd-clients-ca-cilium")
if err != nil {
return err
}
dir := "/etc/kubernetes/pki/cilium"
name := "etcd-client-cilium"
humanName := dir + "/" + name
klog.Infof("signing certificate for %q", humanName)
cert, privateKey, etcdClientsCACertificate, err := pki.IssueCert(req, b.KeyStore)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating directories %q: %v", dir, err)
@ -152,39 +155,12 @@ func (b *CiliumBuilder) buildCiliumEtcdSecrets(c *fi.ModelBuilderContext) error
}
}
name := "etcd-client"
humanName := dir + "/" + name
privateKey, err := pki.GeneratePrivateKey()
if err != nil {
return fmt.Errorf("unable to create private key %q: %v", humanName, err)
}
certTmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: "cilium",
},
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
klog.Infof("signing certificate for %q", humanName)
cert, err := pki.SignNewCertificate(privateKey, certTmpl, etcdClientsCACertificate.Certificate, etcdClientsCAPrivateKey)
if err != nil {
return fmt.Errorf("error signing certificate for %q: %v", humanName, err)
}
p := filepath.Join(dir, name)
{
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
if err := cert.WriteToFile(p+".crt", 0644); err != nil {
return fmt.Errorf("error writing certificate key file %q: %v", p+".crt", err)
}
{
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
if err := privateKey.WriteToFile(p+".key", 0600); err != nil {
return fmt.Errorf("error writing private key file %q: %v", p+".key", err)
}
return nil

View File

@ -20,8 +20,6 @@ import (
"reflect"
"testing"
"crypto/x509"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/pki"
@ -50,8 +48,6 @@ func (f fakeStatusStore) GetApiIngressStatus(cluster *kops.Cluster) ([]kops.ApiI
type fakeKeyStore struct {
FindKeypairFn func(name string) (*pki.Certificate, *pki.PrivateKey, bool, error)
CreateKeypairFn func(signer string, name string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error)
// StoreKeypair writes the keypair to the store
StoreKeypairFn func(id string, cert *pki.Certificate, privateKey *pki.PrivateKey) error
@ -63,10 +59,6 @@ func (f fakeKeyStore) FindKeypair(name string) (*pki.Certificate, *pki.PrivateKe
return f.FindKeypairFn(name)
}
func (f fakeKeyStore) CreateKeypair(signer string, name string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) {
return f.CreateKeypairFn(signer, name, template, privateKey)
}
func (f fakeKeyStore) StoreKeypair(id string, cert *pki.Certificate, privateKey *pki.PrivateKey) error {
return f.StoreKeypairFn(id, cert, privateKey)
}

View File

@ -3,8 +3,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"cert_utils.go",
"certificate.go",
"csr.go",
"issue.go",
"privatekey.go",
"sshkey.go",
],
@ -20,6 +22,7 @@ go_test(
name = "go_default_test",
srcs = [
"certificate_test.go",
"issue_test.go",
"privatekey_test.go",
"sshkey_test.go",
],

View File

@ -14,19 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package fitasks
package pki
import (
"bytes"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"sort"
"strings"
"k8s.io/klog"
)
func pkixNameToString(name *pkix.Name) string {
func PkixNameToString(name *pkix.Name) string {
seq := name.ToRDNSequence()
var s bytes.Buffer
for _, rdnSet := range seq {
@ -61,32 +62,6 @@ func pkixNameToString(name *pkix.Name) string {
return s.String()
}
func parsePkixName(s string) (*pkix.Name, error) {
name := new(pkix.Name)
tokens := strings.Split(s, ",")
for _, token := range tokens {
token = strings.TrimSpace(token)
kv := strings.SplitN(token, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("unrecognized token (expected k=v): %q", token)
}
k := strings.ToLower(kv[0])
v := kv[1]
switch k {
case "cn":
name.CommonName = v
case "o":
name.Organization = append(name.Organization, v)
default:
return nil, fmt.Errorf("unrecognized key %q in token %q", k, token)
}
}
return name, nil
}
var keyUsageStrings = map[x509.KeyUsage]string{
x509.KeyUsageDigitalSignature: "KeyUsageDigitalSignature",
x509.KeyUsageContentCommitment: "KeyUsageContentCommitment",
@ -153,3 +128,29 @@ func parseExtKeyUsage(s string) (x509.ExtKeyUsage, bool) {
}
return 0, false
}
// BuildTypeDescription extracts the type based on the certificate extensions
func BuildTypeDescription(cert *x509.Certificate) string {
var options []string
if cert.IsCA {
options = append(options, "CA")
}
options = append(options, keyUsageToString(cert.KeyUsage)...)
for _, extKeyUsage := range cert.ExtKeyUsage {
options = append(options, extKeyUsageToString(extKeyUsage))
}
sort.Strings(options)
s := strings.Join(options, ",")
for k, v := range wellKnownCertificateTypes {
if v == s {
s = k
}
}
return s
}

View File

@ -97,8 +97,8 @@ func TestGenerateCertificate(t *testing.T) {
CommonName: tc.name,
}
cert, err := SignNewCertificate(key, &tc.template, tc.signer, tc.signerKey)
require.NoError(t, err, "SignNewCertificate")
cert, err := signNewCertificate(key, &tc.template, tc.signer, tc.signerKey)
require.NoError(t, err, "signNewCertificate")
{
subject := cert.Certificate.Subject

View File

@ -45,7 +45,7 @@ func BuildPKISerial(timestamp int64) *big.Int {
return serial
}
func SignNewCertificate(privateKey *PrivateKey, template *x509.Certificate, signer *x509.Certificate, signerPrivateKey *PrivateKey) (*Certificate, error) {
func signNewCertificate(privateKey *PrivateKey, template *x509.Certificate, signer *x509.Certificate, signerPrivateKey *PrivateKey) (*Certificate, error) {
if template.PublicKey == nil {
rsaPrivateKey, ok := privateKey.Key.(*rsa.PrivateKey)
if ok {

176
pkg/pki/issue.go Normal file
View File

@ -0,0 +1,176 @@
/*
Copyright 2020 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 pki
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"hash/fnv"
"math/big"
"net"
"sort"
"strings"
"time"
"k8s.io/klog"
)
var wellKnownCertificateTypes = map[string]string{
"ca": "CA,KeyUsageCRLSign,KeyUsageCertSign",
"client": "ExtKeyUsageClientAuth,KeyUsageDigitalSignature",
"clientServer": "ExtKeyUsageClientAuth,ExtKeyUsageServerAuth,KeyUsageDigitalSignature,KeyUsageKeyEncipherment",
"server": "ExtKeyUsageServerAuth,KeyUsageDigitalSignature,KeyUsageKeyEncipherment",
}
type IssueCertRequest struct {
// Signer is the keypair to use to sign. Ignored if Type is "CA", in which case the cert will be self-signed.
Signer string
// Type is the type of certificate i.e. CA, server, client etc.
Type string
// Subject is the certificate subject.
Subject pkix.Name
// AlternateNames is a list of alternative names for this certificate.
AlternateNames []string
// PrivateKey is the private key for this certificate. If nil, a new private key will be generated.
PrivateKey *PrivateKey
// MinValidDays is the lower bound on the certificate validity, in days. If specified, up to 30 days
// will be added so that certificate generated at the same time on different hosts will be unlikely to
// expire at the same time. The default is 10 years (without the up to 30 day skew).
MinValidDays int
// Serial is the certificate serial number. If nil, a random number will be generated.
Serial *big.Int
}
type Keystore interface {
// FindKeypair finds a cert & private key, returning nil where either is not found
// (if the certificate is found but not keypair, that is not an error: only the cert will be returned).
// This func returns a cert, private key and a bool. The bool value is whether the keypair is stored
// in a legacy format. This bool is used by a keypair
// task to convert a Legacy Keypair to the new Keypair API format.
FindKeypair(name string) (*Certificate, *PrivateKey, bool, error)
}
// IssueCert issues a certificate, either a self-signed CA or from a CA in a keystore.
func IssueCert(request *IssueCertRequest, keystore Keystore) (issuedCertificate *Certificate, issuedKey *PrivateKey, caCertificate *Certificate, err error) {
certificateType := request.Type
if expanded, found := wellKnownCertificateTypes[certificateType]; found {
certificateType = expanded
}
template := &x509.Certificate{
BasicConstraintsValid: true,
IsCA: false,
SerialNumber: request.Serial,
}
tokens := strings.Split(certificateType, ",")
for _, t := range tokens {
if strings.HasPrefix(t, "KeyUsage") {
ku, found := parseKeyUsage(t)
if !found {
return nil, nil, nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.KeyUsage |= ku
} else if strings.HasPrefix(t, "ExtKeyUsage") {
ku, found := parseExtKeyUsage(t)
if !found {
return nil, nil, nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.ExtKeyUsage = append(template.ExtKeyUsage, ku)
} else if t == "CA" {
template.IsCA = true
} else {
return nil, nil, nil, fmt.Errorf("unrecognized certificate option: %q", t)
}
}
template.Subject = request.Subject
var alternateNames []string
alternateNames = append(alternateNames, request.AlternateNames...)
for _, san := range alternateNames {
san = strings.TrimSpace(san)
if san == "" {
continue
}
if ip := net.ParseIP(san); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, san)
}
}
var caPrivateKey *PrivateKey
var signer *x509.Certificate
if !template.IsCA {
var err error
caCertificate, caPrivateKey, _, err = keystore.FindKeypair(request.Signer)
if err != nil {
return nil, nil, nil, err
}
if caPrivateKey == nil {
return nil, nil, nil, fmt.Errorf("ca key for %q was not found; cannot issue certificates", request.Signer)
}
if caCertificate == nil {
return nil, nil, nil, fmt.Errorf("ca certificate for %q was not found; cannot issue certificates", request.Signer)
}
signer = caCertificate.Certificate
}
privateKey := request.PrivateKey
if privateKey == nil {
var err error
privateKey, err = GeneratePrivateKey()
if err != nil {
return nil, nil, nil, err
}
}
// Skew the certificate lifetime by up to 30 days based on information about the generating node.
// This is so that different nodes created at the same time have the certificates they generated
// expire at different times, but all certificates on a given node expire around the same time.
if request.MinValidDays != 0 {
hash := fnv.New32()
addrs, err := net.InterfaceAddrs()
sort.Slice(addrs, func(i, j int) bool {
return addrs[i].String() < addrs[j].String()
})
if err == nil {
for _, addr := range addrs {
_, _ = hash.Write([]byte(addr.String()))
}
} else {
klog.Warningf("cannot skew certificate lifetime: failed to get interface addresses: %v", err)
}
template.NotAfter = time.Now().Add(time.Hour * 24 * time.Duration(request.MinValidDays)).Add(time.Hour * time.Duration(hash.Sum32()%(30*24))).UTC()
}
certificate, err := signNewCertificate(privateKey, template, signer, caPrivateKey)
if err != nil {
return nil, nil, nil, err
}
if signer == nil {
caCertificate = certificate
}
return certificate, privateKey, caCertificate, err
}

216
pkg/pki/issue_test.go Normal file
View File

@ -0,0 +1,216 @@
/*
Copyright 2020 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 pki
import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockKeystore struct {
t *testing.T
signer string
cert *Certificate
key *PrivateKey
invoked bool
}
func (m *mockKeystore) FindKeypair(name string) (*Certificate, *PrivateKey, bool, error) {
assert.False(m.t, m.invoked, "invoked already")
m.invoked = true
assert.Equal(m.t, m.signer, name, "name argument")
return m.cert, m.key, false, nil
}
func TestIssueCert(t *testing.T) {
caCertificate, err := ParsePEMCertificate([]byte("-----BEGIN CERTIFICATE-----\nMIIBRjCB8aADAgECAhAzhRMOcwfggPtgZNIOFU19MA0GCSqGSIb3DQEBCwUAMBIx\nEDAOBgNVBAMTB1Rlc3QgQ0EwHhcNMjAwNTE1MDIzNjI0WhcNMzAwNTE1MDIzNjI0\nWjASMRAwDgYDVQQDEwdUZXN0IENBMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM/S\ncagGaiDA3jJWBXUr8rM19TWLA65jK/iA05FCsmQbyvETs5gbJdBfnhQp8wkKFlkt\nKxZ34k3wQUzoB1lv8/kCAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADQQCDOxvs58AVAWgWLtD3Obvy7XXsKx6d\nMzg9epbiQchLE4G/jlbgVu7vwh8l5XFNfQooG6stCU7pmLFXkXzkJQxr\n-----END CERTIFICATE-----\n"))
require.NoError(t, err)
caPrivateKey, err := ParsePEMPrivateKey([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBPAIBAAJBAM/ScagGaiDA3jJWBXUr8rM19TWLA65jK/iA05FCsmQbyvETs5gb\nJdBfnhQp8wkKFlktKxZ34k3wQUzoB1lv8/kCAwEAAQJBAJzXQZeBX87gP9DVQsEv\nLbc6XZjPFTQi/ChLcWALaf5J7drFJHUcWbKIHzOmM3fm3lQlb/1IcwOBU5cTY0e9\nBVECIQD73kxOWWAIzKqMOvFZ9s79Et7G1HUMnVAVKJ1NS1uvYwIhANM7LULdi0YD\nbcHvDl3+Msj4cPH7CXAJFyPWaQZPlXPzAiEAhDg6jpbUl0n57guzT6sFFk2lrXMy\nzyB2PeVITp9UzkkCIEpcF7flQ+U2ycmuvVELbpdfFmupIw5ktNex4DEPjR5PAiEA\n68vR1L1Kaja/GzU76qAQaYA/V1Ag4sPmOQdEaVZKu78=\n-----END RSA PRIVATE KEY-----\n"))
require.NoError(t, err)
privateKey, err := ParsePEMPrivateKey([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBANgL5cR2cLOB7oZZTiuiUmMwQRBaia8yLULt+XtBtDHf0lPOrn78\nvLPh7P7zRBgHczbTddcsg68g9vAfb9TC5M8CAwEAAQJAJytxCv+WS1VhU4ZZf9u8\nKDOVeEuR7uuf/SR8OPaenvPqONpYbZSVjnWnRBRHvg3HaHchQqH32UljZUojs9z4\nEQIhAO/yoqCFckfqswOGwWyYX1oNOtU8w9ulXlZqAtZieavVAiEA5n/tKHoZyx3U\nbZcks/wns1WqhAoSmDJpMyVXOVrUlBMCIDGnalQBiYasYOMn7bsFRSYjertJ2dYI\nQJ9tTK0Er90JAiAmpVQx8SbZ80pmhWzV8HUHkFligf3UHr+cn6ocJ6p0mQIgB728\npdvrS5zRPoUN8BHfWOZcPrElKTuJjP2kH6eNPvI=\n-----END RSA PRIVATE KEY-----"))
require.NoError(t, err)
for _, tc := range []struct {
name string
req IssueCertRequest
expectedKeyUsage x509.KeyUsage
expectedExtKeyUsage []x509.ExtKeyUsage
expectedSubject pkix.Name
expectedDNSNames []string
expectedIPAddresses []net.IP
}{
{
name: "ca",
req: IssueCertRequest{
Type: "ca",
Subject: pkix.Name{
CommonName: "Test CA",
},
},
expectedKeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign,
expectedSubject: pkix.Name{CommonName: "Test CA"},
},
{
name: "client",
req: IssueCertRequest{
Type: "client",
Subject: pkix.Name{
CommonName: "Test client",
Organization: []string{"system:masters"},
},
Serial: BuildPKISerial(123456),
},
expectedKeyUsage: x509.KeyUsageDigitalSignature,
expectedExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
expectedSubject: pkix.Name{CommonName: "Test client", Organization: []string{"system:masters"}},
},
{
name: "clientOneYear",
req: IssueCertRequest{
Type: "client",
Subject: pkix.Name{
CommonName: "Test client",
},
MinValidDays: 365,
},
expectedKeyUsage: x509.KeyUsageDigitalSignature,
expectedExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
expectedSubject: pkix.Name{CommonName: "Test client"},
},
{
req: IssueCertRequest{
Type: "clientServer",
Subject: pkix.Name{
CommonName: "Test client/server",
},
AlternateNames: []string{"*.internal.test.cluster.local", "localhost", "127.0.0.1"},
PrivateKey: privateKey,
},
expectedKeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
expectedExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
expectedSubject: pkix.Name{CommonName: "Test client/server"},
expectedDNSNames: []string{"*.internal.test.cluster.local", "localhost"},
expectedIPAddresses: []net.IP{net.ParseIP("127.0.0.1").To4()},
},
{
name: "server",
req: IssueCertRequest{
Type: "server",
Subject: pkix.Name{
CommonName: "Test server",
},
AlternateNames: []string{"*.internal.test.cluster.local", "localhost", "127.0.0.1"},
PrivateKey: privateKey,
},
expectedKeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
expectedExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
expectedSubject: pkix.Name{CommonName: "Test server"},
expectedDNSNames: []string{"*.internal.test.cluster.local", "localhost"},
expectedIPAddresses: []net.IP{net.ParseIP("127.0.0.1").To4()},
},
} {
t.Run(tc.name, func(t *testing.T) {
var minExpectedValidity int64
if tc.req.MinValidDays == 0 {
minExpectedValidity = time.Now().Add(time.Hour * 10 * 365 * 24).Unix()
} else {
minExpectedValidity = time.Now().Add(time.Hour * 24 * time.Duration(tc.req.MinValidDays)).Unix()
}
var keystore Keystore
if tc.req.Type != "ca" {
tc.req.Signer = tc.name + "-signer"
keystore = &mockKeystore{
t: t,
signer: tc.req.Signer,
cert: caCertificate,
key: caPrivateKey,
}
}
certificate, key, caCert, err := IssueCert(&tc.req, keystore)
require.NoError(t, err)
cert := certificate.Certificate
if tc.req.Signer == "" {
assert.Equal(t, cert.Issuer, cert.Subject, "self-signed")
assert.NoError(t, cert.CheckSignatureFrom(cert), "check signature")
assert.Equal(t, certificate, caCert, "returned CA cert")
} else {
assert.Equal(t, cert.Issuer, caCertificate.Certificate.Subject, "cert signer")
assert.NoError(t, cert.CheckSignatureFrom(caCertificate.Certificate), "check signature")
assert.Equal(t, caCertificate, caCert, "returned CA cert")
}
// type
assert.Equal(t, certificate.IsCA, cert.IsCA, "IsCA matches")
assert.Equal(t, tc.req.Type == "ca", cert.IsCA, "IsCA")
assert.True(t, cert.BasicConstraintsValid, "BasicConstraintsValid")
assert.Equal(t, tc.expectedKeyUsage, cert.KeyUsage, "KeyUsage")
assert.ElementsMatch(t, tc.expectedExtKeyUsage, cert.ExtKeyUsage)
assert.Nil(t, cert.ExtraExtensions, "ExtraExtensions")
// subject
assert.Equal(t, certificate.Subject, cert.Subject, "Subject matches")
actualName := cert.Subject
actualName.Names = nil
assert.Equal(t, tc.expectedSubject, actualName, "Subject")
// alternateNames
assert.Equal(t, tc.expectedDNSNames, cert.DNSNames, "DNSNames")
assert.Equal(t, tc.expectedIPAddresses, cert.IPAddresses, "IPAddresses")
assert.Empty(t, cert.EmailAddresses, "EmailAddresses")
// privateKey
rsaPrivateKey, ok := key.Key.(*rsa.PrivateKey)
require.True(t, ok, "private key is RSA")
if tc.req.PrivateKey == nil {
assert.Equal(t, 2048, rsaPrivateKey.N.BitLen(), "Private key length")
} else {
assert.Equal(t, tc.req.PrivateKey, key, "Private key")
}
assert.Equal(t, &rsaPrivateKey.PublicKey, cert.PublicKey, "certificate public key matches private key")
assert.Equal(t, certificate.PublicKey, cert.PublicKey, "PublicKey")
// serial
if tc.req.Serial != nil {
assert.Equal(t, cert.SerialNumber, tc.req.Serial, "SerialNumber")
} else {
assert.Greater(t, cert.SerialNumber.BitLen(), 110, "SerialNumber bit length")
}
// validity
var maxExpectedValidity int64
if tc.req.MinValidDays == 0 {
maxExpectedValidity = time.Now().Add(time.Hour * 10 * 365 * 24).Unix()
} else {
maxExpectedValidity = time.Now().Add(time.Hour * 24 * time.Duration(tc.req.MinValidDays+30)).Unix()
}
assert.Less(t, cert.NotBefore.Unix(), time.Now().Add(time.Hour*-47).Unix(), "NotBefore")
assert.GreaterOrEqual(t, cert.NotAfter.Unix(), minExpectedValidity, "NotAfter")
assert.LessOrEqual(t, cert.NotAfter.Unix(), maxExpectedValidity, "NotAfter")
})
}
}

View File

@ -18,7 +18,6 @@ package fi
import (
"bytes"
"crypto/x509"
"fmt"
"k8s.io/kops/pkg/apis/kops"
@ -53,8 +52,6 @@ type Keystore interface {
// task to convert a Legacy Keypair to the new Keypair API format.
FindKeypair(name string) (*pki.Certificate, *pki.PrivateKey, bool, error)
CreateKeypair(signer string, name string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error)
// StoreKeypair writes the keypair to the store
StoreKeypair(id string, cert *pki.Certificate, privateKey *pki.PrivateKey) error

View File

@ -19,11 +19,8 @@ package fi
import (
"bytes"
"context"
"crypto/x509"
"fmt"
"math/big"
"sync"
"time"
"golang.org/x/crypto/ssh"
"k8s.io/apimachinery/pkg/api/errors"
@ -40,9 +37,6 @@ type ClientsetCAStore struct {
cluster *kops.Cluster
namespace string
clientset kopsinternalversion.KopsInterface
mutex sync.Mutex
cachedCaKeysets map[string]*keyset
}
var _ CAStore = &ClientsetCAStore{}
@ -51,10 +45,9 @@ var _ SSHCredentialStore = &ClientsetCAStore{}
// NewClientsetCAStore is the constructor for ClientsetCAStore
func NewClientsetCAStore(cluster *kops.Cluster, clientset kopsinternalversion.KopsInterface, namespace string) CAStore {
c := &ClientsetCAStore{
cluster: cluster,
clientset: clientset,
namespace: namespace,
cachedCaKeysets: make(map[string]*keyset),
cluster: cluster,
clientset: clientset,
namespace: namespace,
}
return c
@ -64,38 +57,14 @@ func NewClientsetCAStore(cluster *kops.Cluster, clientset kopsinternalversion.Ko
func NewClientsetSSHCredentialStore(cluster *kops.Cluster, clientset kopsinternalversion.KopsInterface, namespace string) SSHCredentialStore {
// Note: currently identical to NewClientsetCAStore
c := &ClientsetCAStore{
cluster: cluster,
clientset: clientset,
namespace: namespace,
cachedCaKeysets: make(map[string]*keyset),
cluster: cluster,
clientset: clientset,
namespace: namespace,
}
return c
}
// readCAKeypairs retrieves the CA keypair.
// (No longer generates a keypair if not found.)
func (c *ClientsetCAStore) readCAKeypairs(ctx context.Context, id string) (*keyset, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
cached := c.cachedCaKeysets[id]
if cached != nil {
return cached, nil
}
keyset, err := c.loadKeyset(ctx, id)
if err != nil {
return nil, err
}
if keyset == nil {
return nil, nil
}
c.cachedCaKeysets[id] = keyset
return keyset, nil
}
// keyset is a parsed Keyset
type keyset struct {
legacyFormat bool
@ -319,67 +288,6 @@ func (c *ClientsetCAStore) ListSSHCredentials() ([]*kops.SSHCredential, error) {
return items, nil
}
func (c *ClientsetCAStore) issueCert(signer string, name string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) {
ctx := context.TODO()
klog.Infof("Issuing new certificate: %q", name)
template.SerialNumber = serial
caKeyset, err := c.readCAKeypairs(ctx, signer)
if err != nil {
return nil, err
}
if caKeyset == nil {
return nil, fmt.Errorf("ca keyset was not found; cannot issue certificates")
}
if caKeyset.primary == nil {
return nil, fmt.Errorf("ca keyset did not have any key data; cannot issue certificates")
}
if caKeyset.primary.certificate == nil {
return nil, fmt.Errorf("ca certificate was not found; cannot issue certificates")
}
if caKeyset.primary.privateKey == nil {
return nil, fmt.Errorf("ca privateKey was not found; cannot issue certificates")
}
cert, err := pki.SignNewCertificate(privateKey, template, caKeyset.primary.certificate.Certificate, caKeyset.primary.privateKey)
if err != nil {
return nil, err
}
if _, err := c.storeAndVerifyKeypair(ctx, name, cert, privateKey); err != nil {
return nil, err
}
return cert, nil
}
// storeAndVerifyKeypair writes the keypair, also re-reading it to double-check it
func (c *ClientsetCAStore) storeAndVerifyKeypair(ctx context.Context, name string, cert *pki.Certificate, privateKey *pki.PrivateKey) (*keyset, error) {
id := cert.Certificate.SerialNumber.String()
if err := c.storeKeypair(ctx, name, id, cert, privateKey); err != nil {
return nil, err
}
// Make double-sure it round-trips
keyset, err := c.loadKeyset(ctx, name)
if err != nil {
return nil, fmt.Errorf("error fetching stored certificate: %v", err)
}
if keyset == nil {
return nil, fmt.Errorf("stored certificate not found: %v", err)
}
if keyset.primary == nil {
return nil, fmt.Errorf("stored certificate did not have data: %v", err)
}
if keyset.primary.id != id {
return nil, fmt.Errorf("stored certificate changed concurrently (id mismatch)")
}
return keyset, nil
}
// StoreKeypair implements CAStore::StoreKeypair
func (c *ClientsetCAStore) StoreKeypair(name string, cert *pki.Certificate, privateKey *pki.PrivateKey) error {
ctx := context.TODO()
@ -429,18 +337,6 @@ func (c *ClientsetCAStore) FindPrivateKeyset(name string) (*kops.Keyset, error)
return o, nil
}
// CreateKeypair implements CAStore::CreateKeypair
func (c *ClientsetCAStore) CreateKeypair(signer string, id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) {
serial := c.buildSerial()
cert, err := c.issueCert(signer, id, serial, privateKey, template)
if err != nil {
return nil, err
}
return cert, nil
}
// addKey saves the specified key to the registry
func (c *ClientsetCAStore) addKey(ctx context.Context, name string, keysetType kops.KeysetType, item *kops.KeysetItem) error {
create := false
@ -573,12 +469,6 @@ func (c *ClientsetCAStore) storeKeypair(ctx context.Context, name string, id str
return c.addKey(ctx, name, kops.SecretTypeKeypair, item)
}
// buildSerial returns a serial for use when issuing certificates
func (c *ClientsetCAStore) buildSerial() *big.Int {
t := time.Now().UnixNano()
return pki.BuildPKISerial(t)
}
// AddSSHPublicKey implements CAStore::AddSSHPublicKey
func (c *ClientsetCAStore) AddSSHPublicKey(name string, pubkey []byte) error {
ctx := context.TODO()

View File

@ -3,7 +3,6 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"cert_utils.go",
"keypair.go",
"keypair_fitask.go",
"managedfile.go",

View File

@ -17,24 +17,17 @@ limitations under the License.
package fitasks
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"net"
"sort"
"strings"
"time"
"k8s.io/klog"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
)
var wellKnownCertificateTypes = map[string]string{
"ca": "CA,KeyUsageCRLSign,KeyUsageCertSign",
"client": "ExtKeyUsageClientAuth,KeyUsageDigitalSignature",
"clientServer": "ExtKeyUsageClientAuth,ExtKeyUsageServerAuth,KeyUsageDigitalSignature,KeyUsageKeyEncipherment",
"server": "ExtKeyUsageServerAuth,KeyUsageDigitalSignature,KeyUsageKeyEncipherment",
}
//go:generate fitask -type=Keypair
type Keypair struct {
// Name is the name of the keypair
@ -97,12 +90,12 @@ func (e *Keypair) Find(c *fi.Context) (*Keypair, error) {
actual := &Keypair{
Name: &name,
AlternateNames: alternateNames,
Subject: pkixNameToString(&cert.Subject),
Type: buildTypeDescription(cert.Certificate),
Subject: pki.PkixNameToString(&cert.Subject),
Type: pki.BuildTypeDescription(cert.Certificate),
LegacyFormat: legacyFormat,
}
actual.Signer = &Keypair{Subject: pkixNameToString(&cert.Certificate.Issuer)}
actual.Signer = &Keypair{Subject: pki.PkixNameToString(&cert.Certificate.Issuer)}
// Avoid spurious changes
actual.Lifecycle = e.Lifecycle
@ -168,11 +161,6 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
return fi.RequiredField("Name")
}
template, err := e.BuildCertificateTemplate()
if err != nil {
return err
}
changeStoredFormat := false
createCertificate := false
if a == nil {
@ -209,11 +197,6 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
// TODO: Eventually rotate keys / don't always reuse?
if privateKey == nil {
klog.V(2).Infof("Creating privateKey %q", name)
privateKey, err = pki.GeneratePrivateKey()
if err != nil {
return err
}
}
signer := fi.CertificateId_CA
@ -221,7 +204,38 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
signer = fi.StringValue(e.Signer.Name)
}
cert, err := c.Keystore.CreateKeypair(signer, name, template, privateKey)
klog.Infof("Issuing new certificate: %q", *e.Name)
serial := pki.BuildPKISerial(time.Now().UnixNano())
subjectPkix, err := parsePkixName(e.Subject)
if err != nil {
return fmt.Errorf("error parsing Subject: %v", err)
}
if len(subjectPkix.ToRDNSequence()) == 0 {
return fmt.Errorf("subject name was empty for SSL keypair %q", *e.Name)
}
req := pki.IssueCertRequest{
Signer: signer,
Type: e.Type,
Subject: *subjectPkix,
AlternateNames: e.AlternateNames,
PrivateKey: privateKey,
Serial: serial,
}
cert, privateKey, _, err := pki.IssueCert(&req, c.Keystore)
if err != nil {
return err
}
err = c.Keystore.StoreKeypair(name, cert, privateKey)
if err != nil {
return err
}
// Make double-sure it round-trips
_, _, _, err = c.Keystore.FindKeypair(name)
if err != nil {
return err
}
@ -249,98 +263,28 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
return nil
}
// BuildCertificateTemplate is responsible for constructing a certificate template
func (e *Keypair) BuildCertificateTemplate() (*x509.Certificate, error) {
template, err := buildCertificateTemplateForType(e.Type)
if err != nil {
return nil, err
}
func parsePkixName(s string) (*pkix.Name, error) {
name := new(pkix.Name)
subjectPkix, err := parsePkixName(e.Subject)
if err != nil {
return nil, fmt.Errorf("error parsing Subject: %v", err)
}
if len(subjectPkix.ToRDNSequence()) == 0 {
return nil, fmt.Errorf("Subject name was empty for SSL keypair %q", *e.Name)
}
template.Subject = *subjectPkix
var alternateNames []string
alternateNames = append(alternateNames, e.AlternateNames...)
for _, san := range alternateNames {
san = strings.TrimSpace(san)
if san == "" {
continue
tokens := strings.Split(s, ",")
for _, token := range tokens {
token = strings.TrimSpace(token)
kv := strings.SplitN(token, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("unrecognized token (expected k=v): %q", token)
}
if ip := net.ParseIP(san); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, san)
k := strings.ToLower(kv[0])
v := kv[1]
switch k {
case "cn":
name.CommonName = v
case "o":
name.Organization = append(name.Organization, v)
default:
return nil, fmt.Errorf("unrecognized key %q in token %q", k, token)
}
}
return template, nil
}
func buildCertificateTemplateForType(certificateType string) (*x509.Certificate, error) {
if expanded, found := wellKnownCertificateTypes[certificateType]; found {
certificateType = expanded
}
template := &x509.Certificate{
BasicConstraintsValid: true,
IsCA: false,
}
tokens := strings.Split(certificateType, ",")
for _, t := range tokens {
if strings.HasPrefix(t, "KeyUsage") {
ku, found := parseKeyUsage(t)
if !found {
return nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.KeyUsage |= ku
} else if strings.HasPrefix(t, "ExtKeyUsage") {
ku, found := parseExtKeyUsage(t)
if !found {
return nil, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.ExtKeyUsage = append(template.ExtKeyUsage, ku)
} else if t == "CA" {
template.IsCA = true
} else {
return nil, fmt.Errorf("unrecognized certificate option: %q", t)
}
}
return template, nil
}
// buildTypeDescription extracts the type based on the certificate extensions
func buildTypeDescription(cert *x509.Certificate) string {
var options []string
if cert.IsCA {
options = append(options, "CA")
}
options = append(options, keyUsageToString(cert.KeyUsage)...)
for _, extKeyUsage := range cert.ExtKeyUsage {
options = append(options, extKeyUsageToString(extKeyUsage))
}
sort.Strings(options)
s := strings.Join(options, ",")
for k, v := range wellKnownCertificateTypes {
if v == s {
s = k
}
}
return s
return name, nil
}

View File

@ -18,13 +18,11 @@ package fi
import (
"bytes"
"crypto/x509"
"fmt"
"math/big"
"os"
"strings"
"sync"
"time"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog"
@ -41,17 +39,8 @@ type VFSCAStore struct {
basedir vfs.Path
cluster *kops.Cluster
mutex sync.Mutex
cachedCAs map[string]*cachedEntry
// SerialGenerator is the function for generating certificate serial numbers
// It can be replaced for testing purposes.
SerialGenerator func() *big.Int
}
type cachedEntry struct {
certificates *keyset
privateKeys *keyset
mutex sync.Mutex
cachedCA *keyset
}
var _ CAStore = &VFSCAStore{}
@ -59,14 +48,8 @@ var _ SSHCredentialStore = &VFSCAStore{}
func NewVFSCAStore(cluster *kops.Cluster, basedir vfs.Path) *VFSCAStore {
c := &VFSCAStore{
basedir: basedir,
cluster: cluster,
cachedCAs: make(map[string]*cachedEntry),
}
c.SerialGenerator = func() *big.Int {
t := time.Now().UnixNano()
return pki.BuildPKISerial(t)
basedir: basedir,
cluster: cluster,
}
return c
@ -76,9 +59,8 @@ func NewVFSCAStore(cluster *kops.Cluster, basedir vfs.Path) *VFSCAStore {
func NewVFSSSHCredentialStore(cluster *kops.Cluster, basedir vfs.Path) SSHCredentialStore {
// Note currently identical to NewVFSCAStore
c := &VFSCAStore{
basedir: basedir,
cluster: cluster,
cachedCAs: make(map[string]*cachedEntry),
basedir: basedir,
cluster: cluster,
}
return c
@ -88,47 +70,6 @@ func (s *VFSCAStore) VFSPath() vfs.Path {
return s.basedir
}
// Retrieves the CA keypair. No longer generates keypairs if not found.
func (s *VFSCAStore) readCAKeypairs(id string) (*keyset, *keyset, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
cached := s.cachedCAs[id]
if cached != nil {
return cached.certificates, cached.privateKeys, nil
}
caCertificates, err := s.loadCertificates(s.buildCertificatePoolPath(id))
if err != nil {
return nil, nil, err
}
var caPrivateKeys *keyset
if caCertificates != nil {
caPrivateKeys, err = s.loadPrivateKeys(s.buildPrivateKeyPoolPath(id))
if err != nil {
return nil, nil, err
}
if caPrivateKeys == nil {
klog.Warningf("CA private key was not found")
//return nil, fmt.Errorf("error loading CA private key - key not found")
}
}
if caPrivateKeys == nil {
// We no longer generate CA certificates automatically - too race-prone
return caCertificates, caPrivateKeys, nil
}
cached = &cachedEntry{certificates: caCertificates, privateKeys: caPrivateKeys}
s.cachedCAs[id] = cached
return cached.certificates, cached.privateKeys, nil
}
func (c *VFSCAStore) buildCertificatePoolPath(name string) vfs.Path {
return c.basedir.Join("issued", name)
}
@ -586,46 +527,6 @@ func mirrorSSHCredential(cluster *kops.Cluster, basedir vfs.Path, sshCredential
return nil
}
func (c *VFSCAStore) issueCert(signer string, id string, serial *big.Int, privateKey *pki.PrivateKey, template *x509.Certificate) (*pki.Certificate, error) {
klog.Infof("Issuing new certificate: %q", id)
template.SerialNumber = serial
var cert *pki.Certificate
if template.IsCA {
var err error
cert, err = pki.SignNewCertificate(privateKey, template, nil, nil)
if err != nil {
return nil, err
}
} else {
caCertificates, caPrivateKeys, err := c.readCAKeypairs(signer)
if err != nil {
return nil, err
}
if caPrivateKeys == nil || caPrivateKeys.primary == nil || caPrivateKeys.primary.privateKey == nil {
return nil, fmt.Errorf("ca key for %q was not found; cannot issue certificates", signer)
}
if caCertificates == nil || caCertificates.primary == nil || caCertificates.primary.certificate == nil {
return nil, fmt.Errorf("ca certificate for %q was not found; cannot issue certificates", signer)
}
cert, err = pki.SignNewCertificate(privateKey, template, caCertificates.primary.certificate.Certificate, caPrivateKeys.primary.privateKey)
if err != nil {
return nil, err
}
}
err := c.StoreKeypair(id, cert, privateKey)
if err != nil {
return nil, err
}
// Make double-sure it round-trips
p := c.buildCertificatePath(id, serial.String())
return c.loadOneCertificate(p)
}
func (c *VFSCAStore) StoreKeypair(name string, cert *pki.Certificate, privateKey *pki.PrivateKey) error {
serial := cert.Certificate.SerialNumber.String()
@ -684,14 +585,28 @@ func (c *VFSCAStore) loadPrivateKeys(p vfs.Path) (*keyset, error) {
func (c *VFSCAStore) findPrivateKeyset(id string) (*keyset, error) {
var keys *keyset
var err error
if id == CertificateId_CA {
_, caPrivateKeys, err := c.readCAKeypairs(id)
c.mutex.Lock()
defer c.mutex.Unlock()
cached := c.cachedCA
if cached != nil {
return cached, nil
}
keys, err = c.loadPrivateKeys(c.buildPrivateKeyPoolPath(id))
if err != nil {
return nil, err
}
keys = caPrivateKeys
if keys == nil {
klog.Warningf("CA private key was not found")
// We no longer generate CA certificates automatically - too race-prone
} else {
c.cachedCA = keys
}
} else {
var err error
p := c.buildPrivateKeyPoolPath(id)
keys, err = c.loadPrivateKeys(p)
if err != nil {
@ -729,17 +644,6 @@ func (c *VFSCAStore) FindPrivateKeyset(name string) (*kops.Keyset, error) {
return o, nil
}
func (c *VFSCAStore) CreateKeypair(signer string, id string, template *x509.Certificate, privateKey *pki.PrivateKey) (*pki.Certificate, error) {
serial := c.SerialGenerator()
cert, err := c.issueCert(signer, id, serial, privateKey, template)
if err != nil {
return nil, err
}
return cert, nil
}
func (c *VFSCAStore) storePrivateKey(name string, ki *keysetItem) error {
if ki.privateKey == nil {
return fmt.Errorf("privateKey not provided to storeCertificate")

View File

@ -55,8 +55,7 @@ func TestVFSCAStoreRoundTrip(t *testing.T) {
}
s := &VFSCAStore{
basedir: basePath,
cachedCAs: make(map[string]*cachedEntry),
basedir: basePath,
}
privateKeyData := "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4JwpEprZ5n8RIEt6jT2lAh+UDgRgx/4px21gjgywQivYHVxH\nAZexVb/E9pBa9Q2G9B1Q7TCO7YsUVRQy4JMDZVt+McFnWVwexnqBYFNcVjkEmDgA\ngvCYGE0P9d/RwRL4KuLHo+u6fv7P0jXMN+CpOxyLhYZZNa0ZOZDHsSiJSQSj9WGF\nGHrbCf0KVDpKieR1uBqHrRO+mLR5zkX2L58m74kjK4dsBhmjeq/7OAoTmiG2QgJ/\nP2IjyhiA2mRqY+hl55lwEUV/0yHYEkJC8LdGkwwZz2eF77aSPGmi/A2CSKgMwDTx\n9m+P7jcpWreYw6NG9BueGoDIve/tgFKwvVFF6QIDAQABAoIBAA0ktjaTfyrAxsTI\nBezb7Zr5NBW55dvuII299cd6MJo+rI/TRYhvUv48kY8IFXp/hyUjzgeDLunxmIf9\n/Zgsoic9Ol44/g45mMduhcGYPzAAeCdcJ5OB9rR9VfDCXyjYLlN8H8iU0734tTqM\n0V13tQ9zdSqkGPZOIcq/kR/pylbOZaQMe97BTlsAnOMSMKDgnftY4122Lq3GYy+t\nvpr+bKVaQZwvkLoSU3rECCaKaghgwCyX7jft9aEkhdJv+KlwbsGY6WErvxOaLWHd\ncuMQjGapY1Fa/4UD00mvrA260NyKfzrp6+P46RrVMwEYRJMIQ8YBAk6N6Hh7dc0G\n8Z6i1m0CgYEA9HeCJR0TSwbIQ1bDXUrzpftHuidG5BnSBtax/ND9qIPhR/FBW5nj\n22nwLc48KkyirlfIULd0ae4qVXJn7wfYcuX/cJMLDmSVtlM5Dzmi/91xRiFgIzx1\nAsbBzaFjISP2HpSgL+e9FtSXaaqeZVrflitVhYKUpI/AKV31qGHf04sCgYEA6zTV\n99Sb49Wdlns5IgsfnXl6ToRttB18lfEKcVfjAM4frnkk06JpFAZeR+9GGKUXZHqs\nz2qcplw4d/moCC6p3rYPBMLXsrGNEUFZqBlgz72QA6BBq3X0Cg1Bc2ZbK5VIzwkg\nST2SSux6ccROfgULmN5ZiLOtdUKNEZpFF3i3qtsCgYADT/s7dYFlatobz3kmMnXK\nsfTu2MllHdRys0YGHu7Q8biDuQkhrJwhxPW0KS83g4JQym+0aEfzh36bWcl+u6R7\nKhKj+9oSf9pndgk345gJz35RbPJYh+EuAHNvzdgCAvK6x1jETWeKf6btj5pF1U1i\nQ4QNIw/QiwIXjWZeubTGsQKBgQCbduLu2rLnlyyAaJZM8DlHZyH2gAXbBZpxqU8T\nt9mtkJDUS/KRiEoYGFV9CqS0aXrayVMsDfXY6B/S/UuZjO5u7LtklDzqOf1aKG3Q\ndGXPKibknqqJYH+bnUNjuYYNerETV57lijMGHuSYCf8vwLn3oxBfERRX61M/DU8Z\nworz/QKBgQDCTJI2+jdXg26XuYUmM4XXfnocfzAXhXBULt1nENcogNf1fcptAVtu\nBAiz4/HipQKqoWVUYmxfgbbLRKKLK0s0lOWKbYdVjhEm/m2ZU8wtXTagNwkIGoyq\nY/C1Lox4f1ROJnCjc/hfcOjcxX5M8A8peecHWlVtUPKTJgxQ7oMKcw==\n-----END RSA PRIVATE KEY-----\n"