Boulder specific API for GETing "stale" ACME resources. (#4645)

This builds on the work @sh7dm started in #4600. I primarily did some
refactoring, added enforcement of the stale check for authorizations and
challenges, and completed the unit test coverage.

A new Boulder-specific (e.g. not specified by ACME / RFC 8555) API is added for
fetching order, authorization, challenge, and certificate resources by URL
without using POST-as-GET. Since we intend this API to only be used by humans
for debugging and we want to ensure ACME client devs use the standards compliant
method we restrict the GET API to only allowing access to "stale" resources
where the required staleness is defined by the WFE2 "staleTimeout"
configuration value (set to 5m in dev/CI).

Since authorizations don't have a creation date tracked we add
a `authorizationLifetimeDays` and `pendingAuthorizationLifetimeDays`
configuration parameter to the WFE2 that matches the RA's configuration. These
values are subtracted from the authorization expiry to find the creation date to
enforce the staleness check for authz/challenge GETs.

One other note: Resources accessed via the GET API will have Link relation URLs
pointing to the standard ACME API URL. E.g. a GET to a stale challenge will have
a response header with a link "up" relation URL pointing at the POST-as-GET URL
for the associated authorization. I wanted to avoid complicating
`prepAuthorizationForDisplay` and `prepChallengeForDisplay` to be aware of the
GET API and update or exclude the Link relations. This seems like a fine
trade-off since we don't expect machine consumption of the GET API results
(these are for human debugging).

Replaces #4600
Resolves #4577
This commit is contained in:
Daniel McCarney 2020-01-15 09:56:48 -05:00 committed by GitHub
parent 0e02b07aaf
commit 925540d7be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 387 additions and 22 deletions

View File

@ -10,6 +10,7 @@ import (
"io/ioutil"
"net/http"
"os"
"time"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
@ -83,6 +84,21 @@ type config struct {
// SHA256 hashes of SubjectPublicKeyInfo's that should be considered
// administratively blocked.
BlockedKeyFile string
// StaleTimeout determines how old should data be to be accessed via Boulder-specific GET-able APIs
StaleTimeout cmd.ConfigDuration
// AuthorizationLifetimeDays defines how long authorizations will be
// considered valid for. The WFE uses this to find the creation date of
// authorizations by subtracing this value from the expiry. It should match
// the value configured in the RA.
AuthorizationLifetimeDays int
// PendingAuthorizationLifetimeDays defines how long authorizations may be in
// the pending state before expiry. The WFE uses this to find the creation
// date of pending authorizations by subtracting this value from the expiry.
// It should match the value configured in the RA.
PendingAuthorizationLifetimeDays int
}
Syslog cmd.SyslogConfig
@ -259,7 +275,22 @@ func main() {
kp, err := goodkey.NewKeyPolicy("", c.WFE.BlockedKeyFile)
cmd.FailOnError(err, "Unable to create key policy")
rac, sac, rns, npm := setupWFE(c, logger, stats, clk)
wfe, err := wfe2.NewWebFrontEndImpl(stats, clk, kp, certChains, issuerCerts, rns, npm, logger)
if c.WFE.StaleTimeout.Duration == 0 {
c.WFE.StaleTimeout.Duration = time.Minute * 10
}
authorizationLifetime := 30 * (24 * time.Hour)
if c.WFE.AuthorizationLifetimeDays != 0 {
authorizationLifetime = time.Duration(c.WFE.AuthorizationLifetimeDays) * (24 * time.Hour)
}
pendingAuthorizationLifetime := 7 * (24 * time.Hour)
if c.WFE.PendingAuthorizationLifetimeDays != 0 {
pendingAuthorizationLifetime = time.Duration(c.WFE.PendingAuthorizationLifetimeDays) * (24 * time.Hour)
}
wfe, err := wfe2.NewWebFrontEndImpl(stats, clk, kp, certChains, issuerCerts, rns, npm, logger, c.WFE.StaleTimeout.Duration, authorizationLifetime, pendingAuthorizationLifetime)
cmd.FailOnError(err, "Unable to create WFE")
wfe.RA = rac
wfe.SA = sac

View File

@ -7,12 +7,15 @@ import (
"errors"
"fmt"
"io/ioutil"
"math/big"
"net"
"strings"
"testing"
"time"
"github.com/jmhodges/clock"
"gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/test"
jose "gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
@ -289,6 +292,7 @@ func (sa *StorageAuthority) GetCertificate(_ context.Context, serial string) (co
return core.Certificate{
RegistrationID: 1,
DER: certBlock.Bytes,
Issued: sa.clk.Now().Add(-1 * time.Hour),
}, nil
} else if serial == "0000000000000000000000000000000000b2" {
certPemBytes, _ := ioutil.ReadFile("test/178.crt")
@ -296,6 +300,14 @@ func (sa *StorageAuthority) GetCertificate(_ context.Context, serial string) (co
return core.Certificate{
RegistrationID: 1,
DER: certBlock.Bytes,
Issued: sa.clk.Now().Add(-1 * time.Hour),
}, nil
} else if serial == "0000000000000000000000000000000000b3" {
_, cert := test.ThrowAwayCertWithSerial(&testing.T{}, 1, big.NewInt(0xb3))
return core.Certificate{
RegistrationID: 1,
DER: cert.Raw,
Issued: sa.clk.Now(),
}, nil
} else {
return core.Certificate{}, errors.New("No cert")
@ -481,10 +493,12 @@ func (sa *StorageAuthority) GetOrder(_ context.Context, req *sapb.OrderRequest)
status := string(core.StatusValid)
one := int64(1)
serial := "serial"
created := sa.clk.Now().AddDate(-30, 0, 0).Unix()
exp := sa.clk.Now().AddDate(30, 0, 0).Unix()
validOrder := &corepb.Order{
Id: req.Id,
RegistrationID: &one,
Created: &created,
Expires: &exp,
Names: []string{"example.com"},
Status: &status,
@ -523,6 +537,12 @@ func (sa *StorageAuthority) GetOrder(_ context.Context, req *sapb.OrderRequest)
validOrder.Status = &ready
}
// Order 9 is fresh
if *req.Id == 9 {
now := sa.clk.Now().Unix()
validOrder.Created = &now
}
return validOrder, nil
}

View File

@ -42,6 +42,9 @@
"http://boulder:4430/acme/issuer-cert": [ "test/test-ca2.pem" ],
"http://127.0.0.1:4000/acme/issuer-cert": [ "test/test-ca2.pem" ]
},
"staleTimeout": "5m",
"authorizationLifetimeDays": 30,
"pendingAuthorizationLifetimeDays": 7,
"features": {
"HeadNonceStatusOK": true,
"RemoveWFE2AccountID": true,

View File

@ -17,7 +17,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_model/go"
io_prometheus_client "github.com/prometheus/client_model/go"
)
func fatalf(t *testing.T, format string, args ...interface{}) {
@ -217,12 +217,21 @@ func GaugeValueWithLabels(vecGauge *prometheus.GaugeVec, labels prometheus.Label
// The certificate returned from this function is the bare minimum needed for
// most tests and isn't a robust example of a complete end entity certificate.
func ThrowAwayCert(t *testing.T, nameCount int) (string, *x509.Certificate) {
k, err := rsa.GenerateKey(rand.Reader, 512)
AssertNotError(t, err, "rsa.GenerateKey failed")
var serialBytes [16]byte
_, _ = rand.Read(serialBytes[:])
serialNum := big.NewInt(0).SetBytes(serialBytes[:])
sn := big.NewInt(0).SetBytes(serialBytes[:])
return ThrowAwayCertWithSerial(t, nameCount, sn)
}
// ThrowAwayCertWithSerial is a small test helper function that creates a self-signed
// certificate for nameCount random example.com subdomains and returns the
// parsed certificate and the serial in string form or aborts the test.
// The certificate returned from this function is the bare minimum needed for
// most tests and isn't a robust example of a complete end entity certificate.
func ThrowAwayCertWithSerial(t *testing.T, nameCount int, sn *big.Int) (string, *x509.Certificate) {
k, err := rsa.GenerateKey(rand.Reader, 512)
AssertNotError(t, err, "rsa.GenerateKey failed")
var names []string
for i := 0; i < nameCount; i++ {
@ -232,12 +241,13 @@ func ThrowAwayCert(t *testing.T, nameCount int) (string, *x509.Certificate) {
}
template := &x509.Certificate{
SerialNumber: serialNum,
SerialNumber: sn,
DNSNames: names,
IssuingCertificateURL: []string{"http://localhost:4000/acme/issuer-cert"},
}
testCertDER, err := x509.CreateCertificate(rand.Reader, template, template, &k.PublicKey, k)
AssertNotError(t, err, "x509.CreateCertificate failed")
testCert, err := x509.ParseCertificate(testCertDER)
AssertNotError(t, err, "failed to parse self-signed cert DER")
return fmt.Sprintf("%036x", serialNum), testCert
return fmt.Sprintf("%036x", sn), testCert
}

66
wfe2/stale.go Normal file
View File

@ -0,0 +1,66 @@
package wfe2
import (
"net/http"
"strings"
"time"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/web"
)
// requiredStale checks if a request is a GET request with a logEvent indicating
// the endpoint starts with getAPIPrefix. If true then the caller is expected to
// apply staleness requirements via staleEnoughToGETOrder, staleEnoughToGETCert
// and staleEnoughToGETAuthz.
func requiredStale(req *http.Request, logEvent *web.RequestEvent) bool {
return req.Method == http.MethodGet && strings.HasPrefix(logEvent.Endpoint, getAPIPrefix)
}
// staleEnoughToGETOrder checks if the given order was created long enough ago
// in the past to be acceptably stale for accessing via the Boulder specific GET
// API.
func (wfe *WebFrontEndImpl) staleEnoughToGETOrder(order *corepb.Order) *probs.ProblemDetails {
return wfe.staleEnoughToGET("Order", time.Unix(*order.Created, 0))
}
// staleEnoughToGETCert checks if the given cert was issued long enough in the
// past to be acceptably stale for accessing via the Boulder specific GET API.
func (wfe *WebFrontEndImpl) staleEnoughToGETCert(cert core.Certificate) *probs.ProblemDetails {
return wfe.staleEnoughToGET("Certificate", cert.Issued)
}
// staleEnoughToGETAuthz checks if the given authorization was created long
// enough ago in the past to be acceptably stale for accessing via the Boulder
// specific GET API. Since authorization creation date is not tracked directly
// the appropriate lifetime for the authz is subtracted from the expiry to find
// the creation date.
func (wfe *WebFrontEndImpl) staleEnoughToGETAuthz(authz core.Authorization) *probs.ProblemDetails {
// We don't directly track authorization creation time. Instead subtract the
// pendingAuthorization lifetime from the expiry. This will be inaccurate if
// we change the pendingAuthorizationLifetime but is sufficient for the weak
// staleness requirements of the GET API.
createdTime := authz.Expires.Add(-wfe.pendingAuthorizationLifetime)
// if the authz is valid then we need to subtract the authorizationLifetime
// instead of the pendingAuthorizationLifetime.
if authz.Status == core.StatusValid {
createdTime = authz.Expires.Add(-wfe.authorizationLifetime)
}
return wfe.staleEnoughToGET("Authorization", createdTime)
}
// staleEnoughToGET checks that the createDate for the given resource is at
// least wfe.staleTimeout in the past. If the resource is newer than the
// wfe.staleTimeout then an unauthorized problem is returned.
func (wfe *WebFrontEndImpl) staleEnoughToGET(resourceType string, createDate time.Time) *probs.ProblemDetails {
if wfe.clk.Since(createDate) < wfe.staleTimeout {
return probs.Unauthorized(
"%s is too new for GET API. "+
"You should only use this non-standard API to access resources created more than %s ago",
resourceType,
wfe.staleTimeout)
}
return nil
}

43
wfe2/stale_test.go Normal file
View File

@ -0,0 +1,43 @@
package wfe2
import (
"net/http"
"testing"
"github.com/letsencrypt/boulder/test"
"github.com/letsencrypt/boulder/web"
)
func TestRequiredStale(t *testing.T) {
testCases := []struct {
name string
req *http.Request
logEvent *web.RequestEvent
expectRequired bool
}{
{
name: "not GET",
req: &http.Request{Method: http.MethodPost},
logEvent: &web.RequestEvent{},
expectRequired: false,
},
{
name: "GET, not getAPIPrefix",
req: &http.Request{Method: http.MethodGet},
logEvent: &web.RequestEvent{},
expectRequired: false,
},
{
name: "GET, getAPIPrefix",
req: &http.Request{Method: http.MethodGet},
logEvent: &web.RequestEvent{Endpoint: getAPIPrefix + "whatever"},
expectRequired: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
test.AssertEquals(t, requiredStale(tc.req, tc.logEvent), tc.expectRequired)
})
}
}

View File

@ -60,6 +60,12 @@ const (
newOrderPath = "/acme/new-order"
orderPath = "/acme/order/"
finalizeOrderPath = "/acme/finalize/"
getAPIPrefix = "/get/"
getOrderPath = getAPIPrefix + "order/"
getAuthzv2Path = getAPIPrefix + "authz-v3/"
getChallengev2Path = getAPIPrefix + "chall-v3/"
getCertPath = getAPIPrefix + "cert/"
)
// WebFrontEndImpl provides all the logic for Boulder's web-facing interface,
@ -116,6 +122,19 @@ type WebFrontEndImpl struct {
// Maximum duration of a request
RequestTimeout time.Duration
// StaleTimeout determines the required staleness for resources allowed to be
// accessed via Boulder-specific GET-able APIs. Resources newer than
// staleTimeout must be accessed via POST-as-GET and the RFC 8555 ACME API. We
// do this to incentivize client developers to use the standard API.
staleTimeout time.Duration
// How long before authorizations and pending authorizations expire. The
// Boulder specific GET-able API uses these values to find the creation date
// of authorizations to determine if they are stale enough. The values should
// match the ones used by the RA.
authorizationLifetime time.Duration
pendingAuthorizationLifetime time.Duration
}
// NewWebFrontEndImpl constructs a web service for Boulder
@ -128,6 +147,9 @@ func NewWebFrontEndImpl(
remoteNonceService noncepb.NonceServiceClient,
noncePrefixMap map[string]noncepb.NonceServiceClient,
logger blog.Logger,
staleTimeout time.Duration,
authorizationLifetime time.Duration,
pendingAuthorizationLifetime time.Duration,
) (WebFrontEndImpl, error) {
wfe := WebFrontEndImpl{
log: logger,
@ -138,6 +160,9 @@ func NewWebFrontEndImpl(
stats: initStats(stats),
remoteNonceService: remoteNonceService,
noncePrefixMap: noncePrefixMap,
staleTimeout: staleTimeout,
authorizationLifetime: authorizationLifetime,
pendingAuthorizationLifetime: pendingAuthorizationLifetime,
}
if wfe.remoteNonceService == nil {
@ -350,12 +375,17 @@ func (wfe *WebFrontEndImpl) Handler(stats prometheus.Registerer) http.Handler {
wfe.HandleFunc(m, directoryPath, wfe.Directory, "GET", "POST")
wfe.HandleFunc(m, newNoncePath, wfe.Nonce, "GET", "POST")
// POST-as-GETable ACME endpoints
// TODO(@cpu): After November 1st, 2019 support for "GET" to the following
// TODO(@cpu): After November 1st, 2020 support for "GET" to the following
// endpoints will be removed, leaving only POST-as-GET support.
wfe.HandleFunc(m, orderPath, wfe.GetOrder, "GET", "POST")
wfe.HandleFunc(m, authzv2Path, wfe.Authorization, "GET", "POST")
wfe.HandleFunc(m, challengev2Path, wfe.Challenge, "GET", "POST")
wfe.HandleFunc(m, certPath, wfe.Certificate, "GET", "POST")
// Boulder-specific GET-able resource endpoints
wfe.HandleFunc(m, getOrderPath, wfe.GetOrder, "GET")
wfe.HandleFunc(m, getAuthzv2Path, wfe.Authorization, "GET")
wfe.HandleFunc(m, getChallengev2Path, wfe.Challenge, "GET")
wfe.HandleFunc(m, getCertPath, wfe.Certificate, "GET")
// We don't use our special HandleFunc for "/" because it matches everything,
// meaning we can wind up returning 405 when we mean to return 404. See
@ -1032,6 +1062,13 @@ func (wfe *WebFrontEndImpl) Challenge(
return
}
if requiredStale(request, logEvent) {
if prob := wfe.staleEnoughToGETAuthz(authz); prob != nil {
wfe.sendError(response, logEvent, prob, nil)
return
}
}
if authz.Identifier.Type == identifier.DNS {
logEvent.DNSName = authz.Identifier.Value
}
@ -1453,6 +1490,13 @@ func (wfe *WebFrontEndImpl) Authorization(
return
}
if requiredStale(request, logEvent) {
if prob := wfe.staleEnoughToGETAuthz(authz); prob != nil {
wfe.sendError(response, logEvent, prob, nil)
return
}
}
// If this was a POST that has an associated requestAccount and that account
// doesn't own the authorization, abort before trying to deactivate the authz
// or return its details
@ -1531,6 +1575,13 @@ func (wfe *WebFrontEndImpl) Certificate(ctx context.Context, logEvent *web.Reque
return
}
if requiredStale(request, logEvent) {
if prob := wfe.staleEnoughToGETCert(cert); prob != nil {
wfe.sendError(response, logEvent, prob, nil)
return
}
}
// If there was a requesterAccount (e.g. because it was a POST-as-GET request)
// then the requesting account must be the owner of the certificate, otherwise
// return an unauthorized error.
@ -1957,6 +2008,13 @@ func (wfe *WebFrontEndImpl) GetOrder(ctx context.Context, logEvent *web.RequestE
return
}
if requiredStale(request, logEvent) {
if prob := wfe.staleEnoughToGETOrder(order); prob != nil {
wfe.sendError(response, logEvent, prob, nil)
return
}
}
if *order.RegistrationID != acctID {
wfe.sendError(response, logEvent, probs.NotFound("No order found for account ID %d", acctID), nil)
return

View File

@ -26,7 +26,7 @@ import (
"time"
"github.com/jmhodges/clock"
"gopkg.in/square/go-jose.v2"
jose "gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
@ -368,7 +368,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock) {
issuerCert,
}
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, certChains, issuerCertificates, nil, nil, blog.NewMock())
wfe, err := NewWebFrontEndImpl(stats, fc, testKeyPolicy, certChains, issuerCertificates, nil, nil, blog.NewMock(), 10*time.Second, 30*24*time.Hour, 7*24*time.Hour)
test.AssertNotError(t, err, "Unable to create WFE")
wfe.SubscriberAgreementURL = agreementURL
@ -1810,6 +1810,8 @@ func TestGetCertificate(t *testing.T) {
test.AssertNotError(t, err, "Error reading ../test/test-ca2.pem")
noCache := "public, max-age=0, no-cache"
newSerial := "/acme/cert/0000000000000000000000000000000000b3"
newGetSerial := "/get/cert/0000000000000000000000000000000000b3"
goodSerial := "/acme/cert/0000000000000000000000000000000000b2"
notFound := `{"type":"` + probs.V2ErrorNS + `malformed","detail":"Certificate not found","status":404}`
@ -1820,6 +1822,7 @@ func TestGetCertificate(t *testing.T) {
ExpectedHeaders map[string]string
ExpectedBody string
ExpectedCert []byte
AnyCert bool
}{
{
Name: "Valid serial",
@ -1877,6 +1880,34 @@ func TestGetCertificate(t *testing.T) {
ExpectedStatus: http.StatusNotFound,
ExpectedBody: notFound,
},
{
Name: "New cert",
Request: makeGet(newGetSerial),
ExpectedStatus: http.StatusForbidden,
ExpectedBody: `{
"type": "` + probs.V2ErrorNS + `unauthorized",
"detail": "Certificate is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago",
"status": 403
}`,
},
{
Name: "New cert, old endpoint",
Request: makeGet(newSerial),
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"Content-Type": pkixContent,
},
AnyCert: true,
},
{
Name: "New cert, POST-as-GET",
Request: makePost(1, nil, newSerial, ""),
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"Content-Type": pkixContent,
},
AnyCert: true,
},
}
for _, tc := range testCases {
@ -1900,6 +1931,10 @@ func TestGetCertificate(t *testing.T) {
test.AssertEquals(t, headers.Get(h), v)
}
if tc.AnyCert { // Certificate is randomly generated, don't match it
return
}
if len(tc.ExpectedCert) > 0 {
// If the expectation was to return a certificate, check that it was the one expected
bodyBytes := responseWriter.Body.Bytes()
@ -2479,6 +2514,7 @@ func TestGetOrder(t *testing.T) {
Name string
Request *http.Request
Response string
Endpoint string
}{
{
Name: "Good request",
@ -2530,12 +2566,32 @@ func TestGetOrder(t *testing.T) {
Request: makePost(1, "1/1", ""),
Response: `{"status": "valid","expires": "1970-01-01T00:00:00.9466848Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/1","certificate":"http://localhost/acme/cert/serial"}`,
},
{
Name: "GET new order",
Request: makeGet("1/9"),
Response: `{"type":"` + probs.V2ErrorNS + `unauthorized","detail":"Order is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago","status":403}`,
Endpoint: "/get/order/",
},
{
Name: "GET new order from old endpoint",
Request: makeGet("1/9"),
Response: `{"status": "valid","expires": "1970-01-01T00:00:00.9466848Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`,
},
{
Name: "POST-as-GET new order",
Request: makePost(1, "1/9", ""),
Response: `{"status": "valid","expires": "1970-01-01T00:00:00.9466848Z","identifiers":[{"type":"dns", "value":"example.com"}], "authorizations":["http://localhost/acme/authz-v3/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
responseWriter := httptest.NewRecorder()
if tc.Endpoint != "" {
wfe.GetOrder(ctx, &web.RequestEvent{Extra: make(map[string]interface{}), Endpoint: tc.Endpoint}, responseWriter, tc.Request)
} else {
wfe.GetOrder(ctx, newRequestEvent(), responseWriter, tc.Request)
}
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.Response)
})
}
@ -3142,3 +3198,81 @@ func TestPrepAccountForDisplay(t *testing.T) {
// The ID field should now be zeroed
test.AssertEquals(t, acct.ID, int64(0))
}
func TestGETAPIAuthz(t *testing.T) {
wfe, _ := setupWFE(t)
makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) {
return &http.Request{URL: &url.URL{Path: path}, Method: "GET"},
&web.RequestEvent{Endpoint: endpoint}
}
testCases := []struct {
name string
path string
expectTooFreshErr bool
}{
{
name: "fresh authz",
path: "1",
expectTooFreshErr: true,
},
{
name: "old authz",
path: "2",
expectTooFreshErr: false,
},
}
tooFreshErr := `{"type":"` + probs.V2ErrorNS + `unauthorized","detail":"Authorization is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago","status":403}`
for _, tc := range testCases {
responseWriter := httptest.NewRecorder()
req, logEvent := makeGet(tc.path, getAuthzv2Path)
wfe.Authorization(context.Background(), logEvent, responseWriter, req)
if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr {
t.Errorf("expected too fresh error, got http.StatusOK")
} else {
test.AssertEquals(t, responseWriter.Code, http.StatusForbidden)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tooFreshErr)
}
}
}
func TestGETAPIChallenge(t *testing.T) {
wfe, _ := setupWFE(t)
makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) {
return &http.Request{URL: &url.URL{Path: path}, Method: "GET"},
&web.RequestEvent{Endpoint: endpoint}
}
testCases := []struct {
name string
path string
expectTooFreshErr bool
}{
{
name: "fresh authz challenge",
path: "1/-ZfxEw",
expectTooFreshErr: true,
},
{
name: "old authz challenge",
path: "2/-ZfxEw",
expectTooFreshErr: false,
},
}
tooFreshErr := `{"type":"` + probs.V2ErrorNS + `unauthorized","detail":"Authorization is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago","status":403}`
for _, tc := range testCases {
responseWriter := httptest.NewRecorder()
req, logEvent := makeGet(tc.path, getAuthzv2Path)
wfe.Challenge(context.Background(), logEvent, responseWriter, req)
if responseWriter.Code == http.StatusOK && tc.expectTooFreshErr {
t.Errorf("expected too fresh error, got http.StatusOK")
} else {
test.AssertEquals(t, responseWriter.Code, http.StatusForbidden)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tooFreshErr)
}
}
}