diff --git a/Makefile b/Makefile index f239106..c3a8248 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ all: check build test: - go test -v ./... + go test --race -v ./... check: test golangci-lint run diff --git a/certificate.go b/certificate.go index 4dd90ed..7c2e6f4 100644 --- a/certificate.go +++ b/certificate.go @@ -30,6 +30,7 @@ import ( "net/url" "os" "strings" + "sync" "time" "github.com/tsaarni/x500dn" @@ -94,6 +95,10 @@ type Certificate struct { // GeneratedCert is a pointer to the generated certificate and private key. // It is automatically set after calling any of the Certificate functions. GeneratedCert *tls.Certificate `json:"-" hash:"-"` + + // lazyInitialize ensures that only single goroutine can run lazy initialization of certificate concurrently. + // Concurrent regeneration of certificate and private key by explicit call to Generate() is not supported. + lazyInitialize sync.Mutex } type KeyType uint @@ -245,6 +250,9 @@ func (c *Certificate) defaults() error { } func (c *Certificate) ensureGenerated() error { + c.lazyInitialize.Lock() + defer c.lazyInitialize.Unlock() + if c.GeneratedCert == nil { err := c.Generate() if err != nil { diff --git a/certificate_test.go b/certificate_test.go index 6bfec01..3d5eac8 100644 --- a/certificate_test.go +++ b/certificate_test.go @@ -26,6 +26,7 @@ import ( "net/url" "os" "path" + "sync" "testing" "time" @@ -401,3 +402,20 @@ func TestCertificateChainInPEM(t *testing.T) { assert.Empty(t, rest) } + +func TestParallelCertificateLazyInitialization(t *testing.T) { + cert := Certificate{Subject: "CN=Joe"} + + // Trigger lazy initialization by calling one of the generator methods in parallel. + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(cert *Certificate) { + defer wg.Done() + _, err := cert.X509Certificate() + assert.Nil(t, err) + }(&cert) + } + + wg.Wait() +} diff --git a/crl.go b/crl.go index 2e38fa0..669b7de 100644 --- a/crl.go +++ b/crl.go @@ -23,6 +23,7 @@ import ( "fmt" "math/big" "os" + "sync" "time" ) @@ -44,6 +45,9 @@ type CRL struct { // Issuer is the CA certificate issuing this CRL. // If not set, it defaults to the issuer of certificates added to Revoked list. Issuer *Certificate + + // mutex ensures that only single goroutine can generate CRL concurrently. + mutex sync.Mutex } // Add appends a Certificate to CRL list. @@ -64,6 +68,9 @@ func (crl *CRL) Add(cert *Certificate) error { // DER returns the CRL as DER buffer. // Error is not nil if generation fails. func (crl *CRL) DER() (crlBytes []byte, err error) { + crl.mutex.Lock() + defer crl.mutex.Unlock() + if crl.Issuer == nil { if len(crl.Revoked) == 0 { return nil, fmt.Errorf("issuer not known: either set Issuer or add certificates to the CRL") diff --git a/crl_test.go b/crl_test.go index 230904c..05976ec 100644 --- a/crl_test.go +++ b/crl_test.go @@ -17,6 +17,7 @@ package certyaml import ( "crypto/x509" "math/big" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -98,3 +99,22 @@ func TestEmptyCRL(t *testing.T) { _, err = crl.DER() assert.NotNil(t, err) } + +func TestParallelCRLLazyInitialization(t *testing.T) { + ca := Certificate{Subject: "CN=ca"} + revoked := Certificate{Subject: "CN=Joe", Issuer: &ca} + crl := CRL{Revoked: []*Certificate{&revoked}} + + // Call CRL generation in parallel. + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func(cert *Certificate) { + defer wg.Done() + _, err := crl.DER() + assert.Nil(t, err) + }(&ca) + } + + wg.Wait() +}