From b724671435f36089827d8b8cfeb43a419c19e159 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 30 Jun 2022 11:46:24 -0700 Subject: [PATCH] ocsp/responder: add live source (#6200) Note this is not yet plumbed up into cmd/ocsp-responder/main.go, because we will want do that with it wrapped in a Redis caching layer. Fixes #6190. --- go.mod | 1 + go.sum | 2 + ocsp/responder/live/live.go | 52 +++++++ ocsp/responder/live/live_test.go | 78 ++++++++++ vendor/golang.org/x/sync/AUTHORS | 3 + vendor/golang.org/x/sync/CONTRIBUTORS | 3 + vendor/golang.org/x/sync/LICENSE | 27 ++++ vendor/golang.org/x/sync/PATENTS | 22 +++ .../golang.org/x/sync/semaphore/semaphore.go | 136 ++++++++++++++++++ vendor/modules.txt | 3 + 10 files changed, 327 insertions(+) create mode 100644 ocsp/responder/live/live.go create mode 100644 ocsp/responder/live/live_test.go create mode 100644 vendor/golang.org/x/sync/AUTHORS create mode 100644 vendor/golang.org/x/sync/CONTRIBUTORS create mode 100644 vendor/golang.org/x/sync/LICENSE create mode 100644 vendor/golang.org/x/sync/PATENTS create mode 100644 vendor/golang.org/x/sync/semaphore/semaphore.go diff --git a/go.mod b/go.mod index 313e759c4..ddfd1bfab 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( go.opentelemetry.io/otel v0.19.0 // indirect go.opentelemetry.io/otel/trace v0.19.0 // indirect golang.org/x/mod v0.4.2 // indirect + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/go.sum b/go.sum index c6a8ef071..447014b8e 100644 --- a/go.sum +++ b/go.sum @@ -590,6 +590,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/ocsp/responder/live/live.go b/ocsp/responder/live/live.go new file mode 100644 index 000000000..4db333f67 --- /dev/null +++ b/ocsp/responder/live/live.go @@ -0,0 +1,52 @@ +package live + +import ( + "context" + + capb "github.com/letsencrypt/boulder/ca/proto" + "github.com/letsencrypt/boulder/core" + "github.com/letsencrypt/boulder/ocsp/responder" + rapb "github.com/letsencrypt/boulder/ra/proto" + "golang.org/x/crypto/ocsp" + "golang.org/x/sync/semaphore" + "google.golang.org/grpc" +) + +type ocspGenerator interface { + GenerateOCSP(ctx context.Context, in *rapb.GenerateOCSPRequest, opts ...grpc.CallOption) (*capb.OCSPResponse, error) +} + +type Source struct { + ra ocspGenerator + sem *semaphore.Weighted +} + +func New(ra ocspGenerator, maxInflight int64) *Source { + return &Source{ + ra: ra, + sem: semaphore.NewWeighted(maxInflight), + } +} + +func (s *Source) Response(ctx context.Context, req *ocsp.Request) (*responder.Response, error) { + err := s.sem.Acquire(ctx, 1) + if err != nil { + return nil, err + } + defer s.sem.Release(1) + + resp, err := s.ra.GenerateOCSP(ctx, &rapb.GenerateOCSPRequest{ + Serial: core.SerialToString(req.SerialNumber), + }) + if err != nil { + return nil, err + } + parsed, err := ocsp.ParseResponse(resp.Response, nil) + if err != nil { + return nil, err + } + return &responder.Response{ + Raw: resp.Response, + Response: parsed, + }, nil +} diff --git a/ocsp/responder/live/live_test.go b/ocsp/responder/live/live_test.go new file mode 100644 index 000000000..fc9212cec --- /dev/null +++ b/ocsp/responder/live/live_test.go @@ -0,0 +1,78 @@ +package live + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "testing" + + capb "github.com/letsencrypt/boulder/ca/proto" + "github.com/letsencrypt/boulder/core" + rapb "github.com/letsencrypt/boulder/ra/proto" + "github.com/letsencrypt/boulder/test" + "golang.org/x/crypto/ocsp" + "google.golang.org/grpc" +) + +// mockOCSPGenerator is an ocspGenerator that always emits the provided bytes +// when serial number 1 is requested, but otherwise returns an error. +type mockOCSPGenerator struct { + resp []byte +} + +func (m mockOCSPGenerator) GenerateOCSP(ctx context.Context, in *rapb.GenerateOCSPRequest, opts ...grpc.CallOption) (*capb.OCSPResponse, error) { + expectedSerial := core.SerialToString(big.NewInt(1)) + if in.Serial != expectedSerial { + return nil, fmt.Errorf("expected serial %s, got %s", expectedSerial, in.Serial) + } + + return &capb.OCSPResponse{Response: m.resp}, nil +} + +func TestLiveResponse(t *testing.T) { + // Make a fake CA to sign OCSP with + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + template := &x509.Certificate{ + SerialNumber: big.NewInt(1337), + BasicConstraintsValid: true, + IsCA: true, + Subject: pkix.Name{CommonName: "test CA"}, + } + issuerBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + + issuer, err := x509.ParseCertificate(issuerBytes) + if err != nil { + t.Fatal(err) + } + + eeSerial := big.NewInt(1) + + respBytes, err := ocsp.CreateResponse(issuer, issuer, ocsp.Response{ + SerialNumber: eeSerial, + }, key) + if err != nil { + t.Fatal(err) + } + + source := New(mockOCSPGenerator{respBytes}, 1) + resp, err := source.Response(context.Background(), &ocsp.Request{ + SerialNumber: eeSerial, + }) + test.AssertNotError(t, err, "getting response") + test.AssertByteEquals(t, resp.Raw, respBytes) + expectedSerial := "000000000000000000000000000000000001" + if core.SerialToString(resp.SerialNumber) != expectedSerial { + t.Errorf("expected serial %s, got %s", expectedSerial, resp.SerialNumber) + } +} diff --git a/vendor/golang.org/x/sync/AUTHORS b/vendor/golang.org/x/sync/AUTHORS new file mode 100644 index 000000000..15167cd74 --- /dev/null +++ b/vendor/golang.org/x/sync/AUTHORS @@ -0,0 +1,3 @@ +# This source code refers to The Go Authors for copyright purposes. +# The master list of authors is in the main Go distribution, +# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/golang.org/x/sync/CONTRIBUTORS b/vendor/golang.org/x/sync/CONTRIBUTORS new file mode 100644 index 000000000..1c4577e96 --- /dev/null +++ b/vendor/golang.org/x/sync/CONTRIBUTORS @@ -0,0 +1,3 @@ +# This source code was written by the Go contributors. +# The master list of contributors is in the main Go distribution, +# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/golang.org/x/sync/LICENSE b/vendor/golang.org/x/sync/LICENSE new file mode 100644 index 000000000..6a66aea5e --- /dev/null +++ b/vendor/golang.org/x/sync/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/sync/PATENTS b/vendor/golang.org/x/sync/PATENTS new file mode 100644 index 000000000..733099041 --- /dev/null +++ b/vendor/golang.org/x/sync/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/sync/semaphore/semaphore.go b/vendor/golang.org/x/sync/semaphore/semaphore.go new file mode 100644 index 000000000..30f632c57 --- /dev/null +++ b/vendor/golang.org/x/sync/semaphore/semaphore.go @@ -0,0 +1,136 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package semaphore provides a weighted semaphore implementation. +package semaphore // import "golang.org/x/sync/semaphore" + +import ( + "container/list" + "context" + "sync" +) + +type waiter struct { + n int64 + ready chan<- struct{} // Closed when semaphore acquired. +} + +// NewWeighted creates a new weighted semaphore with the given +// maximum combined weight for concurrent access. +func NewWeighted(n int64) *Weighted { + w := &Weighted{size: n} + return w +} + +// Weighted provides a way to bound concurrent access to a resource. +// The callers can request access with a given weight. +type Weighted struct { + size int64 + cur int64 + mu sync.Mutex + waiters list.List +} + +// Acquire acquires the semaphore with a weight of n, blocking until resources +// are available or ctx is done. On success, returns nil. On failure, returns +// ctx.Err() and leaves the semaphore unchanged. +// +// If ctx is already done, Acquire may still succeed without blocking. +func (s *Weighted) Acquire(ctx context.Context, n int64) error { + s.mu.Lock() + if s.size-s.cur >= n && s.waiters.Len() == 0 { + s.cur += n + s.mu.Unlock() + return nil + } + + if n > s.size { + // Don't make other Acquire calls block on one that's doomed to fail. + s.mu.Unlock() + <-ctx.Done() + return ctx.Err() + } + + ready := make(chan struct{}) + w := waiter{n: n, ready: ready} + elem := s.waiters.PushBack(w) + s.mu.Unlock() + + select { + case <-ctx.Done(): + err := ctx.Err() + s.mu.Lock() + select { + case <-ready: + // Acquired the semaphore after we were canceled. Rather than trying to + // fix up the queue, just pretend we didn't notice the cancelation. + err = nil + default: + isFront := s.waiters.Front() == elem + s.waiters.Remove(elem) + // If we're at the front and there're extra tokens left, notify other waiters. + if isFront && s.size > s.cur { + s.notifyWaiters() + } + } + s.mu.Unlock() + return err + + case <-ready: + return nil + } +} + +// TryAcquire acquires the semaphore with a weight of n without blocking. +// On success, returns true. On failure, returns false and leaves the semaphore unchanged. +func (s *Weighted) TryAcquire(n int64) bool { + s.mu.Lock() + success := s.size-s.cur >= n && s.waiters.Len() == 0 + if success { + s.cur += n + } + s.mu.Unlock() + return success +} + +// Release releases the semaphore with a weight of n. +func (s *Weighted) Release(n int64) { + s.mu.Lock() + s.cur -= n + if s.cur < 0 { + s.mu.Unlock() + panic("semaphore: released more than held") + } + s.notifyWaiters() + s.mu.Unlock() +} + +func (s *Weighted) notifyWaiters() { + for { + next := s.waiters.Front() + if next == nil { + break // No more waiters blocked. + } + + w := next.Value.(waiter) + if s.size-s.cur < w.n { + // Not enough tokens for the next waiter. We could keep going (to try to + // find a waiter with a smaller request), but under load that could cause + // starvation for large requests; instead, we leave all remaining waiters + // blocked. + // + // Consider a semaphore used as a read-write lock, with N tokens, N + // readers, and one writer. Each reader can Acquire(1) to obtain a read + // lock. The writer can Acquire(N) to obtain a write lock, excluding all + // of the readers. If we allow the readers to jump ahead in the queue, + // the writer will starve — there is always one token available for every + // reader. + break + } + + s.cur += w.n + s.waiters.Remove(next) + close(w.ready) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index bb5f9f146..dc2b99920 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -225,6 +225,9 @@ golang.org/x/net/internal/timeseries golang.org/x/net/ipv4 golang.org/x/net/ipv6 golang.org/x/net/trace +# golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f +## explicit +golang.org/x/sync/semaphore # golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 ## explicit; go 1.17 golang.org/x/sys/execabs