Merge "Implement genCAEx and genSignedCertEx with Subj support"
This commit is contained in:
commit
e77bac1571
365
pkg/document/plugin/templater/extlib/crypto.go
Normal file
365
pkg/document/plugin/templater/extlib/crypto.go
Normal 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
|
||||||
|
}
|
170
pkg/document/plugin/templater/extlib/crypto_test.go
Normal file
170
pkg/document/plugin/templater/extlib/crypto_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
24
pkg/document/plugin/templater/extlib/fs.go
Normal file
24
pkg/document/plugin/templater/extlib/fs.go
Normal 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)
|
||||||
|
}
|
26
pkg/document/plugin/templater/extlib/fs_test.go
Normal file
26
pkg/document/plugin/templater/extlib/fs_test.go
Normal 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"))
|
||||||
|
}
|
39
pkg/document/plugin/templater/extlib/funcmap.go
Normal file
39
pkg/document/plugin/templater/extlib/funcmap.go
Normal 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,
|
||||||
|
}
|
31
pkg/document/plugin/templater/extlib/regexgen.go
Normal file
31
pkg/document/plugin/templater/extlib/regexgen.go
Normal 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
|
||||||
|
}
|
57
pkg/document/plugin/templater/extlib/regexgen_test.go
Normal file
57
pkg/document/plugin/templater/extlib/regexgen_test.go
Normal 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)
|
||||||
|
}
|
220
pkg/document/plugin/templater/extlib/sprig_crypto.go
Normal file
220
pkg/document/plugin/templater/extlib/sprig_crypto.go
Normal 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
|
||||||
|
}
|
168
pkg/document/plugin/templater/extlib/sprig_crypto_test.go
Normal file
168
pkg/document/plugin/templater/extlib/sprig_crypto_test.go
Normal 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
|
||||||
|
}
|
32
pkg/document/plugin/templater/extlib/yaml.go
Normal file
32
pkg/document/plugin/templater/extlib/yaml.go
Normal 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)
|
||||||
|
}
|
44
pkg/document/plugin/templater/extlib/yaml_test.go
Normal file
44
pkg/document/plugin/templater/extlib/yaml_test.go
Normal 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))
|
||||||
|
}
|
@ -19,15 +19,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
sprig "github.com/Masterminds/sprig/v3"
|
|
||||||
"github.com/lucasjones/reggen"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
|
|
||||||
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
|
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{}
|
var _ kio.Filter = &plugin{}
|
||||||
@ -48,14 +48,24 @@ func New(obj map[string]interface{}) (kio.Filter, error) {
|
|||||||
}, nil
|
}, 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) {
|
func (t *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||||
docfs := fs.NewDocumentFs()
|
|
||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
funcMap := sprig.TxtFuncMap()
|
|
||||||
funcMap["toUint32"] = func(i int) uint32 { return uint32(i) }
|
funcMap := template.FuncMap{}
|
||||||
funcMap["toYaml"] = toYaml
|
funcMap = funcMapAppend(funcMap, sprig.TxtFuncMap())
|
||||||
funcMap["regexGen"] = regexGen
|
funcMap = funcMapAppend(funcMap, extlib.GenericFuncMap())
|
||||||
funcMap["fileExists"] = docfs.Exists
|
|
||||||
tmpl, err := template.New("tmpl").Funcs(funcMap).Parse(t.Template)
|
tmpl, err := template.New("tmpl").Funcs(funcMap).Parse(t.Template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -79,28 +89,3 @@ func (t *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
|||||||
}
|
}
|
||||||
return append(items, res.Nodes...), nil
|
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)
|
|
||||||
}
|
|
||||||
|
@ -24,6 +24,11 @@ import (
|
|||||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
|
||||||
"opendev.org/airship/airshipctl/pkg/document/plugin/templater"
|
"opendev.org/airship/airshipctl/pkg/document/plugin/templater"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -213,3 +218,67 @@ NoFileExists: false
|
|||||||
assert.Equal(t, tc.expectedOut, buf.String())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user