Merge "Implement genCAEx and genSignedCertEx with Subj support"

This commit is contained in:
Zuul 2021-02-01 20:22:53 +00:00 committed by Gerrit Code Review
commit e77bac1571
13 changed files with 1265 additions and 35 deletions

View File

@ -0,0 +1,365 @@
/*
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
https://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 extlib
import (
"bytes"
"errors"
"fmt"
"strings"
"unicode/utf8"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"time"
)
func toUint32(i int) uint32 { return uint32(i) }
type dnParser struct {
in string
i int
cur bytes.Buffer
state int
dn []string
}
func (p *dnParser) startOver() {
p.dn = append(p.dn, p.cur.String())
p.cur = bytes.Buffer{}
p.state = 0
}
func (p *dnParser) parseParam(r rune) error {
switch r {
case '\\':
p.state = 2
case '+', '/':
return fmt.Errorf("string %s has separator '%c', but didn't have value on position %d", p.in, r, p.i+1)
case '=':
p.state = 1
p.cur.WriteRune(r)
case '"', ',', '<', '>', ';':
return fmt.Errorf("string %s position %d: having %c without '\\'", p.in, p.i+1, r)
default:
p.cur.WriteRune(r)
}
return nil
}
func (p *dnParser) parseValue(r rune) error {
switch r {
case '\\':
p.state = 3
case '+', '/':
p.startOver()
case '=':
return fmt.Errorf("string %s has extra '=' on position %d", p.in, p.i+1)
case '"', ',', '<', '>', ';':
return fmt.Errorf("string %s position %d: having %c without '\\'", p.in, p.i+1, r)
default:
p.cur.WriteRune(r)
}
return nil
}
func (p *dnParser) parseParamEscape(r rune) error {
switch r {
case '=', '+', '/', '"', ',', '<', '>', ';':
p.cur.WriteRune(r)
p.state = 0
default:
return fmt.Errorf("string %s pos %d: %c shouldn't follow after '\\'", p.in, p.i+1, r)
}
return nil
}
func (p *dnParser) parseValueEscape(r rune) error {
switch r {
case '=', '+', '/', '"', ',', '<', '>', ';':
p.cur.WriteRune(r)
p.state = 1
default:
return fmt.Errorf("string %s pos %d: %c shouldn't follow after '\\'", p.in, p.i+1, r)
}
return nil
}
func (p *dnParser) Parse(in string) error {
p.cur = bytes.Buffer{}
p.state = 0
p.dn = nil
p.in = in
var err error
for p.i = 0; p.i < len(p.in); {
r, size := utf8.DecodeRuneInString(p.in[p.i:])
switch p.state {
case 0: // initial state
err = p.parseParam(r)
case 1: // the same, but after =
err = p.parseValue(r)
case 2: // state inside \
err = p.parseParamEscape(r)
case 3: // state inside \ after =
err = p.parseValueEscape(r)
}
if err != nil {
return err
}
p.i += size
}
if p.state != 1 {
return fmt.Errorf("string %s terminates incorrectly", p.in)
}
p.startOver()
return nil
}
// Converts RFC 2253 Distinguished Names syntax back to
// Name similar to what openssl parse_name function [1]
// does, except that if it doesn't have / as a first simbol
// it assumes that the whole string is CN.
// we don't support MultiRdn - + is treated the same way as /.
// [1] https://github.com/openssl/openssl/blob/d8ab30be9cc4d4e77008d4037e696bc41ce293f8/apps/lib/apps.c#L1624
func nameFromString(in string) (*pkix.Name, error) {
if len(in) > 0 && in[0] != '/' {
return &pkix.Name{
CommonName: in,
}, nil
}
in = in[1:]
if len(in) == 0 {
return &pkix.Name{
CommonName: in,
}, nil
}
p := &dnParser{}
err := p.Parse(in)
if err != nil {
return nil, err
}
return nameFromDn(p.dn)
}
func nameFromDn(dn []string) (*pkix.Name, error) {
name := pkix.Name{}
for _, v := range dn {
sv := strings.Split(v, "=")
if len(sv) != 2 {
return nil, fmt.Errorf("%s must have a form key=value", v)
}
switch sv[0] {
case "CN":
if name.CommonName != "" {
return nil, fmt.Errorf("CN is already set")
}
name.CommonName = sv[1]
case "SERIALNUMBER":
if name.SerialNumber != "" {
return nil, fmt.Errorf("SERIALNUMBER is already set")
}
name.SerialNumber = sv[1]
case "C":
name.Country = append(name.Country, sv[1])
case "O":
name.Organization = append(name.Organization, sv[1])
case "OU":
name.OrganizationalUnit = append(name.OrganizationalUnit, sv[1])
case "L":
name.Locality = append(name.Locality, sv[1])
case "ST":
name.Province = append(name.Province, sv[1])
case "STREET":
name.StreetAddress = append(name.StreetAddress, sv[1])
case "POSTALCODE":
name.PostalCode = append(name.PostalCode, sv[1])
default:
return nil, fmt.Errorf("unsupported property %s", sv[0])
}
}
return &name, nil
}
func generateCertificateAuthorityEx(
subj string,
daysValid int,
) (certificate, error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return certificate{}, fmt.Errorf("error generating rsa key: %s", err)
}
return generateCertificateAuthorityWithKeyInternalEx(subj, daysValid, priv)
}
func generateCertificateAuthorityWithPEMKeyEx(
subj string,
daysValid int,
privPEM string,
) (certificate, error) {
priv, err := parsePrivateKeyPEM(privPEM)
if err != nil {
return certificate{}, fmt.Errorf("parsing private key: %s", err)
}
return generateCertificateAuthorityWithKeyInternalEx(subj, daysValid, priv)
}
func generateCertificateAuthorityWithKeyInternalEx(
subj string,
daysValid int,
priv crypto.PrivateKey,
) (certificate, error) {
ca := certificate{}
template, err := getBaseCertTemplateEx(subj, nil, nil, daysValid)
if err != nil {
return ca, err
}
// Override KeyUsage and IsCA
template.KeyUsage = x509.KeyUsageKeyEncipherment |
x509.KeyUsageDigitalSignature |
x509.KeyUsageCertSign
template.IsCA = true
ca.Cert, ca.Key, err = getCertAndKey(template, priv, template, priv)
return ca, err
}
func generateSignedCertificateEx(
subj string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
ca certificate,
) (certificate, error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return certificate{}, fmt.Errorf("error generating rsa key: %s", err)
}
return generateSignedCertificateWithKeyInternalEx(subj, ips, alternateDNS, daysValid, ca, priv)
}
func generateSignedCertificateWithPEMKeyEx(
subj string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
ca certificate,
privPEM string,
) (certificate, error) {
priv, err := parsePrivateKeyPEM(privPEM)
if err != nil {
return certificate{}, fmt.Errorf("parsing private key: %s", err)
}
return generateSignedCertificateWithKeyInternalEx(subj, ips, alternateDNS, daysValid, ca, priv)
}
func generateSignedCertificateWithKeyInternalEx(
subj string,
ips []interface{},
alternateDNS []interface{},
daysValid int,
ca certificate,
priv crypto.PrivateKey,
) (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,
)
}
signerKey, err := parsePrivateKeyPEM(ca.Key)
if err != nil {
return cert, fmt.Errorf(
"error parsing private key: %s",
err,
)
}
template, err := getBaseCertTemplateEx(subj, ips, alternateDNS, daysValid)
if err != nil {
return cert, err
}
cert.Cert, cert.Key, err = getCertAndKey(
template,
priv,
signerCert,
signerKey,
)
return cert, err
}
func getBaseCertTemplateEx(
subj 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
}
serialNumberUpperBound := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberUpperBound)
if err != nil {
return nil, err
}
name, err := nameFromString(subj)
if err != nil {
return nil, err
}
return &x509.Certificate{
SerialNumber: serialNumber,
Subject: *name,
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
}

View File

@ -0,0 +1,170 @@
/*
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
https://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 extlib
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"crypto/x509/pkix"
)
func TestToUint32(t *testing.T) {
assert.Equal(t, uint32(1), toUint32(1))
assert.Equal(t, uint32(0xffffffff), toUint32(-1))
}
func TestNameFromString(t *testing.T) {
testCases := []struct {
in string
expectedOut pkix.Name
expectedErr string
}{
{
in: `Kubernetes API`,
expectedOut: pkix.Name{
CommonName: `Kubernetes API`,
},
},
{
in: `/CN=Kubernetes API`,
expectedOut: pkix.Name{
CommonName: `Kubernetes API`,
},
},
{
in: `/CN=James \"Jim\" Smith\, III+O=example`,
expectedOut: pkix.Name{
CommonName: `James "Jim" Smith, III`,
Organization: []string{
`example`,
},
},
},
{
in: `/CN=admin/O=system:masters`,
expectedOut: pkix.Name{
CommonName: `admin`,
Organization: []string{
`system:masters`,
},
},
},
{
in: `/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/CN=leaf`,
expectedOut: pkix.Name{
CommonName: `leaf`,
Country: []string{
`AU`,
},
Province: []string{
`Some-State`,
},
Organization: []string{
`Internet Widgits Pty Ltd`,
},
},
},
{
in: `/C=AU/ST=QLD/CN=SSLeay\/rsa test cert`,
expectedOut: pkix.Name{
CommonName: `SSLeay/rsa test cert`,
Country: []string{
`AU`,
},
Province: []string{
`QLD`,
},
},
},
{
in: `/CN=CN/SERIALNUMBER=SN` +
`/C=C1/C=C2` +
`/O=O1/O=O2` +
`/OU=OU1/OU=OU2` +
`/L=L1/L=L2` +
`/ST=ST1/ST=ST2` +
`/STREET=S1/STREET=S2` +
`/POSTALCODE=PC1/POSTALCODE=PC2`,
expectedOut: pkix.Name{
CommonName: `CN`,
SerialNumber: `SN`,
Country: []string{`C1`, `C2`},
Organization: []string{`O1`, `O2`},
OrganizationalUnit: []string{`OU1`, `OU2`},
Locality: []string{`L1`, `L2`},
Province: []string{`ST1`, `ST2`},
StreetAddress: []string{`S1`, `S2`},
PostalCode: []string{`PC1`, `PC2`},
},
},
{
in: `/C=AU/ST=QLD/CN=SSLeay\/rsa test cert\`,
expectedErr: `string C=AU/ST=QLD/CN=SSLeay\/rsa test cert\ terminates incorrectly`,
},
{
in: `/C=A\U/ST=QLD/CN=SSLeay\/rsa test cert`,
expectedErr: `string C=A\U/ST=QLD/CN=SSLeay\/rsa test cert pos 5: U shouldn't follow after '\'`,
},
{
in: `/C\N=AU/ST=QLD/CN=SSLeay\/rsa test cert`,
expectedErr: `string C\N=AU/ST=QLD/CN=SSLeay\/rsa test cert pos 3: N shouldn't follow after '\'`,
},
{
in: `/CN=AU/ST=QLD/CN=SSLeay\/rsa <>",test cert`,
expectedErr: `string CN=AU/ST=QLD/CN=SSLeay\/rsa <>",test cert position 29: having < without '\'`,
},
{
in: `/CN=AU=AU/ST=QLD/CN=SSLeay\/rsa test cert`,
expectedErr: `string CN=AU=AU/ST=QLD/CN=SSLeay\/rsa test cert has extra '=' on position 6`,
},
{
in: `/CN=AU/ST=QLD/CN<>",t=SSLeay\/rsa test cert`,
expectedErr: `string CN=AU/ST=QLD/CN<>",t=SSLeay\/rsa test cert position 16: having < without '\'`,
},
{
in: `/CN=AU/ST/CN=SSLeay\/rsa <>",test cert`,
expectedErr: `string CN=AU/ST/CN=SSLeay\/rsa <>",test cert has separator '/', but didn't have value on position 9`,
},
{
in: `/CN=AU/CN\<=SSLeay test cert`,
expectedErr: `unsupported property CN<`,
},
{
in: `/CN=/SP=xxx/CN=SSLeay\/rsa test cert`,
expectedErr: `unsupported property SP`,
},
{
in: `/CN=1/CN=SSLeay\/rsa test cert`,
expectedErr: `CN is already set`,
},
{
in: `/CN=1/SERIALNUMBER=1/SERIALNUMBER=2`,
expectedErr: `SERIALNUMBER is already set`,
},
}
for _, tc := range testCases {
r, err := nameFromString(tc.in)
if tc.expectedErr != "" {
assert.EqualError(t, err, tc.expectedErr)
continue
}
require.NoError(t, err)
assert.Equal(t, tc.expectedOut, *r)
}
}

View File

@ -0,0 +1,24 @@
/*
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
https://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 extlib
import (
"opendev.org/airship/airshipctl/pkg/fs"
)
func fileExists(path string) bool {
docfs := fs.NewDocumentFs()
return docfs.Exists(path)
}

View File

@ -0,0 +1,26 @@
/*
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
https://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 extlib
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFileExists(t *testing.T) {
assert.Equal(t, true, fileExists("fs_test.go"))
assert.Equal(t, false, fileExists("fs_test_nonexistent.go"))
}

View File

@ -0,0 +1,39 @@
/*
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
https://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 extlib
import (
"text/template"
)
// GenericFuncMap returns a copy of the function map
func GenericFuncMap() template.FuncMap {
gfm := make(template.FuncMap, len(genericMap))
for k, v := range genericMap {
gfm[k] = v
}
return gfm
}
var genericMap = map[string]interface{}{
"genCAEx": generateCertificateAuthorityEx,
"genCAWithKeyEx": generateCertificateAuthorityWithPEMKeyEx,
"genSignedCertEx": generateSignedCertificateEx,
"genSignedCertWithKeyEx": generateSignedCertificateWithPEMKeyEx,
"fileExists": fileExists,
"regexGen": regexGen,
"toYaml": toYaml,
"toUint32": toUint32,
}

View File

@ -0,0 +1,31 @@
/*
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
https://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 extlib
import (
"github.com/lucasjones/reggen"
)
// Generate Regex
func regexGen(regex string, limit int) string {
if limit <= 0 {
panic("Limit cannot be less than or equal to 0")
}
str, err := reggen.Generate(regex, limit)
if err != nil {
panic(err)
}
return str
}

View File

@ -0,0 +1,57 @@
/*
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
https://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 extlib
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRegexGen(t *testing.T) {
tpl := `{{- $regex := "^[a-z]{5,10}$" }}
{{- $nomatchregex := "^[a-z]{0,4}$" }}
true={{- regexMatch $regex (regexGen $regex 10) }},
false={{- regexMatch $nomatchregex (regexGen $regex 10) }}
`
out, err := runRaw(tpl, nil)
if err != nil {
t.Error(err)
}
assert.Equal(t, `
true=true,
false=false
`, out)
}
func TestRegexPanicOnPattern(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()
regexGen("[a-z", 1)
}
func TestRegexPanicOnLimit(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()
regexGen("[a-z]{0,4}", 0)
}

View File

@ -0,0 +1,220 @@
/*
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
https://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 extlib
import (
"bytes"
"errors"
"fmt"
"strings"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"math/big"
"net"
)
// that pieces of code were copied from
// https://github.com/Masterminds/sprig/blob/868e7517d046cb7540e10345b09c0d70da584c8e/crypto.go#L405
type certificate struct {
Cert string
Key string
}
// DSAKeyFormat stores the format for DSA keys.
// Used by pemBlockForKey
type DSAKeyFormat struct {
Version int
P, Q, G, Y, X *big.Int
}
func getCertAndKey(
template *x509.Certificate,
signeeKey crypto.PrivateKey,
parent *x509.Certificate,
signingKey crypto.PrivateKey,
) (string, string, error) {
signeePubKey, err := getPublicKey(signeeKey)
if err != nil {
return "", "", fmt.Errorf("error retrieving public key from signee key: %s", err)
}
derBytes, err := x509.CreateCertificate(
rand.Reader,
template,
parent,
signeePubKey,
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,
pemBlockForKey(signeeKey),
); err != nil {
return "", "", fmt.Errorf("error pem-encoding key: %s", err)
}
return certBuffer.String(), keyBuffer.String(), nil
}
func pemBlockForKey(priv interface{}) *pem.Block {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
case *dsa.PrivateKey:
val := DSAKeyFormat{
P: k.P, Q: k.Q, G: k.G,
Y: k.Y, X: k.X,
}
bytes, err := asn1.Marshal(val)
if err != nil {
return nil
}
return &pem.Block{Type: "DSA PRIVATE KEY", Bytes: bytes}
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return nil
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
default:
// attempt PKCS#8 format for all other keys
b, err := x509.MarshalPKCS8PrivateKey(k)
if err != nil {
return nil
}
return &pem.Block{Type: "PRIVATE KEY", Bytes: b}
}
}
func parsePrivateKeyPEM(pemBlock string) (crypto.PrivateKey, error) {
block, _ := pem.Decode([]byte(pemBlock))
if block == nil {
return nil, errors.New("no PEM data in input")
}
if block.Type == "PRIVATE KEY" {
priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("decoding PEM as PKCS#8: %s", err)
}
return priv, nil
} else if !strings.HasSuffix(block.Type, " PRIVATE KEY") {
return nil, fmt.Errorf("no private key data in PEM block of type %s", block.Type)
}
switch block.Type[:len(block.Type)-12] { // strip " PRIVATE KEY"
case "RSA":
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing RSA private key from PEM: %s", err)
}
return priv, nil
case "EC":
priv, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing EC private key from PEM: %s", err)
}
return priv, nil
case "DSA":
var k DSAKeyFormat
_, err := asn1.Unmarshal(block.Bytes, &k)
if err != nil {
return nil, fmt.Errorf("parsing DSA private key from PEM: %s", err)
}
priv := &dsa.PrivateKey{
PublicKey: dsa.PublicKey{
Parameters: dsa.Parameters{
P: k.P, Q: k.Q, G: k.G,
},
Y: k.Y,
},
X: k.X,
}
return priv, nil
default:
return nil, fmt.Errorf("invalid private key type %s", block.Type)
}
}
func getPublicKey(priv crypto.PrivateKey) (crypto.PublicKey, error) {
switch k := priv.(type) {
case interface{ Public() crypto.PublicKey }:
return k.Public(), nil
case *dsa.PrivateKey:
return &k.PublicKey, nil
default:
return nil, fmt.Errorf("unable to get public key for type %T", priv)
}
}
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
}

View File

@ -0,0 +1,168 @@
/*
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
https://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 extlib
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"testing"
"text/template"
"github.com/stretchr/testify/assert"
sprig "github.com/Masterminds/sprig/v3"
)
const (
beginCertificate = "-----BEGIN CERTIFICATE-----"
endCertificate = "-----END CERTIFICATE-----"
)
var (
// fastCertKeyAlgos is the list of private key algorithms that are supported for certificate use, and
// are fast to generate.
fastCertKeyAlgos = []string{
"ecdsa",
"ed25519",
}
)
// copy needed tests from https://github.com/Masterminds/sprig/blob/master/crypto_test.go
func testGenCAEx(t *testing.T, keyAlgo *string, subj, expCN string) {
var genCAExpr string
if keyAlgo == nil {
genCAExpr = "genCAEx"
} else {
genCAExpr = fmt.Sprintf(`genPrivateKey "%s" | genCAWithKeyEx`, *keyAlgo)
}
tpl := fmt.Sprintf(
`{{- $ca := %s "%s" 365 }}
{{ $ca.Cert }}
`,
genCAExpr,
subj,
)
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, expCN, cert.Subject.CommonName)
assert.True(t, cert.IsCA)
}
func TestGenCAEx(t *testing.T) {
testGenCAEx(t, nil, "foo ca", "foo ca")
testGenCAEx(t, nil, "/CN=bar ca", "bar ca")
for i, keyAlgo := range fastCertKeyAlgos {
t.Run(keyAlgo, func(t *testing.T) {
testGenCAEx(t, &fastCertKeyAlgos[i], "foo ca", "foo ca")
testGenCAEx(t, &fastCertKeyAlgos[i], "/CN=bar ca", "bar ca")
})
}
}
func testGenSignedCertEx(t *testing.T, caKeyAlgo, certKeyAlgo *string, subj, expCn string) {
const (
ip1 = "10.0.0.1"
ip2 = "10.0.0.2"
dns1 = "bar.com"
dns2 = "bat.com"
)
var genCAExpr, genSignedCertExpr string
if caKeyAlgo == nil {
genCAExpr = "genCAEx"
} else {
genCAExpr = fmt.Sprintf(`genPrivateKey "%s" | genCAWithKeyEx`, *caKeyAlgo)
}
if certKeyAlgo == nil {
genSignedCertExpr = "genSignedCertEx"
} else {
genSignedCertExpr = fmt.Sprintf(`genPrivateKey "%s" | genSignedCertWithKeyEx`, *certKeyAlgo)
}
tpl := fmt.Sprintf(
`{{- $ca := %s "foo" 3650 }}
{{- $cert := %s "%s" (list "%s" "%s") (list "%s" "%s") 365 $ca }}
{{ $cert.Cert }}`,
genCAExpr,
genSignedCertExpr,
subj,
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, expCn, cert.Subject.CommonName)
assert.Equal(t, 1, cert.SerialNumber.Sign())
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 TestGenSignedCertEx(t *testing.T) {
testGenSignedCertEx(t, nil, nil, "foo ca", "foo ca")
testGenSignedCertEx(t, nil, nil, "/CN=bar ca", "bar ca")
for i, caKeyAlgo := range fastCertKeyAlgos {
for j, certKeyAlgo := range fastCertKeyAlgos {
t.Run(fmt.Sprintf("%s-%s", caKeyAlgo, certKeyAlgo), func(t *testing.T) {
testGenSignedCertEx(t, &fastCertKeyAlgos[i], &fastCertKeyAlgos[j], "foo ca", "foo ca")
testGenSignedCertEx(t, &fastCertKeyAlgos[i], &fastCertKeyAlgos[j], "/CN=bar ca", "bar ca")
})
}
}
}
// runRaw runs a template with the given variables and returns the result.
func runRaw(tpl string, vars interface{}) (string, error) {
funcMap := sprig.TxtFuncMap()
for i, v := range GenericFuncMap() {
funcMap[i] = v
}
t := template.Must(template.New("test").Funcs(funcMap).Parse(tpl))
var b bytes.Buffer
err := t.Execute(&b, vars)
if err != nil {
return "", err
}
return b.String(), nil
}

View File

@ -0,0 +1,32 @@
/*
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
https://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 extlib
import (
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Render input yaml as output yaml
// This function is from the Helm project:
// https://github.com/helm/helm
// Copyright The Helm Authors
func toYaml(v interface{}) string {
data, err := yaml.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return string(data)
}

View File

@ -0,0 +1,44 @@
/*
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
https://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 extlib
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestToYaml(t *testing.T) {
data := []struct {
A string
B int
}{
{
A: "test1",
B: 1,
},
{
A: "test2",
B: 2,
},
}
assert.Equal(t, `
- a: test1
b: 1
- a: test2
b: 2
`[1:], toYaml(&data))
}

View File

@ -19,15 +19,15 @@ import (
"fmt"
"text/template"
sprig "github.com/Masterminds/sprig/v3"
"github.com/lucasjones/reggen"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
"opendev.org/airship/airshipctl/pkg/fs"
sprig "github.com/Masterminds/sprig/v3"
extlib "opendev.org/airship/airshipctl/pkg/document/plugin/templater/extlib"
)
var _ kio.Filter = &plugin{}
@ -48,14 +48,24 @@ func New(obj map[string]interface{}) (kio.Filter, error) {
}, nil
}
func funcMapAppend(fma, fmb template.FuncMap) template.FuncMap {
for k, v := range fmb {
_, ok := fma[k]
if ok {
panic(fmt.Errorf("Trying to redefine function %s that already exists", k))
}
fma[k] = v
}
return fma
}
func (t *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
docfs := fs.NewDocumentFs()
out := &bytes.Buffer{}
funcMap := sprig.TxtFuncMap()
funcMap["toUint32"] = func(i int) uint32 { return uint32(i) }
funcMap["toYaml"] = toYaml
funcMap["regexGen"] = regexGen
funcMap["fileExists"] = docfs.Exists
funcMap := template.FuncMap{}
funcMap = funcMapAppend(funcMap, sprig.TxtFuncMap())
funcMap = funcMapAppend(funcMap, extlib.GenericFuncMap())
tmpl, err := template.New("tmpl").Funcs(funcMap).Parse(t.Template)
if err != nil {
return nil, err
@ -79,28 +89,3 @@ func (t *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
}
return append(items, res.Nodes...), nil
}
// Generate Regex
func regexGen(regex string, limit int) string {
if limit <= 0 {
panic("Limit cannot be less than or equal to 0")
}
str, err := reggen.Generate(regex, limit)
if err != nil {
panic(err)
}
return str
}
// Render input yaml as output yaml
// This function is from the Helm project:
// https://github.com/helm/helm
// Copyright The Helm Authors
func toYaml(v interface{}) string {
data, err := yaml.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return string(data)
}

View File

@ -24,6 +24,11 @@ import (
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/yaml"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"opendev.org/airship/airshipctl/pkg/document/plugin/templater"
)
@ -213,3 +218,67 @@ NoFileExists: false
assert.Equal(t, tc.expectedOut, buf.String())
}
}
func TestGenSignedCertEx(t *testing.T) {
testCases := []struct {
cfg string
expectedSubject pkix.Name
}{
{
cfg: `
apiVersion: airshipit.org/v1alpha1
kind: Templater
metadata:
name: notImportantHere
values:
name: test
regex: "^[a-z]{5,10}$"
limit: 0
template: |
{{- $targetClusterCa:=genCAEx "Kubernetes API" 3650 }}
{{- $targetKubeconfigCert:= genSignedCertEx "/CN=admin/O=system:masters" nil nil 365 $targetClusterCa }}
cert: {{ $targetKubeconfigCert.Cert|b64enc|quote }}
`,
expectedSubject: pkix.Name{
CommonName: `admin`,
Organization: []string{
`system:masters`,
},
},
},
}
for _, tc := range testCases {
cfg := make(map[string]interface{})
err := yaml.Unmarshal([]byte(tc.cfg), &cfg)
require.NoError(t, err)
plugin, err := templater.New(cfg)
require.NoError(t, err)
buf := &bytes.Buffer{}
nodes, err := plugin.Filter(nil)
require.NoError(t, err)
err = kio.ByteWriter{Writer: buf}.Write(nodes)
require.NoError(t, err)
res := make(map[string]string)
err = yaml.Unmarshal(buf.Bytes(), &res)
require.NoError(t, err)
key, err := base64.StdEncoding.DecodeString(res["cert"])
require.NoError(t, err)
der, _ := pem.Decode(key)
if der == nil {
t.Errorf("failed to find PEM block")
return
}
cert, err := x509.ParseCertificate(der.Bytes)
if err != nil {
t.Errorf("failed to parse: %s", err)
return
}
cert.Subject.Names = nil
assert.Equal(t, tc.expectedSubject, cert.Subject)
}
}