cacert-gosigner/pkg/config/config_test.go

935 lines
18 KiB
Go

package config_test
import (
"crypto/elliptic"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"git.cacert.org/cacert-gosigner/pkg/config"
)
type TestCurve struct {
}
func (t TestCurve) Params() *elliptic.CurveParams {
panic("not needed")
}
func (t TestCurve) IsOnCurve(_, _ *big.Int) bool {
panic("not needed")
}
func (t TestCurve) Add(_, _, _, _ *big.Int) (x, y *big.Int) {
panic("not needed")
}
func (t TestCurve) Double(_, _ *big.Int) (x, y *big.Int) {
panic("not needed")
}
func (t TestCurve) ScalarMult(_, _ *big.Int, _ []byte) (x, y *big.Int) {
panic("not needed")
}
func (t TestCurve) ScalarBaseMult(_ []byte) (x, y *big.Int) {
panic("not needed")
}
func TestPrivateKeyInfo_MarshalYAML(t *testing.T) {
testCurve := TestCurve{}
testData := []struct {
name string
pkInfo *config.PrivateKeyInfo
expected string
expectErr bool
}{
{
name: "RSA",
pkInfo: &config.PrivateKeyInfo{
Algorithm: x509.RSA,
RSABits: 3072,
},
expected: `algorithm: RSA
rsa-bits: 3072
`,
},
{
name: "ECDSA P-224",
pkInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P224(),
},
expected: `algorithm: EC
ecc-curve: P-224
`,
},
{
name: "ECDSA P-256",
pkInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P256(),
},
expected: `algorithm: EC
ecc-curve: P-256
`,
},
{
name: "ECDSA P-384",
pkInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P384(),
},
expected: `algorithm: EC
ecc-curve: P-384
`,
},
{
name: "ECDSA P-521",
pkInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P521(),
},
expected: `algorithm: EC
ecc-curve: P-521
`,
},
{
name: "ECDSA unsupported curve",
pkInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: testCurve,
},
expectErr: true,
},
}
for _, item := range testData {
t.Run(item.name, func(t *testing.T) {
data, err := yaml.Marshal(item.pkInfo)
if item.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.YAMLEq(t, item.expected, string(data))
})
}
}
func TestPrivateKeyInfo_UnmarshalYAML(t *testing.T) {
testData := []struct {
name string
yaml string
expected *config.PrivateKeyInfo
expectErr bool
}{
{
name: "malformed",
yaml: `noyaml`,
expectErr: true,
},
{
name: "RSA",
yaml: `label: "mykey"
algorithm: "RSA"
rsa-bits: 2048`,
expected: &config.PrivateKeyInfo{
Algorithm: x509.RSA,
RSABits: 2048,
},
},
{
name: "ECDSA P-224",
yaml: `label: "mykey"
algorithm: "EC"
ecc-curve: "P-224"`,
expected: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P224(),
},
},
{
name: "ECDSA P-256",
yaml: `label: "mykey"
algorithm: "EC"
ecc-curve: "P-256"`,
expected: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P256(),
},
},
{
name: "ECDSA P-384",
yaml: `label: "mykey"
algorithm: "EC"
ecc-curve: "P-384"`,
expected: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P384(),
},
},
{
name: "ECDSA P-521",
yaml: `label: "mykey"
algorithm: "EC"
ecc-curve: "P-521"`,
expected: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P521(),
},
},
{
name: "no-algorithm",
yaml: `label: "mykey"`,
expectErr: true,
},
{
name: "RSA-no-rsa-bits",
yaml: `label: "mykey"
algorithm: "RSA"`,
expectErr: true,
},
{
name: "ECDSA-no-curve",
yaml: `label: "mykey"
algorithm: "EC"`,
expectErr: true,
},
{
name: "ECDSA-invalid-curve",
yaml: `label: "mykey"
algorithm: "EC"
ecc-curve: "fancy-but-wrong"`,
expectErr: true,
},
{
name: "wrong-key-algorithm",
yaml: `label: "mykey"
algorithm: "ChaCha"`,
expectErr: true,
},
}
for _, item := range testData {
t.Run(item.name, func(t *testing.T) {
pkInfo := &config.PrivateKeyInfo{}
err := yaml.Unmarshal([]byte(item.yaml), pkInfo)
if err != nil && !item.expectErr {
require.NoError(t, err)
}
if item.expectErr {
assert.Error(t, err)
}
if !item.expectErr {
assert.Equal(t, item.expected, pkInfo)
}
})
}
}
func TestCaCertificateEntry_UnmarshalYAML(t *testing.T) {
testData := []struct {
name, yaml string
expected config.CaCertificateEntry
expectErr bool
}{
{
name: "no yaml",
yaml: "no yaml",
expectErr: true,
},
{
name: "no common name",
yaml: `
key-info:
algorithm: EC
ecc-curve: P-521
`,
expectErr: true,
},
{
name: "no key info",
yaml: `
common-name: My Little Test Root CA
`,
expectErr: true,
},
{
name: "EC P-521",
yaml: `
key-info:
algorithm: EC
ecc-curve: P-521
common-name: My Little Test Root CA
ext-key-usages:
- client
- server
`,
expectErr: true,
},
{
name: "EC root P-521",
yaml: `
key-info:
algorithm: EC
ecc-curve: P-521
common-name: My Little Test Root CA
storage: root
`,
expected: config.CaCertificateEntry{
KeyInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P521(),
},
CommonName: "My Little Test Root CA",
Storage: "root",
},
},
{
name: "EC sub P-256 client server",
yaml: `
key-info:
algorithm: EC
ecc-curve: P-256
common-name: My Little Test Sub CA
parent: root
ext-key-usages:
- client
- server
`,
expected: config.CaCertificateEntry{
KeyInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P256(),
},
Parent: "root",
CommonName: "My Little Test Sub CA",
Storage: "default",
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
},
},
{
name: "EC sub P-256 person",
yaml: `
key-info:
algorithm: EC
ecc-curve: P-256
common-name: My Little Test Sub CA
parent: root
ext-key-usages:
- client
- code
- email
- ocsp
`,
expected: config.CaCertificateEntry{
KeyInfo: &config.PrivateKeyInfo{
Algorithm: x509.ECDSA,
EccCurve: elliptic.P256(),
},
CommonName: "My Little Test Sub CA",
Storage: "default",
Parent: "root",
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageCodeSigning,
x509.ExtKeyUsageEmailProtection,
x509.ExtKeyUsageOCSPSigning,
},
},
},
{
name: "invalid ext-key-usages",
yaml: `
key-info:
algorithm: EC
ecc-curve: P-256
common-name: My Little Test Sub CA
parent: root
ext-key-usages:
- wrong_one
`,
expectErr: true,
},
}
for _, item := range testData {
t.Run(item.name, func(t *testing.T) {
entry := config.CaCertificateEntry{}
err := yaml.Unmarshal([]byte(item.yaml), &entry)
if item.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, item.expected, entry)
})
}
}
func TestLoadConfiguration(t *testing.T) {
testData := map[string]struct {
yaml string
errMsg string
}{
"Good": {
yaml: `---
Settings:
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 20
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s
issuer: http://%s.cas.example.org/
KeyStorage:
default:
type: softhsm
label: ca-storage
CAs:
root:
common-name: "Root CA"
key-info:
algorithm: EC
ecc-curve: P-384
sub1:
common-name: "Sub CA 1"
key-info:
algorithm: EC
ecc-curve: P-256
parent: root
sub2:
common-name: "Sub CA 2"
key-info:
algorithm: EC
ecc-curve: P-256
parent: root
`,
},
"Bad": {
yaml: `no yaml for you: ]`,
errMsg: "could not parse YAML configuration",
},
"NoGlobal": {
yaml: `---
KeyStorage:
default:
label: default
type: softhsm
CAs:
root:
common-name: "Root CA"
key-info:
algorithm: EC
ecc-curve: P-384
`,
errMsg: "configuration entry 'Settings' is missing or empty",
},
"NoKeyStorage": {
yaml: `
Settings:
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 20
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://%s.cas.example.org/
CAs:
root:
common-name: "Root CA"
key-info:
algorithm: EC
ecc-curve: P-384
`,
errMsg: "configuration entry 'KeyStorage' is missing or empty",
},
"NoCAs": {
yaml: `
Settings:
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 20
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://%s.cas.example.org/
KeyStorage:
default:
label: default
type: softhsm
`,
errMsg: "configuration entry 'CAs' is missing or empty",
},
}
for name, item := range testData {
t.Run(name, func(t *testing.T) {
r := strings.NewReader(item.yaml)
sc, err := config.LoadConfiguration(r)
if item.errMsg != "" {
assert.ErrorContains(t, err, item.errMsg)
assert.Nil(t, sc)
} else {
assert.NoError(t, err)
assert.NotNil(t, sc)
}
})
}
}
const sampleConfig = `---
Settings:
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 20
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://%s.cas.example.org/
KeyStorage:
default:
type: softhsm
label: ca-storage
CAs:
root:
common-name: "Root CA"
key-info:
algorithm: EC
ecc-curve: P-384
sub1:
common-name: "Sub CA 1"
key-info:
algorithm: EC
ecc-curve: P-256
parent: root
sub2:
common-name: "Sub CA 2"
key-info:
algorithm: EC
ecc-curve: P-256
parent: root
`
func loadSignerConfig(t *testing.T) *config.SignerConfig {
r := strings.NewReader(sampleConfig)
sc, err := config.LoadConfiguration(r)
require.NoError(t, err)
require.NotNil(t, sc)
return sc
}
func TestSignerConfig_RootCAs(t *testing.T) {
sc := loadSignerConfig(t)
roots := sc.RootCAs()
assert.Equal(t, roots, []string{"root"})
}
func TestSignerConfig_IntermediaryCAs(t *testing.T) {
sc := loadSignerConfig(t)
cAs := sc.IntermediaryCAs()
assert.ElementsMatch(t, cAs, []string{"sub1", "sub2"})
}
func TestSignerConfig_GetParentCA(t *testing.T) {
testData := map[string]struct{ errMsg string }{
"root": {
errMsg: "CA root is a root CA and has no parent",
},
"unknown": {
errMsg: "no CA definition for unknown",
},
"sub1": {},
}
sc := loadSignerConfig(t)
for name, item := range testData {
t.Run(name, func(t *testing.T) {
ca, err := sc.GetParentCA(name)
if item.errMsg != "" {
assert.ErrorContains(t, err, item.errMsg)
} else {
require.NoError(t, err)
assert.Equal(t, "Root CA", ca.CommonName)
}
})
}
}
func TestSignerConfig_CertificateFileName(t *testing.T) {
sc := loadSignerConfig(t)
assert.Equal(t, "root.crt", sc.CertificateFileName("root"))
}
func TestSignerConfig_BuildCRLUrl(t *testing.T) {
sc := loadSignerConfig(t)
ca, err := sc.GetCADefinition("sub1")
require.NoError(t, err)
url := sc.BuildCRLUrl(ca)
assert.Equal(t, "http://crl.example.org/root.crl", url)
}
func TestSignerConfig_BuildIssuerUrl(t *testing.T) {
sc := loadSignerConfig(t)
ca, err := sc.GetCADefinition("sub1")
require.NoError(t, err)
url := sc.BuildIssuerURL(ca)
assert.Equal(t, "http://root.cas.example.org/", url)
}
func TestSignerConfig_BuildOCSPURL(t *testing.T) {
sc := loadSignerConfig(t)
ca, err := sc.GetCADefinition("sub1")
require.NoError(t, err)
url := sc.BuildOCSPURL(ca)
assert.Equal(t, "http://ocsp.example.org/", url)
}
func TestSignerConfig_BuildOCSPURL2(t *testing.T) {
r := strings.NewReader(`---
Settings:
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 20
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/%s
crl: http://crl.example.org/%s.crl
issuer: http://%s.cas.example.org/
KeyStorage:
default:
type: softhsm
label: ca-storage
CAs:
root:
common-name: "Root CA"
key-info:
algorithm: EC
ecc-curve: P-384
sub1:
common-name: "Sub CA 1"
key-info:
algorithm: EC
ecc-curve: P-256
parent: root
sub2:
common-name: "Sub CA 2"
key-info:
algorithm: EC
ecc-curve: P-256
parent: root
`)
sc, err := config.LoadConfiguration(r)
require.NoError(t, err)
require.NotNil(t, sc)
ca, err := sc.GetCADefinition("sub1")
require.NoError(t, err)
url := sc.BuildOCSPURL(ca)
assert.Equal(t, "http://ocsp.example.org/root", url)
}
func TestSettings_UnmarshalYAML(t *testing.T) {
testData := map[string]struct {
yaml string
expected config.Settings
errMsg string
}{
"no yaml": {
yaml: "no yaml",
errMsg: "could not decode YAML",
},
"missing organization": {
yaml: `
validity-years:
root: 10
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://issuer.example.org/%s.crt
`,
errMsg: "invalid Settings you need to specify 'organization'",
},
"missing validity-years": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://issuer.example.org/%s.crt
`,
errMsg: "invalid Settings you must specify validity years for 'root' and 'intermediary'",
},
"missing url-patterns": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 10
intermediary: 5
`,
errMsg: "invalid Settings",
},
"invalid validity-years": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 5
intermediary: 10
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://issuer.example.org/%s.crt
`,
errMsg: "invalid Settings validity of root CA certificates must be equal or greater than those if intermediary CA certificates",
},
"no OCSP pattern": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 10
intermediary: 5
url-patterns:
crl: http://crl.example.org/%s.crl
issuer: http://issuer.example.org/%s.crt
`,
errMsg: "invalid Settings you must specify an 'ocsp' URL pattern",
},
"invalid OCSP pattern": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 10
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/%s_%s
crl: http://crl.example.org/%s.crl
issuer: http://issuer.example.org/%s.crt
`,
errMsg: "invalid Settings url-pattern 'ocsp' must contain zero or one '%s' placeholder",
},
"no CRL pattern": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 10
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
issuer: http://issuer.example.org/%s.crt
`,
errMsg: "invalid Settings you must specify an 'crl' URL pattern",
},
"invalid CRL pattern": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 10
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/
issuer: http://issuer.example.org/%s.crt
`,
errMsg: "invalid Settings url-pattern 'crl' must contain one '%s' placeholder",
},
"no issuer pattern": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 10
intermediary: 5
url-patterns:
crl: http://crl.example.org/%s.crl
ocsp: http://ocsp.example.org/
`,
errMsg: "invalid Settings you must specify an 'issuer' URL pattern",
},
"invalid issuer pattern": {
yaml: `
organization:
organization: ["Acme CAs Ltd."]
validity-years:
root: 10
intermediary: 5
url-patterns:
ocsp: http://ocsp.example.org/
crl: http://crl.example.org/%s.crl
issuer: http://issuer.example.org/
`,
errMsg: "invalid Settings url-pattern 'issuer' must contain one '%s' placeholder",
},
}
for name, item := range testData {
t.Run(name, func(t *testing.T) {
var s config.Settings
err := yaml.Unmarshal([]byte(item.yaml), &s)
if item.errMsg != "" {
assert.ErrorContains(t, err, item.errMsg)
} else {
require.NoError(t, err)
assert.NotNil(t, s)
assert.Equal(t, item.expected, s)
}
})
}
}
func TestKeyStorage_UnmarshalYAML(t *testing.T) {
testData := map[string]struct {
yaml string
expected config.KeyStorage
errMsg string
}{
"no yaml": {
yaml: `no yaml`,
errMsg: "could not decode YAML:",
},
"softhsm": {
yaml: `
label: softhsm
type: softhsm`,
expected: config.KeyStorage{
Label: "softhsm", Module: config.SoftHsmModule,
},
},
"custom": {
yaml: `
label: custom
type: p11module
module: /usr/lib/mytokenp11.so`,
expected: config.KeyStorage{
Label: "custom", Module: "/usr/lib/mytokenp11.so",
},
},
"missing module": {
yaml: `
label: custom
type: p11module`,
errMsg: "specify a 'module' field when using the 'p11module' type",
},
"unsupported type": {
yaml: `
label: something
type: something`,
errMsg: "unsupported KeyStorage type 'something'",
},
"missing label": {
yaml: `
type: softhsm`,
errMsg: "element 'label' must be specified",
},
}
for name, item := range testData {
t.Run(name, func(t *testing.T) {
var ks config.KeyStorage
err := yaml.Unmarshal([]byte(item.yaml), &ks)
if item.errMsg != "" {
assert.ErrorContains(t, err, item.errMsg)
} else {
assert.NoError(t, err)
assert.Equal(t, item.expected, ks)
}
})
}
}
func TestSignerConfig_GetCADefinition(t *testing.T) {
sc := loadSignerConfig(t)
def, err := sc.GetCADefinition("unknown")
assert.ErrorContains(t, err, "no CA definition found for label unknown")
assert.Nil(t, def)
}
func TestSignerConfig_CalculateValidity(t *testing.T) {
sc := loadSignerConfig(t)
for _, ca := range []string{"root", "sub1"} {
t.Run(ca, func(t *testing.T) {
def, err := sc.GetCADefinition(ca)
require.NoError(t, err)
now := time.Now()
notBefore, notAfter := sc.CalculateValidity(def, now)
assert.True(t, now.Equal(notBefore))
assert.True(t, now.Before(notAfter))
})
}
}
func TestSignerConfig_CalculateSubject(t *testing.T) {
testData := map[string]struct {
cn string
}{
"root": {cn: "Root CA"},
"sub1": {cn: "Sub CA 1"},
}
sc := loadSignerConfig(t)
for name, item := range testData {
t.Run(name, func(t *testing.T) {
def, err := sc.GetCADefinition(name)
require.NoError(t, err)
subject := sc.CalculateSubject(def)
assert.Equal(t, pkix.Name{Organization: []string{"Acme CAs Ltd."}, CommonName: item.cn}, subject)
})
}
}