Skip to content

Commit

Permalink
feat: Capture gauntlet workers in WaitGroup
Browse files Browse the repository at this point in the history
Run all user-supplied gauntlet funcs inside a waitgroup.
When users are done with a CA instance, they can call
[CA.Close] to wait for all of the wg goroutines to exit.
  • Loading branch information
ananthb committed Jun 10, 2024
1 parent 81e403c commit 51529eb
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 74 deletions.
2 changes: 2 additions & 0 deletions cmd/bf/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ var caServeCmd = &cli.Command{
if err != nil {
return cli.Exit(fmt.Sprintf("Error creating CA: %s", err), 1)
}
defer ca.Close()

mux.Handle("POST /issue", ca)

Expand Down Expand Up @@ -152,6 +153,7 @@ var caIssueCmd = &cli.Command{
if err != nil {
return cli.Exit(fmt.Sprintf("Error creating CA: %s", err), 1)
}
defer ca.Close()

clientKey, err := cafiles.GetPrivateKey(ctx, clientPrivKeyUri)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/bf/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func issueTLSCert(
if err != nil {
return nil, err
}
defer ca.Close()

caNs := caCert.Namespace
csr := x509.CertificateRequest{
Expand Down
13 changes: 13 additions & 0 deletions tinyca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ import (

// CA is a simple Certificate Authority.
// The CA issues client certificates signed by a root certificate and private key.
// The CA provides an HTTP handler to issue certificates.
// The CA also provides a [Gauntlet] function to customize the certificate template.
// Call Close to release resources when done.
type CA struct {
io.Closer

cert *bifrost.Certificate
key *bifrost.PrivateKey
gh *gauntletHolder
Expand Down Expand Up @@ -233,6 +238,14 @@ func (ca *CA) IssueCertificate(asn1CSR []byte, notBefore, notAfter time.Time) ([
return certBytes, nil
}

// Close releases resources held by the CA.
// Multiple calls to Close are safe.
func (ca *CA) Close() error {
ca.gh.wg.Wait()

return nil
}

func readCsr(contentType string, body []byte) ([]byte, error) {
asn1Data := body

Expand Down
150 changes: 77 additions & 73 deletions tinyca/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,17 @@ import (
"golang.org/x/net/html"
)

const validCsr = `-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`

var (
testns = uuid.Must(uuid.Parse("80485314-6C73-40FF-86C5-A5942A0F514F"))
testNs = uuid.Must(uuid.Parse("80485314-6C73-40FF-86C5-A5942A0F514F"))

serveHTTPTests = []struct {
title string
Expand All @@ -43,28 +52,14 @@ var (
//
// Good requests.
{
title: "ok",
requestBody: []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`),
title: "ok",
requestBody: []byte(validCsr),
expectedCode: http.StatusOK,
},
{
title: "should return a binary DER encoded certificate",
accept: "application/octet-stream",
requestBody: []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`),
title: "should return a binary DER encoded certificate",
accept: "application/octet-stream",
requestBody: []byte(validCsr),
expectedCode: http.StatusOK,
},
{
Expand All @@ -81,29 +76,15 @@ l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
expectedCode: http.StatusOK,
},
{
title: "should return a PEM encoded certificate HTML fragment",
accept: "text/html",
requestBody: []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`),
title: "should return a PEM encoded certificate HTML fragment",
accept: "text/html",
requestBody: []byte(validCsr),
expectedCode: http.StatusOK,
},
{
title: "should return a PEM encoded certificate",
accept: "*/*",
requestBody: []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`),
title: "should return a PEM encoded certificate",
accept: "*/*",
requestBody: []byte(validCsr),
expectedCode: http.StatusOK,
},
// Bad.
Expand All @@ -125,14 +106,7 @@ a9rP0bn1HhVb/P8CIEMAqO2BWQ28M3Io0Wy+MTpqtX7/O1BAnSXT4BvZGUot
accept: "application/json",
requestMethod: http.MethodPost,
expectedCode: http.StatusNotAcceptable,
requestBody: []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`),
requestBody: []byte(validCsr),
},
{
title: "empty request",
Expand Down Expand Up @@ -207,31 +181,17 @@ FOioc6+qkAh+Sv8CIQDxi4eJOHAg3+eSnryb3zgsDIoGWcw3NRWI12Kwwr9Upw==
),
},
{
title: "gauntlet denied",
requestBody: []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`),
title: "gauntlet denied",
requestBody: []byte(validCsr),
gauntlet: func(_ context.Context, _ *bifrost.CertificateRequest) (*x509.Certificate, error) {
return nil, errors.New("boo")
},
expectedCode: http.StatusForbidden,
expectedBody: []byte("bifrost: certificate request denied, boo"),
},
{
title: "gauntlet timeout",
requestBody: []byte(`-----BEGIN CERTIFICATE REQUEST-----
MIIBGjCBwAIBADBeMS0wKwYDVQQDDCQwZjljMmFjNC1iZDdmLTU5MjMtYTc4NS1h
OGJjNGQ4ZTI4MzExLTArBgNVBAoMJDgwNDg1MzE0LTZDNzMtNDBGRi04NkM1LUE1
OTQyQTBGNTE0RjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym
aKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH
f/l+B/agADAKBggqhkjOPQQDAgNJADBGAiEAqvq1FkgO02cZp4Etg1T0KzimcO2Y
l83jqe9OFH2tJOwCIQDpQGF56BlTZG70I6mLhNGq1wVMNclYHq2cVUTPl6iMmg==
-----END CERTIFICATE REQUEST-----`),
title: "gauntlet timeout",
requestBody: []byte(validCsr),
gauntlet: func(ctx context.Context, _ *bifrost.CertificateRequest) (*x509.Certificate, error) {
<-ctx.Done()
return nil, nil
Expand All @@ -249,11 +209,16 @@ func TestCA_ServeHTTP(t *testing.T) {
}

for _, tc := range serveHTTPTests {
tc := tc

t.Run(tc.title, func(t *testing.T) {
t.Parallel()

ca, err := New(cert, key, tc.gauntlet)
if err != nil {
t.Fatal(err)
}
defer ca.Close()

method := http.MethodPost
if tc.requestMethod != "" {
Expand Down Expand Up @@ -288,7 +253,7 @@ func TestCA_ServeHTTP(t *testing.T) {
respBody, _ := io.ReadAll(resp.Body)
if exp := tc.expectedBody; len(exp) != 0 {
if !bytes.Equal(append(exp, "\n"...), respBody) {
t.Fatalf("expected body:\n```\n%s\n```\n\nactual body:\n```\n%s\n```\n",
t.Fatalf("\nexpected body:\n```\n%s\n```\n\nactual body:\n```\n%s\n```\n",
exp, string(respBody))
}
} else if resp.StatusCode < 300 {
Expand All @@ -310,16 +275,16 @@ func TestCA_ServeHTTP(t *testing.T) {
if err != nil {
t.Fatal("response body is not a valid bifrost certificate: ", err)
}
if cert.Namespace != testns {
t.Fatalf("expected namespace: %s, actual: %s\n", testns, cert.Namespace)
if cert.Namespace != testNs {
t.Fatalf("expected namespace: %s, actual: %s\n", testNs, cert.Namespace)
}
case webapp.MimeTypeBytes:
cert, err := bifrost.ParseCertificate(respBody)
if err != nil {
t.Fatal("response body is not a valid bifrost certificate: ", err)
}
if cert.Namespace != testns {
t.Fatalf("expected namespace: %s, actual: %s\n", testns, cert.Namespace)
if cert.Namespace != testNs {
t.Fatalf("expected namespace: %s, actual: %s\n", testNs, cert.Namespace)
}
case webapp.MimeTypeHtml:
_, err := html.Parse(resp.Body)
Expand All @@ -335,6 +300,45 @@ func TestCA_ServeHTTP(t *testing.T) {
}
}

func TestCA_gauntlet_panic(t *testing.T) {
cert, key, err := createCACertKey()
if err != nil {
t.Fatal(err)
}

ca, err := New(
cert,
key,
func(_ context.Context, _ *bifrost.CertificateRequest) (*x509.Certificate, error) {
panic("boom")
},
)
if err != nil {
t.Fatal(err)
}
defer ca.Close()

rr := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodPost, "/", bytes.NewReader([]byte(validCsr)))
if err != nil {
t.Fatal(err)
}

ca.ServeHTTP(rr, req)
resp := rr.Result()
defer resp.Body.Close()

if resp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("expected code: %d, actual: %d\n", http.StatusInternalServerError, resp.StatusCode)
}

body, _ := io.ReadAll(resp.Body)
expected := "bifrost: certificate request aborted, gauntlet panic('boom')\n"
if !bytes.Equal([]byte(expected), body) {
t.Fatalf("\nexpected body:\n```\n%s\n```\n\nactual body:\n```\n%s\n```\n", expected, body)
}
}

func createCACertKey() (*bifrost.Certificate, *bifrost.PrivateKey, error) {
randReader := rand.New(rand.NewSource(42))

Expand All @@ -344,9 +348,9 @@ func createCACertKey() (*bifrost.Certificate, *bifrost.PrivateKey, error) {
return nil, nil, err
}

id := bifrost.UUID(testns, key.PublicKey())
id := bifrost.UUID(testNs, key.PublicKey())

template, err := CACertTemplate(testns, id)
template, err := CACertTemplate(testNs, id)
if err != nil {
return nil, nil, err
}
Expand Down
14 changes: 13 additions & 1 deletion tinyca/gauntlet.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/x509"
"errors"
"fmt"
"sync"
"time"

"github.com/RealImage/bifrost"
Expand All @@ -15,6 +16,8 @@ import (
type gauntletHolder struct {
Gauntlet

wg *sync.WaitGroup

// metrics
denied *metrics.Counter
aborted *metrics.Counter
Expand All @@ -23,7 +26,7 @@ type gauntletHolder struct {

func newGauntletHolder(g Gauntlet, ns uuid.UUID) *gauntletHolder {
if g == nil {
return &gauntletHolder{}
return &gauntletHolder{wg: new(sync.WaitGroup)}
}

denied := bfMetricName("gauntlet_denied_total", ns)
Expand All @@ -33,6 +36,8 @@ func newGauntletHolder(g Gauntlet, ns uuid.UUID) *gauntletHolder {
return &gauntletHolder{
Gauntlet: g,

wg: new(sync.WaitGroup),

denied: bifrost.StatsForNerds.GetOrCreateCounter(denied),
aborted: bifrost.StatsForNerds.GetOrCreateCounter(aborted),
duration: bifrost.StatsForNerds.GetOrCreateHistogram(duration),
Expand All @@ -54,8 +59,15 @@ func (gh *gauntletHolder) throw(csr *bifrost.CertificateRequest) (*x509.Certific
}()

result := make(chan *x509.Certificate, 1)
gh.wg.Add(1)
go func() {
defer gh.wg.Done()
defer close(result)
defer func() {
if r := recover(); r != nil {
cancel(fmt.Errorf("%w, gauntlet panic('%v')", bifrost.ErrRequestAborted, r))
}
}()

start := time.Now()
template, err := gh.Gauntlet(ctx, csr)
Expand Down

0 comments on commit 51529eb

Please sign in to comment.