Skip to content

Commit

Permalink
wfe: implement reuse of valid authorizations. (#259)
Browse files Browse the repository at this point in the history
RFC8555 mentions (https://tools.ietf.org/html/rfc8555#section-7.1.3)
that clients should check the "status" field of an order to determine
whether they need to take any action.

This commit implements authorization reuse, such that any valid and
unexpired authz completed by the ACME account for that identifier has a
chance to be reused in new orders. The chance is controlled by the env
variable PEBBLE_AUTHZREUSE and it defaults to 50 (percent). The
chance is evaluated regardless of whether there is an eligible authz to
reuse or not.
  • Loading branch information
alexzorin authored and Daniel McCarney committed Jul 29, 2019
1 parent ddbb755 commit 07e1be6
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 36 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ To **never** reject a valid nonce as invalid run:

`PEBBLE_WFE_NONCEREJECT=0 pebble`

### Authorization Reuse

ACME servers may choose to reuse valid authorizations from previous orders in new orders. ACME clients [should always check](https://tools.ietf.org/html/rfc8555#section-7.1.3) the status of a new order and its authorizations to confirm whether they need to respond to any challenges.

**Pebble will reuse valid authorizations in new orders, if they exist, 50% of the time**.

The percentage may be controlled with the environment variable `PEBBLE_AUTHZREUSE`, e.g. to always reuse authorizations:

`PEBBLE_AUTHZREUSE=100 pebble`

### Avoiding Client HTTPS Errors

By default Pebble is accessible over HTTPS-only and uses a [test
Expand Down
4 changes: 4 additions & 0 deletions acme/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type Identifier struct {
Value string `json:"value"`
}

func (ident Identifier) Equals(other Identifier) bool {
return ident.Type == other.Type && ident.Value == other.Value
}

type Account struct {
Status string `json:"status"`
Contact []string `json:"contact,omitempty"`
Expand Down
17 changes: 17 additions & 0 deletions db/memorystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"reflect"
"strconv"
"sync"
"time"

"gopkg.in/square/go-jose.v2"

"github.com/letsencrypt/pebble/acme"
"github.com/letsencrypt/pebble/core"
)

Expand Down Expand Up @@ -214,6 +216,21 @@ func (m *MemoryStore) GetAuthorizationByID(id string) *core.Authorization {
return m.authorizationsByID[id]
}

// FindValidAuthorization fetches the first, if any, valid and unexpired authorization for the
// provided identifier, from the ACME account matching accountID.
func (m *MemoryStore) FindValidAuthorization(accountID string, identifier acme.Identifier) *core.Authorization {
m.RLock()
defer m.RUnlock()
for _, authz := range m.authorizationsByID {
if authz.Status == acme.StatusValid && identifier.Equals(authz.Identifier) &&
authz.Order != nil && authz.Order.AccountID == accountID &&
authz.ExpiresDate.After(time.Now()) {
return authz
}
}
return nil
}

func (m *MemoryStore) AddChallenge(chal *core.Challenge) (int, error) {
m.Lock()
defer m.Unlock()
Expand Down
99 changes: 63 additions & 36 deletions wfe/wfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ const (
// http://www.itu.int/rec/T-REC-X.509-201210-I/en
unusedRevocationReason = 7
aACompromiseRevocationReason = 10

// authzReuseEnvVar defines an environment variable name used to provide a
// percentage value for how often Pebble should try to reuse valid authorizations
// for each identifier in an order. The percentage is independent of whether a
// valid authorization exists or not for each identifier in an order.
authzReuseEnvVar = "PEBBLE_AUTHZREUSE"

// The default value when PEBBLE_WFE_AUTHZREUSE is not set, how often to try
// and reuse valid authorizations.
defaultAuthzReuse = 50
)

type wfeHandlerFunc func(context.Context, http.ResponseWriter, *http.Request)
Expand All @@ -116,13 +126,14 @@ func (th *topHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

type WebFrontEndImpl struct {
log *log.Logger
db *db.MemoryStore
nonce *nonceMap
nonceErrPercent int
va *va.VAImpl
ca *ca.CAImpl
strict bool
log *log.Logger
db *db.MemoryStore
nonce *nonceMap
nonceErrPercent int
authzReusePercent int
va *va.VAImpl
ca *ca.CAImpl
strict bool
}

const ToSURL = "data:text/plain,Do%20what%20thou%20wilt"
Expand Down Expand Up @@ -156,14 +167,24 @@ func New(
}
log.Printf("Configured to reject %d%% of good nonces", nonceErrPercent)

// Get authz reuse percent from the environment
authzReusePercent := defaultAuthzReuse
if val, err := strconv.ParseInt(os.Getenv(authzReuseEnvVar), 10, 0); err == nil &&
val >= 0 && val <= 100 {
authzReusePercent = int(val)
}
log.Printf("Configured to attempt authz reuse for each identifier %d%% of the time",
authzReusePercent)

return WebFrontEndImpl{
log: log,
db: db,
nonce: newNonceMap(),
nonceErrPercent: nonceErrPercent,
va: va,
ca: ca,
strict: strict,
log: log,
db: db,
nonce: newNonceMap(),
nonceErrPercent: nonceErrPercent,
authzReusePercent: authzReusePercent,
va: va,
ca: ca,
strict: strict,
}
}

Expand Down Expand Up @@ -1263,36 +1284,42 @@ func (wfe *WebFrontEndImpl) makeAuthorizations(order *core.Order, request *http.

// Lock the order for reading
order.RLock()
// Create one authz for each name in the order's parsed CSR
// Add one authz for each name in the order's parsed CSR
for _, name := range order.Identifiers {
now := time.Now().UTC()
expires := now.Add(pendingAuthzExpire)
ident := acme.Identifier{
Type: name.Type,
Value: name.Value,
}
authz := &core.Authorization{
ID: newToken(),
ExpiresDate: expires,
Order: order,
Authorization: acme.Authorization{
Status: acme.StatusPending,
Identifier: ident,
Expires: expires.UTC().Format(time.RFC3339),
},
}
authz.URL = wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", authzPath, authz.ID))
// Create the challenges for this authz
err := wfe.makeChallenges(authz, request)
if err != nil {
return err
}
// Save the authorization in memory
count, err := wfe.db.AddAuthorization(authz)
if err != nil {
return err
// If there is an existing valid authz for this identifier, we can reuse it
authz := wfe.db.FindValidAuthorization(order.AccountID, ident)
// Otherwise create a new pending authz (and randomly not)
if authz == nil || rand.Intn(100) > wfe.authzReusePercent {
authz = &core.Authorization{
ID: newToken(),
ExpiresDate: expires,
Order: order,
Authorization: acme.Authorization{
Status: acme.StatusPending,
Identifier: ident,
Expires: expires.UTC().Format(time.RFC3339),
},
}
authz.URL = wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", authzPath, authz.ID))
// Create the challenges for this authz
err := wfe.makeChallenges(authz, request)
if err != nil {
return err
}
// Save the authorization in memory
count, err := wfe.db.AddAuthorization(authz)
if err != nil {
return err
}
wfe.log.Printf("There are now %d authorizations in the db\n", count)
}
wfe.log.Printf("There are now %d authorizations in the db\n", count)

authzURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%s", authzPath, authz.ID))
auths = append(auths, authzURL)
authObs = append(authObs, authz)
Expand Down

0 comments on commit 07e1be6

Please sign in to comment.