From 9641f524616f4525d431181b052187f434745d13 Mon Sep 17 00:00:00 2001 From: Kent Rancourt Date: Thu, 5 Oct 2017 10:54:55 -0600 Subject: [PATCH] add cert-generation functions --- crypto.go | 232 +++++++++++++++++++++++++++++++++++++++++++++++++ crypto_test.go | 117 +++++++++++++++++++++++++ docs/crypto.md | 69 ++++++++++++++- functions.go | 7 +- 4 files changed, 421 insertions(+), 4 deletions(-) diff --git a/crypto.go b/crypto.go index a935b6c1..d1b46500 100644 --- a/crypto.go +++ b/crypto.go @@ -10,12 +10,16 @@ import ( "crypto/rsa" "crypto/sha256" "crypto/x509" + "crypto/x509/pkix" "encoding/asn1" "encoding/binary" "encoding/hex" "encoding/pem" + "errors" "fmt" "math/big" + "net" + "time" uuid "github.com/satori/go.uuid" "golang.org/x/crypto/scrypt" @@ -146,3 +150,231 @@ func pemBlockForKey(priv interface{}) *pem.Block { return nil } } + +type certificate struct { + Cert string + Key string +} + +func generateCertificateAuthority( + cn string, + daysValid int, +) (certificate, error) { + ca := certificate{} + + template, err := getBaseCertTemplate(cn, nil, nil, daysValid) + if err != nil { + return ca, err + } + // Override KeyUsage and IsCA + template.KeyUsage = x509.KeyUsageKeyEncipherment | + x509.KeyUsageDigitalSignature | + x509.KeyUsageCertSign + template.IsCA = true + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return ca, fmt.Errorf("error generating rsa key: %s", err) + } + + ca.Cert, ca.Key, err = getCertAndKey(template, priv, template, priv) + if err != nil { + return ca, err + } + + return ca, nil +} + +func generateSelfSignedCertificate( + cn string, + ips []interface{}, + alternateDNS []interface{}, + daysValid int, +) (certificate, error) { + cert := certificate{} + + template, err := getBaseCertTemplate(cn, ips, alternateDNS, daysValid) + if err != nil { + return cert, err + } + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return cert, fmt.Errorf("error generating rsa key: %s", err) + } + + cert.Cert, cert.Key, err = getCertAndKey(template, priv, template, priv) + if err != nil { + return cert, err + } + + return cert, nil +} + +func generateSignedCertificate( + cn string, + ips []interface{}, + alternateDNS []interface{}, + daysValid int, + ca certificate, +) (certificate, error) { + cert := certificate{} + + decodedSignerCert, _ := pem.Decode([]byte(ca.Cert)) + if decodedSignerCert == nil { + return cert, errors.New("unable to decode certificate") + } + signerCert, err := x509.ParseCertificate(decodedSignerCert.Bytes) + if err != nil { + return cert, fmt.Errorf( + "error parsing certificate: decodedSignerCert.Bytes: %s", + err, + ) + } + decodedSignerKey, _ := pem.Decode([]byte(ca.Key)) + if decodedSignerKey == nil { + return cert, errors.New("unable to decode key") + } + signerKey, err := x509.ParsePKCS1PrivateKey(decodedSignerKey.Bytes) + if err != nil { + return cert, fmt.Errorf( + "error parsing prive key: decodedSignerKey.Bytes: %s", + err, + ) + } + + template, err := getBaseCertTemplate(cn, ips, alternateDNS, daysValid) + if err != nil { + return cert, err + } + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return cert, fmt.Errorf("error generating rsa key: %s", err) + } + + cert.Cert, cert.Key, err = getCertAndKey( + template, + priv, + signerCert, + signerKey, + ) + if err != nil { + return cert, err + } + + return cert, nil +} + +func getCertAndKey( + template *x509.Certificate, + signeeKey *rsa.PrivateKey, + parent *x509.Certificate, + signingKey *rsa.PrivateKey, +) (string, string, error) { + derBytes, err := x509.CreateCertificate( + rand.Reader, + template, + parent, + &signeeKey.PublicKey, + signingKey, + ) + if err != nil { + return "", "", fmt.Errorf("error creating certificate: %s", err) + } + + certBuffer := bytes.Buffer{} + if err := pem.Encode( + &certBuffer, + &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}, + ); err != nil { + return "", "", fmt.Errorf("error pem-encoding certificate: %s", err) + } + + keyBuffer := bytes.Buffer{} + if err := pem.Encode( + &keyBuffer, + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(signeeKey), + }, + ); err != nil { + return "", "", fmt.Errorf("error pem-encoding key: %s", err) + } + + return string(certBuffer.Bytes()), string(keyBuffer.Bytes()), nil +} + +func getBaseCertTemplate( + cn string, + ips []interface{}, + alternateDNS []interface{}, + daysValid int, +) (*x509.Certificate, error) { + ipAddresses, err := getNetIPs(ips) + if err != nil { + return nil, err + } + dnsNames, err := getAlternateDNSStrs(alternateDNS) + if err != nil { + return nil, err + } + return &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: cn, + }, + IPAddresses: ipAddresses, + DNSNames: dnsNames, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour * 24 * time.Duration(daysValid)), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageClientAuth, + }, + BasicConstraintsValid: true, + }, nil +} + +func getNetIPs(ips []interface{}) ([]net.IP, error) { + if ips == nil { + return []net.IP{}, nil + } + var ipStr string + var ok bool + var netIP net.IP + netIPs := make([]net.IP, len(ips)) + for i, ip := range ips { + ipStr, ok = ip.(string) + if !ok { + return nil, fmt.Errorf("error parsing ip: %v is not a string", ip) + } + netIP = net.ParseIP(ipStr) + if netIP == nil { + return nil, fmt.Errorf("error parsing ip: %s", ipStr) + } + netIPs[i] = netIP + } + return netIPs, nil +} + +func getAlternateDNSStrs(alternateDNS []interface{}) ([]string, error) { + if alternateDNS == nil { + return []string{}, nil + } + var dnsStr string + var ok bool + alternateDNSStrs := make([]string, len(alternateDNS)) + for i, dns := range alternateDNS { + dnsStr, ok = dns.(string) + if !ok { + return nil, fmt.Errorf( + "error processing alternate dns name: %v is not a string", + dns, + ) + } + alternateDNSStrs[i] = dnsStr + } + return alternateDNSStrs, nil +} diff --git a/crypto_test.go b/crypto_test.go index 01b3bfcc..5da06a72 100644 --- a/crypto_test.go +++ b/crypto_test.go @@ -1,8 +1,18 @@ package sprig import ( + "crypto/x509" + "encoding/pem" + "fmt" "strings" "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + beginCertificate = "-----BEGIN CERTIFICATE-----" + endCertificate = "-----END CERTIFICATE-----" ) func TestSha256Sum(t *testing.T) { @@ -108,3 +118,110 @@ func TestUUIDGeneration(t *testing.T) { t.Error("Expected subsequent UUID generations to be different") } } + +func TestGenCA(t *testing.T) { + const cn = "foo-ca" + + tpl := fmt.Sprintf( + `{{- $ca := genCA "%s" 365 }} +{{ $ca.Cert }} +`, + cn, + ) + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + assert.Contains(t, out, beginCertificate) + assert.Contains(t, out, endCertificate) + + decodedCert, _ := pem.Decode([]byte(out)) + assert.Nil(t, err) + cert, err := x509.ParseCertificate(decodedCert.Bytes) + assert.Nil(t, err) + + assert.Equal(t, cn, cert.Subject.CommonName) + assert.True(t, cert.IsCA) +} + +func TestGenSelfSignedCert(t *testing.T) { + const ( + cn = "foo.com" + ip1 = "10.0.0.1" + ip2 = "10.0.0.2" + dns1 = "bar.com" + dns2 = "bat.com" + ) + + tpl := fmt.Sprintf( + `{{- $cert := genSelfSignedCert "%s" (list "%s" "%s") (list "%s" "%s") 365 }} +{{ $cert.Cert }}`, + cn, + ip1, + ip2, + dns1, + dns2, + ) + + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + assert.Contains(t, out, beginCertificate) + assert.Contains(t, out, endCertificate) + + decodedCert, _ := pem.Decode([]byte(out)) + assert.Nil(t, err) + cert, err := x509.ParseCertificate(decodedCert.Bytes) + assert.Nil(t, err) + + assert.Equal(t, cn, cert.Subject.CommonName) + assert.Equal(t, 2, len(cert.IPAddresses)) + assert.Equal(t, ip1, cert.IPAddresses[0].String()) + assert.Equal(t, ip2, cert.IPAddresses[1].String()) + assert.Contains(t, cert.DNSNames, dns1) + assert.Contains(t, cert.DNSNames, dns2) + assert.False(t, cert.IsCA) +} + +func TestGenSignedCert(t *testing.T) { + const ( + cn = "foo.com" + ip1 = "10.0.0.1" + ip2 = "10.0.0.2" + dns1 = "bar.com" + dns2 = "bat.com" + ) + + tpl := fmt.Sprintf( + `{{- $ca := genCA "foo" 365 }} +{{- $cert := genSignedCert "%s" (list "%s" "%s") (list "%s" "%s") 365 $ca }} +{{ $cert.Cert }} +`, + cn, + ip1, + ip2, + dns1, + dns2, + ) + out, err := runRaw(tpl, nil) + if err != nil { + t.Error(err) + } + + assert.Contains(t, out, beginCertificate) + assert.Contains(t, out, endCertificate) + + decodedCert, _ := pem.Decode([]byte(out)) + assert.Nil(t, err) + cert, err := x509.ParseCertificate(decodedCert.Bytes) + assert.Nil(t, err) + + assert.Equal(t, cn, cert.Subject.CommonName) + assert.Equal(t, 2, len(cert.IPAddresses)) + assert.Equal(t, ip1, cert.IPAddresses[0].String()) + assert.Equal(t, ip2, cert.IPAddresses[1].String()) + assert.Contains(t, cert.DNSNames, dns1) + assert.Contains(t, cert.DNSNames, dns2) + assert.False(t, cert.IsCA) +} diff --git a/docs/crypto.md b/docs/crypto.md index d9d12345..8903b5bd 100644 --- a/docs/crypto.md +++ b/docs/crypto.md @@ -25,9 +25,9 @@ derivePassword 1 "long" "password" "user" "example.com" Note that it is considered insecure to store the parts directly in the template. -## generatePrivateKey +## genPrivateKey -The `generatePrivateKey` function generates a new private key encoded into a PEM +The `genPrivateKey` function generates a new private key encoded into a PEM block. It takes one of the values for its first param: @@ -35,3 +35,68 @@ It takes one of the values for its first param: - `ecdsa`: Generate an elyptical curve DSA key (P256) - `dsa`: Generate a DSA key (L2048N256) - `rsa`: Generate an RSA 4096 key + +## genCA + +The `genCA` function generates a new, self-signed x509 certificate authority. + +It takes the following parameters: + +- Subject's common name (cn) +- Cert validity duration in days + +It returns an object with the following attributes: + +- `Cert`: A PEM-encoded certificate +- `Key`: A PEM-encoded private key + +Example: + +``` +$ca := genCA "foo-ca" 365 +``` + +Note that the returned object can be passed to the `genSignedCert` function +to sign a certificate using this CA. + +## genSelfSignedCert + +The `genSelfSignedCert` function generates a new, self-signed x509 certificate. + +It takes the following parameters: + +- Subject's common name (cn) +- Optional list of IPs; may be nil +- Optional list of alternate DNS names; may be nil +- Cert validity duration in days + +It returns an object with the following attributes: + +- `Cert`: A PEM-encoded certificate +- `Key`: A PEM-encoded private key + +Example: + +``` +$cert := genSelfSignedCert "foo.com" (list "10.0.0.1" "10.0.0.2") (list "bar.com" "bat.com") 365 +``` + +## genSignedCert + +The `genSignedCert` function generates a new, x509 certificate signed by the +specified CA. + +It takes the following parameters: + +- Subject's common name (cn) +- Optional list of IPs; may be nil +- Optional list of alternate DNS names; may be nil +- Cert validity duration in days +- CA (see `genCA`) + +Example: + +``` +$ca := genCA "foo-ca" 365 +$cert := genSignedCert "foo.com" (list "10.0.0.1" "10.0.0.2") (list "bar.com" "bat.com") 365 $ca +``` diff --git a/functions.go b/functions.go index 555eece3..e30c0ba2 100644 --- a/functions.go +++ b/functions.go @@ -252,8 +252,11 @@ var genericMap = map[string]interface{}{ "has": func(needle interface{}, haystack []interface{}) bool { return inList(haystack, needle) }, // Crypto: - "genPrivateKey": generatePrivateKey, - "derivePassword": derivePassword, + "genPrivateKey": generatePrivateKey, + "derivePassword": derivePassword, + "genCA": generateCertificateAuthority, + "genSelfSignedCert": generateSelfSignedCertificate, + "genSignedCert": generateSignedCertificate, // UUIDs: "uuidv4": uuidv4,