Skip to content

Commit

Permalink
Allow parallel generation of certificates and CRLs
Browse files Browse the repository at this point in the history
  • Loading branch information
tsaarni committed Jan 23, 2024
1 parent c2e9407 commit 3f46f02
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
all: check build

test:
go test -v ./...
go test --race -v ./...

check: test
golangci-lint run
Expand Down
8 changes: 8 additions & 0 deletions certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"net/url"
"os"
"strings"
"sync"
"time"

"github.com/tsaarni/x500dn"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"net/url"
"os"
"path"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -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()
}
7 changes: 7 additions & 0 deletions crl.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"math/big"
"os"
"sync"
"time"
)

Expand All @@ -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.
Expand All @@ -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")
Expand Down
20 changes: 20 additions & 0 deletions crl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package certyaml
import (
"crypto/x509"
"math/big"
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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()
}

0 comments on commit 3f46f02

Please sign in to comment.