Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions acme/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type Order struct {
NotAfter string `json:"notAfter,omitempty"`
Authorizations []string `json:"authorizations"`
Certificate string `json:"certificate,omitempty"`

// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
Replaces string `json:"replaces,omitempty"`
}

// An Authorization is created for each identifier in an order
Expand Down
19 changes: 19 additions & 0 deletions ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,25 @@ func (ca *CAImpl) CompleteOrder(order *core.Order) {
order.Unlock()
}

// RecognizedSKID attempts to match the incoming Authority Key Idenfitier (AKID)
// bytes to the Subject Key Identifier (SKID) of an intermediate certificate. It
// returns an error if no match is found.
func (ca *CAImpl) RecognizedSKID(issuer []byte) error {
if issuer == nil {
return errors.New("issuer bytes must not be nil")
}

for _, chain := range ca.chains {
for _, intermediate := range chain.intermediates {
if bytes.Equal(intermediate.cert.Cert.SubjectKeyId, issuer) {
return nil
}
}
}

return errors.New("no known issuer matches the provided Authority Key Identifier ")
}

func (ca *CAImpl) GetNumberOfRootCerts() int {
return len(ca.chains)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/pebble/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func main() {

db := db.NewMemoryStore()
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, alternateRoots, chainLength, c.Pebble.CertificateValidityPeriod)
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress)
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress, db)

for keyID, key := range c.Pebble.ExternalAccountMACKeys {
err := db.AddExternalAccountKeyByID(keyID, key)
Expand Down
86 changes: 86 additions & 0 deletions core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"math/big"
"sync"
"time"

Expand All @@ -27,6 +29,8 @@ type Order struct {
AuthorizationObjects []*Authorization
BeganProcessing bool
CertificateObject *Certificate
// Indicates if the finalized order has been successfully replaced via ARI.
IsReplaced bool
}

func (o *Order) GetStatus() (string, error) {
Expand Down Expand Up @@ -200,3 +204,85 @@ type ValidationRecord struct {
Error *acme.ProblemDetails
ValidatedAt time.Time
}

// CertID represents a unique identifier (CertID) for a certificate as per the
// ACME protocol's "renewalInfo" resource, as specified in draft-ietf-acme-ari-
// 03. The CertID is a composite string derived from the base64url-encoded
// keyIdentifier of the certificate's Authority Key Identifier (AKI) and the
// base64url-encoded serial number of the certificate, separated by a period.
// For more details see:
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-02#section-4.1.
type CertID struct {
KeyIdentifier []byte
SerialNumber *big.Int
// id is the pre-computed hex encoding of SerialNumber.
id string
}

// SerialHex returns a CertID's id field.
func (c CertID) SerialHex() string {
return c.id
}

// NewCertID takes bytes representing a serial number and authority key
// identifier and returns a CertID or an error.
func NewCertID(serial []byte, akid []byte) (*CertID, error) {
if serial == nil || akid == nil {
return nil, errors.New("must send non-nil bytes")
}

return &CertID{
KeyIdentifier: akid,
SerialNumber: new(big.Int).SetBytes(serial),
id: hex.EncodeToString(serial),
}, nil
}

// SuggestedWindow is a type exposed inside the RenewalInfo resource.
type SuggestedWindow struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
}

// IsWithin returns true if the given time is within the suggested window,
// inclusive of the start time and exclusive of the end time.
func (window SuggestedWindow) IsWithin(now time.Time) bool {
return !now.Before(window.Start) && now.Before(window.End)
}

// RenewalInfo is a type which is exposed to clients which query the renewalInfo
// endpoint specified in draft-aaron-ari.
type RenewalInfo struct {
SuggestedWindow SuggestedWindow `json:"suggestedWindow"`
}

// RenewalInfoSimple constructs a `RenewalInfo` object and suggested window
// using a very simple renewal calculation: calculate a point 2/3rds of the way
// through the validity period, then give a 2-day window around that. Both the
// `issued` and `expires` timestamps are expected to be UTC.
func RenewalInfoSimple(issued time.Time, expires time.Time) *RenewalInfo {
validity := expires.Add(time.Second).Sub(issued)
renewalOffset := validity / time.Duration(3)
idealRenewal := expires.Add(-renewalOffset)
return &RenewalInfo{
SuggestedWindow: SuggestedWindow{
Start: idealRenewal.Add(-24 * time.Hour),
End: idealRenewal.Add(24 * time.Hour),
},
}
}

// RenewalInfoImmediate constructs a `RenewalInfo` object with a suggested
// window in the past. Per the draft-ietf-acme-ari-01 spec, clients should
// attempt to renew immediately if the suggested window is in the past. The
// passed `now` is assumed to be a timestamp representing the current moment in
// time.
func RenewalInfoImmediate(now time.Time) *RenewalInfo {
oneHourAgo := now.Add(-1 * time.Hour)
return &RenewalInfo{
SuggestedWindow: SuggestedWindow{
Start: oneHourAgo,
End: oneHourAgo.Add(time.Minute * 30),
},
}
}
57 changes: 55 additions & 2 deletions db/memorystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ type MemoryStore struct {
// key bytes.
accountsByKeyID map[string]*core.Account

ordersByID map[string]*core.Order
ordersByAccountID map[string][]*core.Order
// ordersByIssuedSerial indexes the hex encoding of the certificate's
// SerialNumber.
ordersByIssuedSerial map[string]*core.Order
ordersByID map[string]*core.Order
ordersByAccountID map[string][]*core.Order

authorizationsByID map[string]*core.Authorization

Expand All @@ -66,6 +69,7 @@ func NewMemoryStore() *MemoryStore {
accountRand: rand.New(rand.NewSource(time.Now().UnixNano())),
accountsByID: make(map[string]*core.Account),
accountsByKeyID: make(map[string]*core.Account),
ordersByIssuedSerial: make(map[string]*core.Order),
ordersByID: make(map[string]*core.Order),
ordersByAccountID: make(map[string][]*core.Order),
authorizationsByID: make(map[string]*core.Authorization),
Expand Down Expand Up @@ -94,6 +98,28 @@ func (m *MemoryStore) GetAccountByKey(key crypto.PublicKey) (*core.Account, erro
return m.accountsByKeyID[keyID], nil
}

// UpdateReplacedOrder takes a serial and marks a parent order as
// replaced/not-replaced or returns an error.
//
// We intentionally don't Lock the database inside this method because the inner
// GetOrderByIssuedSerial which is used elsewhere does an RLock which would
// hang.
func (m *MemoryStore) UpdateReplacedOrder(serial string, shouldBeReplaced bool) error {
if serial == "" {
return acme.InternalErrorProblem("no serial provided")
}

originalOrder, err := m.GetOrderByIssuedSerial(serial)
if err != nil {
return acme.InternalErrorProblem(fmt.Sprintf("could not find an order for the given certificate: %s", err))
}
originalOrder.Lock()
defer originalOrder.Unlock()
originalOrder.IsReplaced = shouldBeReplaced

return nil
}

// Note that this function should *NOT* be used for key changes. It assumes
// the public key associated to the account does not change. Use ChangeAccountKey
// to change the account's public key.
Expand Down Expand Up @@ -195,6 +221,19 @@ func (m *MemoryStore) AddOrder(order *core.Order) (int, error) {
return len(m.ordersByID), nil
}

func (m *MemoryStore) AddOrderByIssuedSerial(order *core.Order) error {
m.Lock()
defer m.Unlock()

if order.CertificateObject == nil {
return errors.New("order must have non-empty CertificateObject")
}

m.ordersByIssuedSerial[order.CertificateObject.ID] = order

return nil
}

func (m *MemoryStore) GetOrderByID(id string) *core.Order {
m.RLock()
defer m.RUnlock()
Expand All @@ -212,6 +251,20 @@ func (m *MemoryStore) GetOrderByID(id string) *core.Order {
return nil
}

// GetOrderByIssuedSerial returns the order that resulted in the given certificate
// serial. If no such order exists, an error will be returned.
func (m *MemoryStore) GetOrderByIssuedSerial(serial string) (*core.Order, error) {
m.RLock()
defer m.RUnlock()

order, ok := m.ordersByIssuedSerial[serial]
if !ok {
return nil, errors.New("could not find order resulting in the given certificate serial number")
}

return order, nil
}

func (m *MemoryStore) GetOrdersByAccountID(accountID string) []*core.Order {
m.RLock()
defer m.RUnlock()
Expand Down
19 changes: 17 additions & 2 deletions va/va.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/letsencrypt/challtestsrv"
"github.com/letsencrypt/pebble/v2/acme"
"github.com/letsencrypt/pebble/v2/core"
"github.com/letsencrypt/pebble/v2/db"
)

const (
Expand Down Expand Up @@ -108,12 +109,18 @@ type VAImpl struct {
strict bool
customResolverAddr string
dnsClient *dns.Client

// The VA having a DB client is indeed strange. This is only used to
// facilitate va.setOrderError changing the ARI related order replacement
// field on failed orders.
db *db.MemoryStore
}

func New(
log *log.Logger,
httpPort, tlsPort int,
strict bool, customResolverAddr string,
db *db.MemoryStore,
) *VAImpl {
va := &VAImpl{
log: log,
Expand All @@ -124,6 +131,7 @@ func New(
sleepTime: defaultSleepTime,
strict: strict,
customResolverAddr: customResolverAddr,
db: db,
}

if customResolverAddr != "" {
Expand Down Expand Up @@ -209,10 +217,17 @@ func (va VAImpl) setAuthzValid(authz *core.Authorization, chal *core.Challenge)

// setOrderError updates an order with an error from an authorization
// validation.
func (va VAImpl) setOrderError(order *core.Order, err *acme.ProblemDetails) {
func (va VAImpl) setOrderError(order *core.Order, prob *acme.ProblemDetails) {
order.Lock()
defer order.Unlock()
order.Error = err
order.Error = prob

// Mark the parent order as "not replaced yet" so a new replacement order
// can be attempted.
err := va.db.UpdateReplacedOrder(order.Replaces, false)
if err != nil {
va.log.Printf("Error updating replacement order: %s", err)
}
}

// setAuthzInvalid updates an authorization and an associated challenge to be
Expand Down
2 changes: 1 addition & 1 deletion va/va_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestAuthzRace(_ *testing.T) {

// This whole test can be removed if/when the MemoryStore becomes 100% by value
ms := db.NewMemoryStore()
va := New(log.New(os.Stdout, "Pebble/TestRace", log.LstdFlags), 14000, 15000, false, "")
va := New(log.New(os.Stdout, "Pebble/TestRace", log.LstdFlags), 14000, 15000, false, "", ms)

authz := &core.Authorization{
ID: "auth-id",
Expand Down
Loading