Skip to content

Commit

Permalink
Add support for testing ARI in Pebble (#24)
Browse files Browse the repository at this point in the history
* Support testing ARI against Pebble

* Test replacing a replacement order that had previously failed

* Deactivate prior authorizations to induce a validation failure

* Skip ARI tests if server doesnt support ARI
  • Loading branch information
pgporada authored May 29, 2024
1 parent 79b263f commit 79ababe
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 46 deletions.
21 changes: 13 additions & 8 deletions acme.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ func (c Client) getPollingDurations() (time.Duration, time.Duration) {
return pollInterval, pollTimeout
}

// Helper function to have a central point for performing http requests.
// Stores any returned nonces in the stack.
// Helper function to have a central point for performing http requests. Stores
// any returned nonces in the stack. The caller is responsible for closing the
// body so they can read the response.
func (c Client) do(req *http.Request, addNonce bool) (*http.Response, error) {
// identifier for this client, as well as the default go user agent
if c.userAgentSuffix != "" {
Expand All @@ -100,7 +101,8 @@ func (c Client) do(req *http.Request, addNonce bool) (*http.Response, error) {
return resp, nil
}

// Helper function to perform an HTTP get request and read the body.
// Helper function to perform an HTTP get request and read the body. The caller
// is responsible for closing the body so they can read the response.
func (c Client) getRaw(url string, expectedStatus ...int) (*http.Response, []byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
Expand All @@ -125,7 +127,8 @@ func (c Client) getRaw(url string, expectedStatus ...int) (*http.Response, []byt
return resp, body, nil
}

// Helper function for performing a http get on an acme resource.
// Helper function for performing a http get on an acme resource. The caller is
// responsible for closing the body so they can read the response.
func (c Client) get(url string, out interface{}, expectedStatus ...int) (*http.Response, error) {
resp, body, err := c.getRaw(url, expectedStatus...)
if err != nil {
Expand Down Expand Up @@ -165,8 +168,9 @@ func (c Client) nonce() (string, error) {
return nonce, nil
}

// Helper function to perform an HTTP post request and read the body.
// Will attempt to retry if error is badNonce
// Helper function to perform an HTTP post request and read the body. Will
// attempt to retry if error is badNonce. The caller is responsible for closing
// the body so they can read the response.
func (c Client) postRaw(retryCount int, requestURL, kid string, privateKey crypto.Signer, payload interface{}, expectedStatus []int) (*http.Response, []byte, error) {
nonce, err := c.nonce()
if err != nil {
Expand Down Expand Up @@ -215,7 +219,8 @@ func (c Client) postRaw(retryCount int, requestURL, kid string, privateKey crypt
return resp, body, nil
}

// Helper function for performing a http post to an acme resource.
// Helper function for performing a http post to an acme resource. The caller is
// responsible for closing the body so they can read the response.
func (c Client) post(requestURL, keyID string, privateKey crypto.Signer, payload interface{}, out interface{}, expectedStatus ...int) (*http.Response, error) {
resp, body, err := c.postRaw(0, requestURL, keyID, privateKey, payload, expectedStatus)
if err != nil {
Expand All @@ -240,7 +245,7 @@ func (c Client) post(requestURL, keyID string, privateKey crypto.Signer, payload

var regLink = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`)

// Fetches a http Link header from a http response
// Fetches a http Link header from an http response and closes the body.
func fetchLink(resp *http.Response, wantedLink string) string {
if resp == nil {
return ""
Expand Down
1 change: 1 addition & 0 deletions ari.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func (c Client) GetRenewalInfo(cert *x509.Certificate) (RenewalInfo, error) {
if err != nil {
return ri, err
}
defer resp.Body.Close()

ri.RetryAfter, err = parseRetryAfter(resp.Header.Get("Retry-After"))
return ri, err
Expand Down
102 changes: 99 additions & 3 deletions ari_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
)

func TestClient_GetRenewalInfo(t *testing.T) {
if testClientMeta.Software == clientPebble {
t.Skip("pebble doesnt support ari")
if testClient.dir.RenewalInfo == "" {
t.Skip("acme server does not support ari renewals")
return
}

Expand All @@ -18,13 +18,16 @@ func TestClient_GetRenewalInfo(t *testing.T) {
t.Fatalf("no certificate: %+v", order)
}
certs, err := testClient.FetchCertificates(account, order.Certificate)
t.Logf("Issued serial %s\n", certs[0].SerialNumber.String())
if err != nil {
t.Fatalf("expeceted no error, got: %v", err)
t.Fatalf("expected no error, got: %v", err)
}
if len(certs) < 2 {
t.Fatalf("no certs")
}

renewalInfo, err := testClient.GetRenewalInfo(certs[0])
t.Logf("Suggested renewal window for new issuance: %v\n", renewalInfo.SuggestedWindow)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
Expand All @@ -40,6 +43,99 @@ func TestClient_GetRenewalInfo(t *testing.T) {
if renewalInfo.SuggestedWindow.End.Before(renewalInfo.SuggestedWindow.Start) {
t.Fatalf("suggested window end is before start?")
}

err = testClient.RevokeCertificate(account, certs[0], account.PrivateKey, ReasonUnspecified)
if err != nil {
t.Fatalf("failed to revoke certificate: %v", err)
}

// The renewal window should adjust to allow immediate renewal
renewalInfo, err = testClient.GetRenewalInfo(certs[0])
t.Logf("Suggested renewal window for revoked certificate: %v\n", renewalInfo.SuggestedWindow)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if !renewalInfo.SuggestedWindow.Start.Before(time.Now()) {
t.Fatalf("suggested window start is in the past?")
}
if !renewalInfo.SuggestedWindow.End.Before(time.Now()) {
t.Fatalf("suggested window start is in the past?")
}
if renewalInfo.SuggestedWindow.End.Before(renewalInfo.SuggestedWindow.Start) {
t.Fatalf("suggested window end is before start?")
}
}

func TestClient_IssueReplacementCert(t *testing.T) {
if testClient.dir.RenewalInfo == "" {
t.Skip("acme server does not support ari renewals")
return
}

t.Log("Issuing initial order")
account, order, _ := makeOrderFinalised(t, nil)
if order.Certificate == "" {
t.Fatalf("no certificate: %+v", order)
}

// Replacing the original order should work
t.Log("Issuing first replacement order")
replacementOrder, err := makeReplacementOrderFinalized(t, order, account, nil, false)
if err != nil {
t.Fatal(err)
}

// Replacing the replacement should work
t.Log("Issuing second replacement order")
_, err = makeReplacementOrderFinalized(t, replacementOrder, account, nil, false)
if err != nil {
t.Fatal(err)
}

// Attempting to replace a previously replacement order should fail.
t.Log("Should not be able to create a duplicate replacement")
_, err = makeReplacementOrderFinalized(t, replacementOrder, account, nil, false)
if err == nil {
t.Fatal(err)
}
}

func TestClient_FailedReplacementOrderAllowsAnotherReplacement(t *testing.T) {
if testClient.dir.RenewalInfo == "" {
t.Skip("acme server does not support ari renewals")
return
}

t.Log("Issuing initial order")
account, order, _ := makeOrderFinalised(t, nil)
if order.Certificate == "" {
t.Fatalf("no certificate: %+v", order)
}

// Explicitly deactivate the previous authorization so the VA has to
// re-validate the order and encounter a failure. Upon receiving a
// validation failure, Pebble marks an order as invalid which is what we
// need.
auth, err := testClient.DeactivateAuthorization(account, order.Authorizations[0])
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if auth.Status != "deactivated" {
t.Fatalf("expected deactivated status, got: %s", auth.Status)
}

t.Log("Issuing replacement order which will intentionally fail")
_, err = makeReplacementOrderFinalized(t, order, account, nil, true)
if err == nil {
t.Fatal(err)
}

// Attempting to replace a previously failed replacement order should pass
t.Log("Issuing replacement order for a parent order who previously had a failed replacement order")
_, err = makeReplacementOrderFinalized(t, order, account, nil, false)
if err != nil {
t.Fatal(err)
}
}

func Test_generateCertID(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions order.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ func (c Client) ReplacementOrder(account Account, oldCert *x509.Certificate, ide
if err != nil {
return newOrderResp, err
}
defer resp.Body.Close()

newOrderResp.URL = resp.Header.Get("Location")
return newOrderResp, nil
}
Expand Down
Loading

0 comments on commit 79ababe

Please sign in to comment.