cacert-gosigner/pkg/config/config_test.go

976 lines
20 KiB
Go

/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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 of 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)
})
}
}
func TestSignerConfig_GetKeyStorage(t *testing.T) {
var (
keyStorage *config.KeyStorage
err error
)
sc := loadSignerConfig(t)
keyStorage, err = sc.GetKeyStorage("default")
assert.NoError(t, err)
assert.NotNil(t, keyStorage)
keyStorage, err = sc.GetKeyStorage("undefined")
assert.Error(t, err)
assert.ErrorContains(t, err, "could not find storage definition with label")
assert.Nil(t, keyStorage)
}