Move cert issuance code to pki module

This commit is contained in:
John Gardiner Myers 2020-05-15 21:25:35 -07:00
parent 2bfdf8a9c3
commit c142483cfa
8 changed files with 205 additions and 229 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

@ -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",
],

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
}

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

@ -0,0 +1,144 @@
/*
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"
"math/big"
"net"
"strings"
)
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.
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
// 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)
}
func IssueCert(request *IssueCertRequest, keystore Keystore) (*Certificate, *PrivateKey, 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, 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, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.ExtKeyUsage = append(template.ExtKeyUsage, ku)
} else if t == "CA" {
template.IsCA = true
} else {
return 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 caCertificate *x509.Certificate
var caPrivateKey *PrivateKey
if !template.IsCA {
var err error
var caCert *Certificate
caCert, caPrivateKey, _, err = keystore.FindKeypair(request.Signer)
if err != nil {
return nil, nil, err
}
if caPrivateKey == nil {
return nil, nil, fmt.Errorf("ca key for %q was not found; cannot issue certificates", request.Signer)
}
if caCert == nil {
return nil, nil, fmt.Errorf("ca certificate for %q was not found; cannot issue certificates", request.Signer)
}
caCertificate = caCert.Certificate
}
privateKey := request.PrivateKey
if privateKey == nil {
var err error
privateKey, err = GeneratePrivateKey()
if err != nil {
return nil, nil, err
}
}
certificate, err := SignNewCertificate(privateKey, template, caCertificate, caPrivateKey)
if err != nil {
return nil, nil, err
}
return certificate, privateKey, err
}

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,11 +17,8 @@ limitations under the License.
package fitasks
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net"
"sort"
"strings"
"time"
@ -31,13 +28,6 @@ import (
"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
@ -100,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
@ -227,7 +217,7 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
return fmt.Errorf("subject name was empty for SSL keypair %q", *e.Name)
}
req := issueCertRequest{
req := pki.IssueCertRequest{
Signer: signer,
Type: e.Type,
Subject: *subjectPkix,
@ -235,7 +225,7 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
PrivateKey: privateKey,
Serial: serial,
}
cert, privateKey, err := issueCert(&req, c.Keystore)
cert, privateKey, err := pki.IssueCert(&req, c.Keystore)
if err != nil {
return err
}
@ -273,130 +263,28 @@ func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
return nil
}
type issueCertRequest struct {
// Signer is the keypair to use to sign.
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
func parsePkixName(s string) (*pkix.Name, error) {
name := new(pkix.Name)
// PrivateKey is the private key for this certificate. If nil, a new private key will be generated.
PrivateKey *pki.PrivateKey
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]
// Serial is the certificate serial number. If nil, a random number will be generated.
Serial *big.Int
}
func issueCert(request *issueCertRequest, keystore fi.Keystore) (*pki.Certificate, *pki.PrivateKey, 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, 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, fmt.Errorf("unrecognized certificate option: %v", t)
}
template.ExtKeyUsage = append(template.ExtKeyUsage, ku)
} else if t == "CA" {
template.IsCA = true
} else {
return 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 caCertificate *x509.Certificate
var caPrivateKey *pki.PrivateKey
if !template.IsCA {
var err error
var caCert *pki.Certificate
caCert, caPrivateKey, _, err = keystore.FindKeypair(request.Signer)
if err != nil {
return nil, nil, err
}
if caPrivateKey == nil {
return nil, nil, fmt.Errorf("ca key for %q was not found; cannot issue certificates", request.Signer)
}
if caCert == nil {
return nil, nil, fmt.Errorf("ca certificate for %q was not found; cannot issue certificates", request.Signer)
}
caCertificate = caCert.Certificate
}
privateKey := request.PrivateKey
if privateKey == nil {
var err error
privateKey, err = pki.GeneratePrivateKey()
if err != nil {
return nil, nil, err
}
}
certificate, err := pki.SignNewCertificate(privateKey, template, caCertificate, caPrivateKey)
if err != nil {
return nil, nil, err
}
return certificate, privateKey, err
}
// 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
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
}