Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
12 changes: 12 additions & 0 deletions ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,18 @@ func (ca *CAImpl) CompleteOrder(order *core.Order) {
order.Unlock()
}

func (ca *CAImpl) GetSubjectKeyIDs() core.SubjectKeyIDs {
skis := core.SubjectKeyIDs{}
for _, chain := range ca.chains {
skis = append(skis, chain.root.cert.Cert.SubjectKeyId)
for _, intermediate := range chain.intermediates {
skis = append(skis, intermediate.cert.Cert.SubjectKeyId)
}
}

return skis
}

func (ca *CAImpl) GetNumberOfRootCerts() int {
return len(ca.chains)
}
Expand Down
70 changes: 70 additions & 0 deletions core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"math/big"
"sync"
"time"

Expand All @@ -27,6 +28,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 +203,70 @@ type ValidationRecord struct {
Error *acme.ProblemDetails
ValidatedAt time.Time
}

// SubjectKeyIDs is a convenience type that holds the Subject Key Identifier
// value for each Pebble generated root and intermediate certificate.
type SubjectKeyIDs [][]byte

// 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
}

// 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),
},
}
}
54 changes: 52 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,25 @@ func (m *MemoryStore) GetAccountByKey(key crypto.PublicKey) (*core.Account, erro
return m.accountsByKeyID[keyID], nil
}

// UpdateReplacedOrder
// We intentionally don't Lock the database inside this method because the
// inner GetOrderByIssuedSerial which is used elsewhere does an RLock which
// would hang.
// , account *core.Account
func (m *MemoryStore) UpdateReplacedOrder(serial string) 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.IsReplaced = true

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 +218,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 +248,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
Loading