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:
parent
0e02b07aaf
commit
925540d7be
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
60
wfe2/wfe.go
60
wfe2/wfe.go
|
|
@ -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
|
||||
|
|
|
|||
138
wfe2/wfe_test.go
138
wfe2/wfe_test.go
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue