Skip to content

Commit 66a265c

Browse files
authored
Allow parallel generation of certificates and CRLs (#48)
1 parent c2e9407 commit 66a265c

File tree

5 files changed

+54
-1
lines changed

5 files changed

+54
-1
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
all: check build
22

33
test:
4-
go test -v ./...
4+
go test --race -v ./...
55

66
check: test
77
golangci-lint run

certificate.go

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"net/url"
3131
"os"
3232
"strings"
33+
"sync"
3334
"time"
3435

3536
"github.com/tsaarni/x500dn"
@@ -94,6 +95,10 @@ type Certificate struct {
9495
// GeneratedCert is a pointer to the generated certificate and private key.
9596
// It is automatically set after calling any of the Certificate functions.
9697
GeneratedCert *tls.Certificate `json:"-" hash:"-"`
98+
99+
// lazyInitialize ensures that only single goroutine can run lazy initialization of certificate concurrently.
100+
// Concurrent regeneration of certificate and private key by explicit call to Generate() is not supported.
101+
lazyInitialize sync.Mutex
97102
}
98103

99104
type KeyType uint
@@ -245,6 +250,9 @@ func (c *Certificate) defaults() error {
245250
}
246251

247252
func (c *Certificate) ensureGenerated() error {
253+
c.lazyInitialize.Lock()
254+
defer c.lazyInitialize.Unlock()
255+
248256
if c.GeneratedCert == nil {
249257
err := c.Generate()
250258
if err != nil {

certificate_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"net/url"
2727
"os"
2828
"path"
29+
"sync"
2930
"testing"
3031
"time"
3132

@@ -401,3 +402,20 @@ func TestCertificateChainInPEM(t *testing.T) {
401402

402403
assert.Empty(t, rest)
403404
}
405+
406+
func TestParallelCertificateLazyInitialization(t *testing.T) {
407+
cert := Certificate{Subject: "CN=Joe"}
408+
409+
// Trigger lazy initialization by calling one of the generator methods in parallel.
410+
var wg sync.WaitGroup
411+
for i := 0; i < 10; i++ {
412+
wg.Add(1)
413+
go func(cert *Certificate) {
414+
defer wg.Done()
415+
_, err := cert.X509Certificate()
416+
assert.Nil(t, err)
417+
}(&cert)
418+
}
419+
420+
wg.Wait()
421+
}

crl.go

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"math/big"
2525
"os"
26+
"sync"
2627
"time"
2728
)
2829

@@ -44,6 +45,9 @@ type CRL struct {
4445
// Issuer is the CA certificate issuing this CRL.
4546
// If not set, it defaults to the issuer of certificates added to Revoked list.
4647
Issuer *Certificate
48+
49+
// mutex ensures that only single goroutine can generate CRL concurrently.
50+
mutex sync.Mutex
4751
}
4852

4953
// Add appends a Certificate to CRL list.
@@ -64,6 +68,9 @@ func (crl *CRL) Add(cert *Certificate) error {
6468
// DER returns the CRL as DER buffer.
6569
// Error is not nil if generation fails.
6670
func (crl *CRL) DER() (crlBytes []byte, err error) {
71+
crl.mutex.Lock()
72+
defer crl.mutex.Unlock()
73+
6774
if crl.Issuer == nil {
6875
if len(crl.Revoked) == 0 {
6976
return nil, fmt.Errorf("issuer not known: either set Issuer or add certificates to the CRL")

crl_test.go

+20
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package certyaml
1717
import (
1818
"crypto/x509"
1919
"math/big"
20+
"sync"
2021
"testing"
2122

2223
"github.com/stretchr/testify/assert"
@@ -98,3 +99,22 @@ func TestEmptyCRL(t *testing.T) {
9899
_, err = crl.DER()
99100
assert.NotNil(t, err)
100101
}
102+
103+
func TestParallelCRLLazyInitialization(t *testing.T) {
104+
ca := Certificate{Subject: "CN=ca"}
105+
revoked := Certificate{Subject: "CN=Joe", Issuer: &ca}
106+
crl := CRL{Revoked: []*Certificate{&revoked}}
107+
108+
// Call CRL generation in parallel.
109+
var wg sync.WaitGroup
110+
for i := 0; i < 10; i++ {
111+
wg.Add(1)
112+
go func(cert *Certificate) {
113+
defer wg.Done()
114+
_, err := crl.DER()
115+
assert.Nil(t, err)
116+
}(&ca)
117+
}
118+
119+
wg.Wait()
120+
}

0 commit comments

Comments
 (0)