Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structure of ACME Tidy #20494

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions builtin/logical/pki/acme_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ type acmeAccount struct {
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
Jwk []byte `json:"jwk"`
AcmeDirectory string `json:"acme-directory"`
AccountCreatedDate time.Time `json:"account_created_date"`
AccountRevokedDate time.Time `json:"account_revoked_date"`
}

type acmeOrder struct {
Expand Down
82 changes: 82 additions & 0 deletions builtin/logical/pki/path_acme_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"strings"
"time"

"github.com/hashicorp/go-secure-stdlib/strutil"

Expand Down Expand Up @@ -294,3 +295,84 @@ func (b *backend) acmeNewAccountUpdateHandler(acmeCtx *acmeContext, userCtx *jws
resp := formatAccountResponse(acmeCtx, account)
return resp, nil
}

func (b *backend) tidyAcmeAccountByThumbprint(as *acmeState, ac *acmeContext, keyThumbprint string, certTidyBuffer, accountTidyBuffer time.Duration) error {
thumbprintEntry, err := ac.sc.Storage.Get(ac.sc.Context, acmeThumbprintPrefix+keyThumbprint)
if err != nil {
return fmt.Errorf("error retrieving thumbprint entry %v, unable to find corresponding account entry: %w", keyThumbprint, err)
}
if thumbprintEntry == nil {
return fmt.Errorf("empty thumbprint entry %v, unable to find corresponding account entry", keyThumbprint)
}

var thumbprint acmeThumbprint
err = thumbprintEntry.DecodeJSON(&thumbprint)
if err != nil {
return fmt.Errorf("unable to decode thumbprint entry %v to find account entry: %w", keyThumbprint, err)
}

if len(thumbprint.Kid) == 0 {
return fmt.Errorf("unable to find account entry: empty kid within thumbprint entry: %s", keyThumbprint)
}

// Now Get the Account:
accountEntry, err := ac.sc.Storage.Get(ac.sc.Context, acmeAccountPrefix+thumbprint.Kid)
if err != nil {
return err
}
if accountEntry == nil {
// We delete the Thumbprint Associated with the Account, and we are done
err = ac.sc.Storage.Delete(ac.sc.Context, acmeThumbprintPrefix+keyThumbprint)
if err != nil {
return err
}
return nil
}

var account acmeAccount
err = accountEntry.DecodeJSON(&account)
if err != nil {
return err
}

// Tidy Orders On the Account
orderIds, err := as.ListOrderIds(ac, thumbprint.Kid)
if err != nil {
return err
}
allOrdersTidied := true
for orderId, _ := range orderIds {
wasTidied, err := b.acmeTidyOrder(ac, thumbprint.Kid, acmeAccountPrefix+thumbprint.Kid+"/orders/"+string(orderId), ac.sc, certTidyBuffer)
if err != nil {
return err
}
if !wasTidied {
allOrdersTidied = false
}
}

if allOrdersTidied && time.Now().After(account.AccountCreatedDate.Add(accountTidyBuffer)) {
// Tidy this account
// If it is Revoked or Deactivated:
if (account.Status == StatusRevoked || account.Status == StatusDeactivated) && time.Now().After(account.AccountRevokedDate.Add(accountTidyBuffer)) {
// We Delete the Account Associated with this Thumbprint:
err = ac.sc.Storage.Delete(ac.sc.Context, acmeAccountPrefix+thumbprint.Kid)
if err != nil {
return err
}

// Now we delete the Thumbprint Associated with the Account:
err = ac.sc.Storage.Delete(ac.sc.Context, acmeThumbprintPrefix+keyThumbprint)
if err != nil {
return err
}
} else if account.Status == StatusValid {
// Revoke This Account
account.AccountRevokedDate = time.Now()
account.Status = StatusRevoked
as.UpdateAccount(ac, &account)
}
}

return nil
}
87 changes: 87 additions & 0 deletions builtin/logical/pki/path_acme_order.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,90 @@ func parseOrderIdentifiers(data map[string]interface{}) ([]*ACMEIdentifier, erro

return identifiers, nil
}

func (b *backend) acmeTidyOrder(ac *acmeContext, accountId string, orderPath string, sc *storageContext, certTidyBuffer time.Duration) (wasTidied bool, err error) {
// First we get the order; note that the orderPath includes the account
// It's only accessed at acme/orders/<order_id> with the account context
// It's saved at acme/<account_id>/orders/<orderId>
entry, err := ac.sc.Storage.Get(ac.sc.Context, orderPath)
if err != nil {
return false, fmt.Errorf("error loading order: %w", err)
}
if entry == nil {
return false, fmt.Errorf("order does not exist: %w", ErrMalformed)
}
var order acmeOrder
err = entry.DecodeJSON(&order)
if err != nil {
return false, fmt.Errorf("error decoding order: %w", err)
}

// Determine whether we should tidy this order
shouldTidy := false
// It is faster to check certificate information on the order entry rather than fetch the cert entry to parse:
if !order.CertificateExpiry.IsZero() {
// This implies that a certificate exists
// When a certificate exists, we want to expire and tidy the order when we tidy the certificate:
if time.Now().After(order.CertificateExpiry.Add(certTidyBuffer)) { // It's time to clean
shouldTidy = true
}
} else {
// This implies that no certificate exists
// In this case, we want to expire the order after it has expired (+ some safety buffer)
if time.Now().After(order.Expires) {
shouldTidy = true
}
}
if shouldTidy == false {
return shouldTidy, nil
}

// Tidy this Order
// That includes any certificate acme/<account_id>/orders/orderPath/cert
// That also includes any related authorizations: acme/<account_id>/authorizations/<auth_id>

// First Authorizations
for _, authorizationId := range order.AuthorizationIds {
authorizationEntry, err := sc.Storage.Get(ac.sc.Context, getAuthorizationPath(accountId, authorizationId))
if err != nil {
// If we continue here, we could fail to delete an authorization, but delete the order
// That would leave it dangling forever more, so we need to return
return false, err
}
if authorizationEntry == nil {
// If there was a storage error (above) in a previous run, it's possible that we've already tidied this
// In this case, don't fail, but do log that this shouldn't happen:
b.Logger().Warn("Encountered Missing Authorization Entry During Tidy: %v referred to Auth %v , but that authorization was not found", orderPath, authorizationId)
}

// Check Authorization Has Expired
var authorization ACMEAuthorization
err = authorizationEntry.DecodeJSON(&authorization)
if err != nil {
return false, err
}
expires, err := authorization.GetExpires()
if err != nil {
return false, err
}
if time.Now().Before(expires) {
return false, fmt.Errorf("error tidying authorization %v on order %v, authorization still valid, expires %t", authorizationId, orderPath, expires)
}

// Delete the Authorization
err = ac.sc.Storage.Delete(ac.sc.Context, getAuthorizationPath(accountId, authorizationId))
if err != nil {
return false, err
}
}

// Normal Tidy will Take Care of the Certificate

// And Finally, the order:
err = ac.sc.Storage.Delete(ac.sc.Context, orderPath)
if err != nil {
return false, err
}

return true, nil
}