Skip to content

Commit

Permalink
Improvements and fixes in the X509CA disk (#233)
Browse files Browse the repository at this point in the history
Signed-off-by: Max Lambrecht <[email protected]>
  • Loading branch information
Max Lambrecht authored Jun 21, 2023
1 parent 4139838 commit d55c1b6
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 113 deletions.
26 changes: 16 additions & 10 deletions conf/server/server_full.conf
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,26 @@ providers {

# X509CA "disk": Utilizes a Certificate Authority (CA) certificate, loaded from disk, to issue X509 certificates.
X509CA "disk" {
# key_file_path: The path to the PEM-encoded private key file of the CA.
# This path can be relative or absolute.
# key_file_path: Path to the CA key file. Key files must
# contain a single PEM encoded key. The supported key types are EC
# (ASN.1 or PKCS8 encoded) or RSA (PKCS1 or PKCS8 encoded).
key_file_path = "./conf/server/dummy_root_ca.key"

# cert_file_path: The path to the PEM-encoded certificate file of the CA.
# This path can be relative or absolute.
# cert_file_path: If Galadriel is using a self-signed CA, cert_file_path
# should specify the path to a single PEM encoded certificate
# representing the CA certificate. If not self-signed,
# cert_file_path should specify the path to a file that must contain
# one or more certificates necessary to establish a valid certificate
# chain up the root certificates defined in bundle_file_path.
cert_file_path = "./conf/server/dummy_root_ca.crt"

# bundle_file_path: Optional. Specifies the path to a file with one or more PEM-encoded certificates
# forming an upstream chain of trust. If Galadriel is using a self-signed CA, this can be omitted.
# However, if Galadriel uses an intermediate CA from a chain of trust that leads back to an external root CA
# to issue certificates, this parameter should point to a file containing one or more certificates that form
# this chain of trust. The path can be relative or absolute.
# Note: This bundle does not need to include the root CA itself.
# bundle_file_path: If Galadriel is using a self-signed CA, bundle_file_path
# can be left unset. If not self-signed, then bundle_file_path should
# be the path to a file that must contain one or more certificates
# representing the upstream root certificates and the file at
# cert_file_path contains one or more certificates necessary to chain up
# the root certificates in bundle_file_path (where the first
# certificate in cert_file_path is the CA certificate).
# bundle_file_path = ""
}

Expand Down
12 changes: 6 additions & 6 deletions doc/galadriel_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ providers {

The X509CA section provides configuration details for X.509 CA providers:

| Option | Description |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `disk` | Uses a CA (either ROOT or INTERMEDIATE) and private key loaded from disk to issue X.509 certificates. |
| `key_file_path` | Path to the CA private key file in PEM format. This path can be relative or absolute. |
| `cert_file_path` | Path to the CA certificate file in PEM format. This path can be relative or absolute. |
| `bundle_file_path` | Required when the cert_file_path does not contain a self-signed CA certificate. This is the path to the file containing one or more certificates forming the chain of trust to a root CA. This file should contain any number of intermediate CA certificates that chain back to a root CA. This path can be relative or absolute. |
| Option | Description |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `disk` | Uses a CA (either ROOT or INTERMEDIATE) and private key loaded from disk to issue X.509 certificates. |
| `key_file_path` | Path to the CA private key file in PEM format. This path can be relative or absolute. |
| `cert_file_path` | Path to the CA certificate file in PEM format. If Galadriel is using a self-signed CA, cert_file_path should specify the path to a single PEM encoded certificate representing the CA certificate. If not self-signed, cert_file_path should specify the path to a file that must contain one or more certificates necessary to establish a valid certificate chain up the root certificates defined in bundle_file_path. This path can be relative or absolute. |
| `bundle_file_path` | Required when the cert_file_path does not contain a self-signed CA certificate. This is the path to the file containing one or more root CAs. This path can be relative or absolute. |

#### Example:

Expand Down
30 changes: 30 additions & 0 deletions pkg/common/cryptoutil/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,36 @@ func CertificatesMatch(cert1, cert2 *x509.Certificate) bool {
return bytes.Equal(cert1.Raw, cert2.Raw)
}

// VerifyCertificateChain checks whether the provided certificate chain can be verified against the given intermediates and root CAs.
// The function will return an error if the verification fails.
func VerifyCertificateChain(certChain, intermediates, roots []*x509.Certificate, currentTime time.Time) error {
intermediatePool := x509.NewCertPool()
rootPool := x509.NewCertPool()

for _, cert := range certChain[1:] {
intermediatePool.AddCert(cert)
}
for _, intermediate := range intermediates {
intermediatePool.AddCert(intermediate)
}
for _, root := range roots {
rootPool.AddCert(root)
}

opts := x509.VerifyOptions{
Roots: rootPool,
Intermediates: intermediatePool,
CurrentTime: currentTime,
}

leaf := certChain[0]
if _, err := leaf.Verify(opts); err != nil {
return fmt.Errorf("unable to chain the certificate to a trusted CA: %w", err)
}

return nil
}

// LoadCertificate loads a x509.Certificate from the given path.
func LoadCertificate(path string) (*x509.Certificate, error) {
certFile, err := os.ReadFile(path)
Expand Down
84 changes: 69 additions & 15 deletions pkg/common/cryptoutil/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/stretchr/testify/require"
)

var clk = clock.NewFake()

func TestIsSelfSigned(t *testing.T) {
cert, _ := createRootCA(t)
assert.True(t, IsSelfSigned(cert))
Expand All @@ -30,6 +32,38 @@ func TestCertificatesMatch(t *testing.T) {
assert.False(t, CertificatesMatch(cert, cert2))
}

func TestVerifyCertificateChain(t *testing.T) {
now := clk.Now()

// Generate leaf, intermediate, and root certificates
leaf, intermediate, root := createCertChain(t, DefaultKeyType)
otherRoot, _ := createRootCA(t)

// Test valid certificate chain
certChain := []*x509.Certificate{leaf, intermediate}
intermediates := []*x509.Certificate{intermediate}
roots := []*x509.Certificate{root, otherRoot}

err := VerifyCertificateChain(certChain, intermediates, roots, now)
require.NoError(t, err)

// Test certificate chain with missing root
missingRootChain := []*x509.Certificate{leaf}
missingRootIntermediates := []*x509.Certificate{intermediate}
missingRoots := []*x509.Certificate{}

err = VerifyCertificateChain(missingRootChain, missingRootIntermediates, missingRoots, now)
require.Error(t, err)

// Test certificate chain with wrong root
wrongRootChain := []*x509.Certificate{leaf, intermediate}
wrongRootIntermediates := []*x509.Certificate{intermediate}
wrongRootRoots := []*x509.Certificate{otherRoot}

err = VerifyCertificateChain(wrongRootChain, wrongRootIntermediates, wrongRootRoots, now)
require.Error(t, err)
}

func TestLoadCertificate(t *testing.T) {
// not a certificate
_, err := LoadCertificate(rsaKeyPath)
Expand Down Expand Up @@ -75,7 +109,6 @@ func TestEncodeCertificates(t *testing.T) {
}

func TestCreateX509Template(t *testing.T) {
clk := clock.NewFake()
key, err := GenerateSigner(ECP384)
require.NoError(t, err)
uris := []*url.URL{{Scheme: "https", Host: "domain.test"}}
Expand All @@ -98,7 +131,6 @@ func TestCreateX509Template(t *testing.T) {
}

func TestCreateCATemplate(t *testing.T) {
clk := clock.NewFake()
key, err := GenerateSigner(ECP384)
require.NoError(t, err)
name := pkix.Name{CommonName: "test-cn"}
Expand All @@ -116,7 +148,6 @@ func TestCreateCATemplate(t *testing.T) {
}

func TestCreateRootCATemplate(t *testing.T) {
clk := clock.NewFake()
name := pkix.Name{CommonName: "test-cn"}
twoHours := 2 * time.Hour

Expand All @@ -131,7 +162,6 @@ func TestCreateRootCATemplate(t *testing.T) {
}

func TestSignX509(t *testing.T) {
clk := clock.NewFake()
key, err := GenerateSigner(ECP384)
require.NoError(t, err)
uris := []*url.URL{{Scheme: "https", Host: "domain.test"}}
Expand All @@ -155,7 +185,6 @@ func TestSignX509(t *testing.T) {
}

func TestSelfSignX509(t *testing.T) {
clk := clock.NewFake()
name := pkix.Name{CommonName: "root"}
tmpl, err := CreateRootCATemplate(clk, name, 5*time.Minute)
require.NoError(t, err)
Expand All @@ -172,7 +201,6 @@ func TestSelfSignX509(t *testing.T) {
}

func createRootCA(t *testing.T) (*x509.Certificate, crypto.PrivateKey) {
clk := clock.NewFake()
name := pkix.Name{CommonName: "root-ca"}
tmpl, err := CreateRootCATemplate(clk, name, 5*time.Minute)
require.NoError(t, err)
Expand All @@ -186,25 +214,51 @@ func createRootCA(t *testing.T) (*x509.Certificate, crypto.PrivateKey) {
}

func createCert(t *testing.T, keyType KeyType) (*x509.Certificate, crypto.PrivateKey) {
clk := clock.NewFake()
key, err := GenerateSigner(keyType)
require.NoError(t, err)
name := pkix.Name{CommonName: "test-cn"}

tmpl, err := CreateX509Template(clk, key.Public(), name, nil, nil, 1*time.Hour)
require.NoError(t, err)
require.NotNil(t, tmpl)

// create parent certificate for signing
parentCert, signingKey := createRootCA(t)

tmpl, key := createCertTemplate(t, keyType, pkix.Name{CommonName: "leaf-cert"}, false)
cert, err := SignX509(tmpl, parentCert, signingKey)
require.NoError(t, err)
require.NotNil(t, cert)

return cert, key
}

func createCertChain(t *testing.T, keyType KeyType) (leaf *x509.Certificate, intermediate *x509.Certificate, root *x509.Certificate) {
root, rootKey := createRootCA(t)

intermediateTemplate, intermediateKey := createCertTemplate(t, keyType, pkix.Name{CommonName: "intermediate-cert"}, true)
intermediate, err := SignX509(intermediateTemplate, root, rootKey)
require.NoError(t, err)
require.NotNil(t, intermediate)

certTemplate, _ := createCertTemplate(t, keyType, pkix.Name{CommonName: "leaf-cert"}, false)
leaf, _ = SignX509(certTemplate, intermediate, intermediateKey)
require.NoError(t, err)
require.NotNil(t, leaf)

return
}

func createCertTemplate(t *testing.T, keyType KeyType, name pkix.Name, isCa bool) (*x509.Certificate, crypto.PrivateKey) {
key, err := GenerateSigner(keyType)
require.NoError(t, err)

var tmpl *x509.Certificate
if isCa {
tmpl, err = CreateCATemplate(clk, key.Public(), name, 1*time.Hour)
require.NoError(t, err)
require.NotNil(t, tmpl)
} else {
tmpl, err = CreateX509Template(clk, key.Public(), name, nil, nil, 1*time.Hour)
require.NoError(t, err)
require.NotNil(t, tmpl)
}

return tmpl, key
}

func readFile(t *testing.T, path string) []byte {
data, err := os.ReadFile(path)
require.NoError(t, err)
Expand Down
Loading

0 comments on commit d55c1b6

Please sign in to comment.