mirror of https://github.com/kubernetes/kops.git
575 lines
13 KiB
Go
575 lines
13 KiB
Go
package fi
|
|
|
|
import (
|
|
"bytes"
|
|
crypto_rand "crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"fmt"
|
|
"github.com/golang/glog"
|
|
"k8s.io/kops/upup/pkg/fi/vfs"
|
|
"math/big"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type VFSCAStore struct {
|
|
DryRun bool
|
|
basedir vfs.Path
|
|
|
|
mutex sync.Mutex
|
|
cacheCaCertificates *certificates
|
|
cacheCaPrivateKeys *privateKeys
|
|
}
|
|
|
|
var _ CAStore = &VFSCAStore{}
|
|
|
|
func NewVFSCAStore(basedir vfs.Path) CAStore {
|
|
c := &VFSCAStore{
|
|
basedir: basedir,
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
func (s *VFSCAStore) VFSPath() vfs.Path {
|
|
return s.basedir
|
|
}
|
|
|
|
// Retrieves the CA keypair, generating a new keypair if not found
|
|
func (s *VFSCAStore) readCAKeypairs() (*certificates, *privateKeys, error) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
if s.cacheCaPrivateKeys != nil {
|
|
return s.cacheCaCertificates, s.cacheCaPrivateKeys, nil
|
|
}
|
|
|
|
caCertificates, err := s.loadCertificates(s.buildCertificatePoolPath(CertificateId_CA))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var caPrivateKeys *privateKeys
|
|
|
|
if caCertificates != nil {
|
|
caPrivateKeys, err = s.loadPrivateKeys(s.buildPrivateKeyPoolPath(CertificateId_CA))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if caPrivateKeys == nil {
|
|
glog.Warningf("CA private key was not found; will generate new key")
|
|
//return nil, fmt.Errorf("error loading CA private key - key not found")
|
|
}
|
|
}
|
|
|
|
if caPrivateKeys == nil {
|
|
caCertificates, caPrivateKeys, err = s.generateCACertificate()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
}
|
|
s.cacheCaCertificates = caCertificates
|
|
s.cacheCaPrivateKeys = caPrivateKeys
|
|
|
|
return s.cacheCaCertificates, s.cacheCaPrivateKeys, nil
|
|
}
|
|
|
|
// Creates and stores CA keypair
|
|
// Should be called with the mutex held, to prevent concurrent creation of different keys
|
|
func (c *VFSCAStore) generateCACertificate() (*certificates, *privateKeys, error) {
|
|
subject := &pkix.Name{
|
|
CommonName: "kubernetes",
|
|
}
|
|
serial := c.buildSerial()
|
|
template := &x509.Certificate{
|
|
Subject: *subject,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{},
|
|
BasicConstraintsValid: true,
|
|
IsCA: true,
|
|
}
|
|
|
|
caRsaKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("error generating RSA private key: %v", err)
|
|
}
|
|
|
|
caPrivateKey := &PrivateKey{Key: caRsaKey}
|
|
|
|
caCertificate, err := SignNewCertificate(caPrivateKey, template, nil, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
keyPath := c.buildPrivateKeyPath(CertificateId_CA, serial)
|
|
err = c.storePrivateKey(caPrivateKey, keyPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Make double-sure it round-trips
|
|
privateKeys, err := c.loadPrivateKeys(c.buildPrivateKeyPoolPath(CertificateId_CA))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if privateKeys == nil || privateKeys.primary != serial.String() {
|
|
return nil, nil, fmt.Errorf("failed to round-trip CA private key")
|
|
}
|
|
|
|
certPath := c.buildCertificatePath(CertificateId_CA, serial)
|
|
err = c.storeCertificate(caCertificate, certPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Make double-sure it round-trips
|
|
certificates, err := c.loadCertificates(c.buildCertificatePoolPath(CertificateId_CA))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if certificates == nil || certificates.primary != serial.String() {
|
|
return nil, nil, fmt.Errorf("failed to round-trip CA certifiacate")
|
|
}
|
|
|
|
return certificates, privateKeys, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) buildCertificatePoolPath(id string) vfs.Path {
|
|
return c.basedir.Join("issued", id)
|
|
}
|
|
|
|
func (c *VFSCAStore) buildCertificatePath(id string, serial *big.Int) vfs.Path {
|
|
return c.basedir.Join("issued", id, serial.String()+".crt")
|
|
}
|
|
|
|
func (c *VFSCAStore) buildPrivateKeyPoolPath(id string) vfs.Path {
|
|
return c.basedir.Join("private", id)
|
|
}
|
|
|
|
func (c *VFSCAStore) buildPrivateKeyPath(id string, serial *big.Int) vfs.Path {
|
|
return c.basedir.Join("private", id, serial.String()+".key")
|
|
}
|
|
|
|
type certificates struct {
|
|
certificates map[string]*Certificate
|
|
primary string
|
|
}
|
|
|
|
func (p *certificates) Primary() *Certificate {
|
|
if p.primary == "" {
|
|
return nil
|
|
}
|
|
return p.certificates[p.primary]
|
|
}
|
|
|
|
func (c *VFSCAStore) loadCertificates(p vfs.Path) (*certificates, error) {
|
|
files, err := p.ReadDir()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
certs := &certificates{
|
|
certificates: make(map[string]*Certificate),
|
|
}
|
|
|
|
for _, f := range files {
|
|
cert, err := c.loadOneCertificate(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error loading certificate %q: %v", f, err)
|
|
}
|
|
name := f.Base()
|
|
name = strings.TrimSuffix(name, ".crt")
|
|
certs.certificates[name] = cert
|
|
}
|
|
|
|
if len(certs.certificates) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var primaryVersion *big.Int
|
|
for k := range certs.certificates {
|
|
version, ok := big.NewInt(0).SetString(k, 10)
|
|
if !ok {
|
|
glog.Warningf("Ignoring certificate with non-integer version: %q", k)
|
|
continue
|
|
}
|
|
|
|
if primaryVersion == nil || version.Cmp(primaryVersion) > 0 {
|
|
certs.primary = k
|
|
primaryVersion = version
|
|
}
|
|
}
|
|
|
|
return certs, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) loadOneCertificate(p vfs.Path) (*Certificate, error) {
|
|
data, err := p.ReadFile()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
cert, err := LoadPEMCertificate(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cert == nil {
|
|
return nil, nil
|
|
}
|
|
return cert, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) Cert(id string) (*Certificate, error) {
|
|
cert, err := c.FindCert(id)
|
|
if err == nil && cert == nil {
|
|
if c.DryRun {
|
|
glog.Warningf("using empty certificate, because running with DryRun")
|
|
return &Certificate{}, err
|
|
}
|
|
return nil, fmt.Errorf("cannot find certificate %q", id)
|
|
}
|
|
return cert, err
|
|
|
|
}
|
|
|
|
func (c *VFSCAStore) CertificatePool(id string) (*CertificatePool, error) {
|
|
cert, err := c.FindCertificatePool(id)
|
|
if err == nil && cert == nil {
|
|
if c.DryRun {
|
|
glog.Warningf("using empty certificate, because running with DryRun")
|
|
return &CertificatePool{}, err
|
|
}
|
|
return nil, fmt.Errorf("cannot find certificate pool %q", id)
|
|
}
|
|
return cert, err
|
|
|
|
}
|
|
|
|
func (c *VFSCAStore) FindCert(id string) (*Certificate, error) {
|
|
var certs *certificates
|
|
|
|
if id == CertificateId_CA {
|
|
caCertificates, _, err := c.readCAKeypairs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs = caCertificates
|
|
} else {
|
|
var err error
|
|
p := c.buildCertificatePoolPath(id)
|
|
certs, err = c.loadCertificates(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var cert *Certificate
|
|
if certs != nil && certs.primary != "" {
|
|
cert = certs.certificates[certs.primary]
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) FindCertificatePool(id string) (*CertificatePool, error) {
|
|
var certs *certificates
|
|
|
|
if id == CertificateId_CA {
|
|
caCertificates, _, err := c.readCAKeypairs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
certs = caCertificates
|
|
} else {
|
|
var err error
|
|
p := c.buildCertificatePoolPath(id)
|
|
certs, err = c.loadCertificates(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
pool := &CertificatePool{}
|
|
|
|
if certs != nil {
|
|
pool.Primary = certs.Primary()
|
|
|
|
for k, cert := range certs.certificates {
|
|
if k == certs.primary {
|
|
continue
|
|
}
|
|
pool.Secondary = append(pool.Secondary, cert)
|
|
}
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) List() ([]string, error) {
|
|
var ids []string
|
|
|
|
issuedDir := c.basedir.Join("issued")
|
|
files, err := issuedDir.ReadDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading directory %q: %v", issuedDir, err)
|
|
}
|
|
|
|
for _, f := range files {
|
|
name := f.Base()
|
|
ids = append(ids, name)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) IssueCert(id string, serial *big.Int, privateKey *PrivateKey, template *x509.Certificate) (*Certificate, error) {
|
|
glog.Infof("Issuing new certificate: %q", id)
|
|
|
|
template.SerialNumber = serial
|
|
|
|
p := c.buildCertificatePath(id, serial)
|
|
|
|
caCertificates, caPrivateKeys, err := c.readCAKeypairs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if caPrivateKeys == nil || caPrivateKeys.Primary() == nil {
|
|
return nil, fmt.Errorf("ca.key was not found; cannot issue certificates")
|
|
}
|
|
cert, err := SignNewCertificate(privateKey, template, caCertificates.Primary().Certificate, caPrivateKeys.Primary())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = c.storeCertificate(cert, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Make double-sure it round-trips
|
|
return c.loadOneCertificate(p)
|
|
}
|
|
|
|
func (c *VFSCAStore) AddCert(id string, cert *Certificate) error {
|
|
glog.Infof("Issuing new certificate: %q", id)
|
|
|
|
// We add with a timestamp of zero so this will never be the newest cert
|
|
serial := buildSerial(0)
|
|
|
|
p := c.buildCertificatePath(id, serial)
|
|
|
|
err := c.storeCertificate(cert, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make double-sure it round-trips
|
|
_, err = c.loadOneCertificate(p)
|
|
return err
|
|
}
|
|
|
|
type privateKeys struct {
|
|
keys map[string]*PrivateKey
|
|
primary string
|
|
}
|
|
|
|
func (p *privateKeys) Primary() *PrivateKey {
|
|
if p.primary == "" {
|
|
return nil
|
|
}
|
|
return p.keys[p.primary]
|
|
}
|
|
|
|
func (c *VFSCAStore) loadPrivateKeys(p vfs.Path) (*privateKeys, error) {
|
|
files, err := p.ReadDir()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
keys := &privateKeys{
|
|
keys: make(map[string]*PrivateKey),
|
|
}
|
|
|
|
for _, f := range files {
|
|
key, err := c.loadOnePrivateKey(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error loading private key %q: %v", f, err)
|
|
}
|
|
name := f.Base()
|
|
name = strings.TrimSuffix(name, ".key")
|
|
keys.keys[name] = key
|
|
}
|
|
|
|
if len(keys.keys) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var primaryVersion *big.Int
|
|
for k := range keys.keys {
|
|
version, ok := big.NewInt(0).SetString(k, 10)
|
|
if !ok {
|
|
glog.Warningf("Ignoring private key with non-integer version: %q", k)
|
|
continue
|
|
}
|
|
|
|
if primaryVersion == nil || version.Cmp(primaryVersion) > 0 {
|
|
keys.primary = k
|
|
primaryVersion = version
|
|
}
|
|
}
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) loadOnePrivateKey(p vfs.Path) (*PrivateKey, error) {
|
|
data, err := p.ReadFile()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
k, err := ParsePEMPrivateKey(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing private key from %q: %v", p, err)
|
|
}
|
|
return k, err
|
|
}
|
|
|
|
func ParsePEMPrivateKey(data []byte) (*PrivateKey, error) {
|
|
k, err := parsePEMPrivateKey(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if k == nil {
|
|
return nil, nil
|
|
}
|
|
return &PrivateKey{Key: k}, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) FindPrivateKey(id string) (*PrivateKey, error) {
|
|
var keys *privateKeys
|
|
if id == CertificateId_CA {
|
|
_, caPrivateKeys, err := c.readCAKeypairs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys = caPrivateKeys
|
|
} else {
|
|
var err error
|
|
p := c.buildPrivateKeyPoolPath(id)
|
|
keys, err = c.loadPrivateKeys(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
}
|
|
|
|
var key *PrivateKey
|
|
if keys != nil && keys.primary != "" {
|
|
key = keys.keys[keys.primary]
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) PrivateKey(id string) (*PrivateKey, error) {
|
|
key, err := c.FindPrivateKey(id)
|
|
if err == nil && key == nil {
|
|
if c.DryRun {
|
|
glog.Warningf("using empty certificate, because running with DryRun")
|
|
return &PrivateKey{}, err
|
|
}
|
|
return nil, fmt.Errorf("cannot find SSL key %q", id)
|
|
}
|
|
return key, err
|
|
|
|
}
|
|
|
|
func (c *VFSCAStore) CreateKeypair(id string, template *x509.Certificate) (*Certificate, *PrivateKey, error) {
|
|
serial := c.buildSerial()
|
|
|
|
privateKey, err := c.CreatePrivateKey(id, serial)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
cert, err := c.IssueCert(id, serial, privateKey, template)
|
|
if err != nil {
|
|
// TODO: Delete cert?
|
|
return nil, nil, err
|
|
}
|
|
|
|
return cert, privateKey, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) CreatePrivateKey(id string, serial *big.Int) (*PrivateKey, error) {
|
|
p := c.buildPrivateKeyPath(id, serial)
|
|
|
|
rsaKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error generating RSA private key: %v", err)
|
|
}
|
|
|
|
privateKey := &PrivateKey{Key: rsaKey}
|
|
err = c.storePrivateKey(privateKey, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return privateKey, nil
|
|
}
|
|
|
|
func (c *VFSCAStore) storePrivateKey(privateKey *PrivateKey, p vfs.Path) error {
|
|
var data bytes.Buffer
|
|
_, err := privateKey.WriteTo(&data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return p.WriteFile(data.Bytes())
|
|
}
|
|
|
|
func (c *VFSCAStore) storeCertificate(cert *Certificate, p vfs.Path) error {
|
|
// TODO: replace storePrivateKey & storeCertificate with writeFile(io.WriterTo)?
|
|
var data bytes.Buffer
|
|
_, err := cert.WriteTo(&data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return p.WriteFile(data.Bytes())
|
|
}
|
|
|
|
func (c *VFSCAStore) buildSerial() *big.Int {
|
|
t := time.Now().UnixNano()
|
|
return buildSerial(t)
|
|
}
|
|
|
|
func buildSerial(timestamp int64) *big.Int {
|
|
randomLimit := new(big.Int).Lsh(big.NewInt(1), 32)
|
|
randomComponent, err := crypto_rand.Int(crypto_rand.Reader, randomLimit)
|
|
if err != nil {
|
|
glog.Fatalf("error generating random number: %v", err)
|
|
}
|
|
|
|
serial := big.NewInt(timestamp)
|
|
serial.Lsh(serial, 32)
|
|
serial.Or(serial, randomComponent)
|
|
|
|
return serial
|
|
}
|