Duplicate WFE to WFE2. (#2839)

This PR is the initial duplication of the WFE to create a WFE2
package. The rationale is briefly explained in `wfe2/README.md`.

Per #2822 this PR only lays the groundwork for further customization
and deduplication. Presently both the WFE and WFE2 are identical except
for the following configuration differences:

* The WFE offers HTTP and HTTPS on 4000 and 4430 respectively, the WFE2
  offers HTTP on 4001 and 4431.
* The WFE has a debug port on 8000, the WFE2 uses the next free "8000
  range port" and puts its debug service on 8013

Resolves https://github.com/letsencrypt/boulder/issues/2822
This commit is contained in:
Daniel McCarney 2017-07-05 16:32:45 -04:00 committed by Jacob Hoffman-Andrews
parent 7120d72197
commit bd3e2747ba
21 changed files with 4754 additions and 3 deletions

View File

@ -2,8 +2,8 @@
# be used as the base of the bhsm container in boulder/docker-compose.yml
FROM letsencrypt/boulder-tools:2017-05-25
# Boulder exposes its web application at port TCP 4000
EXPOSE 4000 4002 4003 4430 8053 8055
# Boulder exposes its web application at port TCP 4000 and 4001
EXPOSE 4000 4001 4002 4003 4430 4431 8053 8055
ENV PATH /usr/local/go/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin/
ENV GOPATH /go

181
cmd/boulder-wfe2/main.go Normal file
View File

@ -0,0 +1,181 @@
package main
import (
"crypto/tls"
"flag"
"fmt"
"net/http"
"os"
"github.com/facebookgo/httpdown"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey"
bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
rapb "github.com/letsencrypt/boulder/ra/proto"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/wfe2"
)
const clientName = "WFE2"
type config struct {
WFE struct {
cmd.ServiceConfig
BaseURL string
ListenAddress string
TLSListenAddress string
ServerCertificatePath string
ServerKeyPath string
AllowOrigins []string
CertCacheDuration cmd.ConfigDuration
CertNoCacheExpirationWindow cmd.ConfigDuration
IndexCacheDuration cmd.ConfigDuration
IssuerCacheDuration cmd.ConfigDuration
ShutdownStopTimeout cmd.ConfigDuration
ShutdownKillTimeout cmd.ConfigDuration
SubscriberAgreementURL string
AcceptRevocationReason bool
AllowAuthzDeactivation bool
TLS cmd.TLSConfig
RAService *cmd.GRPCClientConfig
SAService *cmd.GRPCClientConfig
Features map[string]bool
}
SubscriberAgreementURL string
Syslog cmd.SyslogConfig
Common struct {
BaseURL string
IssuerCert string
}
}
func setupWFE(c config, logger blog.Logger, stats metrics.Scope) (core.RegistrationAuthority, core.StorageAuthority) {
var tls *tls.Config
var err error
if c.WFE.TLS.CertFile != nil {
tls, err = c.WFE.TLS.Load()
cmd.FailOnError(err, "TLS config")
}
raConn, err := bgrpc.ClientSetup(c.WFE.RAService, tls, stats)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
rac := bgrpc.NewRegistrationAuthorityClient(rapb.NewRegistrationAuthorityClient(raConn))
saConn, err := bgrpc.ClientSetup(c.WFE.SAService, tls, stats)
cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
sac := bgrpc.NewStorageAuthorityClient(sapb.NewStorageAuthorityClient(saConn))
return rac, sac
}
func main() {
configFile := flag.String("config", "", "File path to the configuration file for this service")
flag.Parse()
if *configFile == "" {
flag.Usage()
os.Exit(1)
}
var c config
err := cmd.ReadConfigFile(*configFile, &c)
cmd.FailOnError(err, "Reading JSON config file into config structure")
err = features.Set(c.WFE.Features)
cmd.FailOnError(err, "Failed to set feature flags")
scope, logger := cmd.StatsAndLogging(c.Syslog)
defer logger.AuditPanic()
logger.Info(cmd.VersionString(clientName))
kp, err := goodkey.NewKeyPolicy("") // don't load any weak keys
cmd.FailOnError(err, "Unable to create key policy")
wfe, err := wfe2.NewWebFrontEndImpl(scope, clock.Default(), kp, logger)
cmd.FailOnError(err, "Unable to create WFE")
rac, sac := setupWFE(c, logger, scope)
wfe.RA = rac
wfe.SA = sac
// TODO: remove this check once the production config uses the SubscriberAgreementURL in the wfe section
if c.WFE.SubscriberAgreementURL != "" {
wfe.SubscriberAgreementURL = c.WFE.SubscriberAgreementURL
} else {
wfe.SubscriberAgreementURL = c.SubscriberAgreementURL
}
wfe.AllowOrigins = c.WFE.AllowOrigins
wfe.AcceptRevocationReason = c.WFE.AcceptRevocationReason
wfe.AllowAuthzDeactivation = c.WFE.AllowAuthzDeactivation
wfe.CertCacheDuration = c.WFE.CertCacheDuration.Duration
wfe.CertNoCacheExpirationWindow = c.WFE.CertNoCacheExpirationWindow.Duration
wfe.IndexCacheDuration = c.WFE.IndexCacheDuration.Duration
wfe.IssuerCacheDuration = c.WFE.IssuerCacheDuration.Duration
wfe.IssuerCert, err = cmd.LoadCert(c.Common.IssuerCert)
cmd.FailOnError(err, fmt.Sprintf("Couldn't read issuer cert [%s]", c.Common.IssuerCert))
logger.Info(fmt.Sprintf("WFE using key policy: %#v", kp))
// Set up paths
wfe.BaseURL = c.Common.BaseURL
logger.Info(fmt.Sprintf("Server running, listening on %s...\n", c.WFE.ListenAddress))
srv := &http.Server{
Addr: c.WFE.ListenAddress,
Handler: wfe.Handler(),
}
go cmd.DebugServer(c.WFE.DebugAddr)
go cmd.ProfileCmd(scope)
hd := &httpdown.HTTP{
StopTimeout: c.WFE.ShutdownStopTimeout.Duration,
KillTimeout: c.WFE.ShutdownKillTimeout.Duration,
}
hdSrv, err := hd.ListenAndServe(srv)
cmd.FailOnError(err, "Error starting HTTP server")
var hdTLSSrv httpdown.Server
if c.WFE.TLSListenAddress != "" {
cer, err := tls.LoadX509KeyPair(c.WFE.ServerCertificatePath, c.WFE.ServerKeyPath)
cmd.FailOnError(err, "Couldn't read WFE server certificate or key")
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cer}}
logger.Info(fmt.Sprintf("TLS Server running, listening on %s...\n", c.WFE.TLSListenAddress))
TLSSrv := &http.Server{
Addr: c.WFE.TLSListenAddress,
Handler: wfe.Handler(),
TLSConfig: tlsConfig,
}
hdTLSSrv, err = hd.ListenAndServe(TLSSrv)
cmd.FailOnError(err, "Error starting TLS server")
}
go cmd.CatchSignals(logger, func() {
_ = hdSrv.Stop()
if hdTLSSrv != nil {
_ = hdTLSSrv.Stop()
}
})
forever := make(chan struct{}, 1)
<-forever
}

View File

@ -0,0 +1 @@
package main

View File

@ -17,9 +17,11 @@ services:
- boulder:127.0.0.1
ports:
- 4000:4000 # ACME
- 4001:4001 # ACMEv2
- 4002:4002 # OCSP
- 4003:4003 # OCSP
- 4430:4430 # ACME via HTTPS
- 4431:4431 # ACMEv2 via HTTPS
- 4500:4500 # ct-test-srv
- 6000:6000 # gsb-test-srv
- 8000:8000 # debug ports

View File

@ -0,0 +1,50 @@
{
"wfe": {
"listenAddress": "0.0.0.0:4001",
"TLSListenAddress": "0.0.0.0:4431",
"serverCertificatePath": "test/wfe.pem",
"serverKeyPath": "test/wfe.key",
"requestTimeout": "10s",
"allowOrigins": ["*"],
"certCacheDuration": "6h",
"certNoCacheExpirationWindow": "96h",
"indexCacheDuration": "24h",
"issuerCacheDuration": "48h",
"shutdownStopTimeout": "10s",
"shutdownKillTimeout": "1m",
"subscriberAgreementURL": "http://boulder:4001/terms/v1",
"acceptRevocationReason": true,
"allowAuthzDeactivation": true,
"debugAddr": ":8013",
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",
"keyFile": "test/grpc-creds/wfe.boulder/key.pem"
},
"raService": {
"serverAddresses": ["ra.boulder:19094"],
"timeout": "15s"
},
"saService": {
"serverAddresses": ["sa.boulder:19095"],
"timeout": "15s"
},
"features": {
"AllowAccountDeactivation": true,
"AllowKeyRollover": true,
"UseAIAIssuerURL": true,
"RandomDirectoryEntry": true,
"DirectoryMeta": true
}
},
"syslog": {
"stdoutlevel": 6,
"sysloglevel": 4
},
"common": {
"issuerCert": "test/test-ca.pem",
"dnsResolver": "127.0.0.1:8053"
}
}

54
test/config/wfe2.json Normal file
View File

@ -0,0 +1,54 @@
{
"wfe": {
"listenAddress": "0.0.0.0:4001",
"TLSListenAddress": "0.0.0.0:4431",
"serverCertificatePath": "test/wfe.pem",
"serverKeyPath": "test/wfe.key",
"requestTimeout": "10s",
"allowOrigins": ["*"],
"certCacheDuration": "6h",
"certNoCacheExpirationWindow": "96h",
"indexCacheDuration": "24h",
"issuerCacheDuration": "48h",
"shutdownStopTimeout": "10s",
"shutdownKillTimeout": "1m",
"subscriberAgreementURL": "http://boulder:4001/terms/v1",
"checkMalformedCSR": true,
"allowAuthzDeactivation": true,
"debugAddr": ":8013",
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",
"keyFile": "test/grpc-creds/wfe.boulder/key.pem"
},
"raService": {
"serverAddresses": ["ra.boulder:19094"],
"timeout": "15s"
},
"saService": {
"serverAddresses": ["sa.boulder:19095"],
"timeout": "15s"
}
},
"allowedSigningAlgos": {
"rsa": true,
"ecdsanistp256": true,
"ecdsanistp384": true,
"ecdsanistp521": false
},
"statsd": {
"server": "localhost:8125",
"prefix": "Boulder"
},
"subscriberAgreementURL": "http://boulder:4001/terms/v1",
"syslog": {
"stdoutlevel": 6
},
"common": {
"issuerCert": "test/test-ca.pem"
}
}

View File

@ -65,9 +65,11 @@ fi
docker run --rm -it \
"${fake_dns_args[@]}" \
-p 4000:4000 \
-p 4001:4001 \
-p 4002:4002 \
-p 4003:4003 \
-p 4430:4430 \
-p 4431:4431 \
-p 8053:8053 \
-p 8055:8055 \
--name boulder \

View File

@ -64,6 +64,7 @@ def start(race_detection):
'gsb-test-srv -apikey my-voice-is-my-passport',
'boulder-sa --config %s' % os.path.join(default_config_dir, "sa.json"),
'boulder-wfe --config %s' % os.path.join(default_config_dir, "wfe.json"),
'boulder-wfe2 --config %s' % os.path.join(default_config_dir, "wfe2.json"),
'boulder-ra --config %s' % os.path.join(default_config_dir, "ra.json"),
'boulder-ca --config %s' % os.path.join(default_config_dir, "ca.json"),
'boulder-va --config %s' % os.path.join(default_config_dir, "va.json"),
@ -100,10 +101,12 @@ def start(race_detection):
# If one of the servers has died, quit immediately.
if not check():
return False
ports = range(8000, 8005) + [4000, 4430]
ports = range(8000, 8005) + [4000, 4001, 4430, 4431]
if default_config_dir.startswith("test/config-next"):
# Add the two 'remote' VA debug ports
ports.extend([8011, 8012])
# Add the wfe v2 debug port
ports.extend([8013])
for debug_port in ports:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', debug_port))

7
wfe2/README.md Normal file
View File

@ -0,0 +1,7 @@
WFE v2
============
The `wfe2` package is copied from the `wfe` package in order to implement the
["ACME v2"](https://letsencrypt.org/2017/06/14/acme-v2-api.html) API. This design choice
was made to facilitate a clean separation between v1 and v2 code and to support
running a separate API process on a different port alongside the v1 API process.

90
wfe2/context.go Normal file
View File

@ -0,0 +1,90 @@
package wfe2
import (
"encoding/json"
"fmt"
"net/http"
"golang.org/x/net/context"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
)
type requestEvent struct {
ID string `json:",omitempty"`
RealIP string `json:",omitempty"`
Endpoint string `json:",omitempty"`
Method string `json:",omitempty"`
Errors []string `json:",omitempty"`
Requester int64 `json:",omitempty"`
Contacts *[]string `json:",omitempty"`
RequestNonce string `json:",omitempty"`
ResponseNonce string `json:",omitempty"`
UserAgent string `json:",omitempty"`
Code int
Payload string `json:",omitempty"`
Extra map[string]interface{} `json:",omitempty"`
}
func (e *requestEvent) AddError(msg string, args ...interface{}) {
e.Errors = append(e.Errors, fmt.Sprintf(msg, args...))
}
type wfeHandlerFunc func(context.Context, *requestEvent, http.ResponseWriter, *http.Request)
func (f wfeHandlerFunc) ServeHTTP(e *requestEvent, w http.ResponseWriter, r *http.Request) {
ctx := context.TODO()
f(ctx, e, w, r)
}
type wfeHandler interface {
ServeHTTP(e *requestEvent, w http.ResponseWriter, r *http.Request)
}
type topHandler struct {
wfe wfeHandler
log blog.Logger
clk clock.Clock
}
func (th *topHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logEvent := &requestEvent{
ID: core.NewToken(),
RealIP: r.Header.Get("X-Real-IP"),
Method: r.Method,
UserAgent: r.Header.Get("User-Agent"),
Extra: make(map[string]interface{}, 0),
}
w.Header().Set("Boulder-Request-ID", logEvent.ID)
defer th.logEvent(logEvent)
th.wfe.ServeHTTP(logEvent, w, r)
}
func (th *topHandler) logEvent(logEvent *requestEvent) {
var msg string
if len(logEvent.Errors) != 0 {
msg = "Terminated request"
} else {
msg = "Successful request"
}
jsonEvent, err := json.Marshal(logEvent)
if err != nil {
th.log.AuditErr(fmt.Sprintf("%s - failed to marshal logEvent - %s", msg, err))
return
}
th.log.Info(fmt.Sprintf("%s JSON=%s", msg, jsonEvent))
}
// Comma-separated list of HTTP clients involved in making this
// request, starting with the original requestor and ending with the
// remote end of our TCP connection (which is typically our own
// proxy).
func getClientAddr(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return xff + "," + r.RemoteAddr
}
return r.RemoteAddr
}

58
wfe2/jose.go Normal file
View File

@ -0,0 +1,58 @@
package wfe2
import (
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"gopkg.in/square/go-jose.v1"
)
func algorithmForKey(key *jose.JsonWebKey) (string, error) {
switch k := key.Key.(type) {
case *rsa.PublicKey:
return string(jose.RS256), nil
case *ecdsa.PublicKey:
switch k.Params().Name {
case "P-256":
return string(jose.ES256), nil
case "P-384":
return string(jose.ES384), nil
case "P-521":
return string(jose.ES512), nil
}
}
return "", signatureValidationError("no signature algorithms suitable for given key type")
}
const (
noAlgorithmForKey = "WFE.Errors.NoAlgorithmForKey"
invalidJWSAlgorithm = "WFE.Errors.InvalidJWSAlgorithm"
invalidAlgorithmOnKey = "WFE.Errors.InvalidAlgorithmOnKey"
)
// Check that (1) there is a suitable algorithm for the provided key based on its
// Golang type, (2) the Algorithm field on the JWK is either absent, or matches
// that algorithm, and (3) the Algorithm field on the JWK is present and matches
// that algorithm. Precondition: parsedJws must have exactly one signature on
// it. Returns stat name to increment if err is non-nil.
func checkAlgorithm(key *jose.JsonWebKey, parsedJws *jose.JsonWebSignature) (string, error) {
algorithm, err := algorithmForKey(key)
if err != nil {
return noAlgorithmForKey, err
}
jwsAlgorithm := parsedJws.Signatures[0].Header.Algorithm
if jwsAlgorithm != algorithm {
return invalidJWSAlgorithm, signatureValidationError(fmt.Sprintf(
"signature type '%s' in JWS header is not supported, expected one of RS256, ES256, ES384 or ES512",
jwsAlgorithm,
))
}
if key.Algorithm != "" && key.Algorithm != algorithm {
return invalidAlgorithmOnKey, signatureValidationError(fmt.Sprintf(
"algorithm '%s' on JWK is unacceptable",
key.Algorithm,
))
}
return "", nil
}

203
wfe2/jose_test.go Normal file
View File

@ -0,0 +1,203 @@
package wfe2
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"testing"
"gopkg.in/square/go-jose.v1"
)
func TestRejectsNone(t *testing.T) {
wfe, _ := setupWFE(t)
_, _, _, prob := wfe.verifyPOST(ctx, newRequestEvent(), makePostRequest(`
{
"header": {
"alg": "none",
"jwk": {
"kty": "RSA",
"n": "vrjT",
"e": "AQAB"
}
},
"payload": "aGkK",
"signature": ""
}
`), true, "foo")
if prob == nil {
t.Fatalf("verifyPOST did not reject JWS with alg: 'none'")
}
if prob.Detail != "signature type 'none' in JWS header is not supported, expected one of RS256, ES256, ES384 or ES512" {
t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: %#v", prob)
}
}
func TestRejectsHS256(t *testing.T) {
wfe, _ := setupWFE(t)
_, _, _, prob := wfe.verifyPOST(ctx, newRequestEvent(), makePostRequest(`
{
"header": {
"alg": "HS256",
"jwk": {
"kty": "RSA",
"n": "vrjT",
"e": "AQAB"
}
},
"payload": "aGkK",
"signature": ""
}
`), true, "foo")
if prob == nil {
t.Fatalf("verifyPOST did not reject JWS with alg: 'HS256'")
}
expected := "signature type 'HS256' in JWS header is not supported, expected one of RS256, ES256, ES384 or ES512"
if prob.Detail != expected {
t.Fatalf("verifyPOST rejected JWS with alg: 'none', but for wrong reason: got '%s', wanted %s", prob, expected)
}
}
func TestCheckAlgorithm(t *testing.T) {
testCases := []struct {
key jose.JsonWebKey
jws jose.JsonWebSignature
expectedErr string
expectedStat string
}{
{
jose.JsonWebKey{
Algorithm: "HS256",
},
jose.JsonWebSignature{},
"no signature algorithms suitable for given key type",
"WFE.Errors.NoAlgorithmForKey",
},
{
jose.JsonWebKey{
Key: &rsa.PublicKey{},
},
jose.JsonWebSignature{
Signatures: []jose.Signature{
{
Header: jose.JoseHeader{
Algorithm: "HS256",
},
},
},
},
"signature type 'HS256' in JWS header is not supported, expected one of RS256, ES256, ES384 or ES512",
"WFE.Errors.InvalidJWSAlgorithm",
},
{
jose.JsonWebKey{
Algorithm: "HS256",
Key: &rsa.PublicKey{},
},
jose.JsonWebSignature{
Signatures: []jose.Signature{
{
Header: jose.JoseHeader{
Algorithm: "HS256",
},
},
},
},
"signature type 'HS256' in JWS header is not supported, expected one of RS256, ES256, ES384 or ES512",
"WFE.Errors.InvalidJWSAlgorithm",
},
{
jose.JsonWebKey{
Algorithm: "HS256",
Key: &rsa.PublicKey{},
},
jose.JsonWebSignature{
Signatures: []jose.Signature{
{
Header: jose.JoseHeader{
Algorithm: "RS256",
},
},
},
},
"algorithm 'HS256' on JWK is unacceptable",
"WFE.Errors.InvalidAlgorithmOnKey",
},
}
for i, tc := range testCases {
stat, err := checkAlgorithm(&tc.key, &tc.jws)
if tc.expectedErr != "" && err.Error() != tc.expectedErr {
t.Errorf("TestCheckAlgorithm %d: Expected '%s', got '%s'", i, tc.expectedErr, err)
}
if tc.expectedStat != "" && stat != tc.expectedStat {
t.Errorf("TestCheckAlgorithm %d: Expected stat '%s', got '%s'", i, tc.expectedStat, stat)
}
}
}
func TestCheckAlgorithmSuccess(t *testing.T) {
_, err := checkAlgorithm(&jose.JsonWebKey{
Algorithm: "RS256",
Key: &rsa.PublicKey{},
}, &jose.JsonWebSignature{
Signatures: []jose.Signature{
{
Header: jose.JoseHeader{
Algorithm: "RS256",
},
},
},
})
if err != nil {
t.Errorf("RS256 key: Expected nil error, got '%s'", err)
}
_, err = checkAlgorithm(&jose.JsonWebKey{
Key: &rsa.PublicKey{},
}, &jose.JsonWebSignature{
Signatures: []jose.Signature{
{
Header: jose.JoseHeader{
Algorithm: "RS256",
},
},
},
})
if err != nil {
t.Errorf("RS256 key: Expected nil error, got '%s'", err)
}
_, err = checkAlgorithm(&jose.JsonWebKey{
Algorithm: "ES256",
Key: &ecdsa.PublicKey{
Curve: elliptic.P256(),
},
}, &jose.JsonWebSignature{
Signatures: []jose.Signature{
{
Header: jose.JoseHeader{
Algorithm: "ES256",
},
},
},
})
if err != nil {
t.Errorf("ES256 key: Expected nil error, got '%s'", err)
}
_, err = checkAlgorithm(&jose.JsonWebKey{
Key: &ecdsa.PublicKey{
Curve: elliptic.P256(),
},
}, &jose.JsonWebSignature{
Signatures: []jose.Signature{
{
Header: jose.JoseHeader{
Algorithm: "ES256",
},
},
},
})
if err != nil {
t.Errorf("ES256 key: Expected nil error, got '%s'", err)
}
}

80
wfe2/probs.go Normal file
View File

@ -0,0 +1,80 @@
package wfe2
import (
"fmt"
"net/http"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/probs"
)
func problemDetailsForBoulderError(err *berrors.BoulderError, msg string) *probs.ProblemDetails {
switch err.Type {
case berrors.NotSupported:
return &probs.ProblemDetails{
Type: probs.ServerInternalProblem,
Detail: fmt.Sprintf("%s :: %s", msg, err),
HTTPStatus: http.StatusNotImplemented,
}
case berrors.Malformed:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
case berrors.Unauthorized:
return probs.Unauthorized(fmt.Sprintf("%s :: %s", msg, err))
case berrors.NotFound:
return probs.NotFound(fmt.Sprintf("%s :: %s", msg, err))
case berrors.RateLimit:
return probs.RateLimited(fmt.Sprintf("%s :: %s", msg, err))
case berrors.InternalServer:
// Internal server error messages may include sensitive data, so we do
// not include it.
return probs.ServerInternal(msg)
case berrors.RejectedIdentifier:
return probs.RejectedIdentifier(fmt.Sprintf("%s :: %s", msg, err))
case berrors.InvalidEmail:
return probs.InvalidEmail(fmt.Sprintf("%s :: %s", msg, err))
default:
// Internal server error messages may include sensitive data, so we do
// not include it.
return probs.ServerInternal(msg)
}
}
// problemDetailsForError turns an error into a ProblemDetails with the special
// case of returning the same error back if its already a ProblemDetails. If the
// error is of an type unknown to ProblemDetailsForError, it will return a
// ServerInternal ProblemDetails.
func problemDetailsForError(err error, msg string) *probs.ProblemDetails {
switch e := err.(type) {
case *probs.ProblemDetails:
return e
case *berrors.BoulderError:
return problemDetailsForBoulderError(e, msg)
case signatureValidationError:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
case core.MalformedRequestError:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
case core.NotSupportedError:
return &probs.ProblemDetails{
Type: probs.ServerInternalProblem,
Detail: fmt.Sprintf("%s :: %s", msg, err),
HTTPStatus: http.StatusNotImplemented,
}
case core.UnauthorizedError:
return probs.Unauthorized(fmt.Sprintf("%s :: %s", msg, err))
case core.NotFoundError:
return probs.NotFound(fmt.Sprintf("%s :: %s", msg, err))
case core.LengthRequiredError:
prob := probs.Malformed("missing Content-Length header")
prob.HTTPStatus = http.StatusLengthRequired
return prob
case core.RateLimitedError:
return probs.RateLimited(fmt.Sprintf("%s :: %s", msg, err))
case core.BadNonceError:
return probs.BadNonce(fmt.Sprintf("%s :: %s", msg, err))
default:
// Internal server error messages may include sensitive data, so we do
// not include it.
return probs.ServerInternal(msg)
}
}

75
wfe2/probs_test.go Normal file
View File

@ -0,0 +1,75 @@
package wfe2
import (
"fmt"
"reflect"
"testing"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test"
)
func TestProblemDetailsFromError(t *testing.T) {
// errMsg is used as the msg argument for `problemDetailsForError` and is
// always returned in the problem detail.
const errMsg = "testError"
// detailMsg is used as the msg argument for the individual error types and is
// sometimes not present in the produced problem's detail.
const detailMsg = "testDetail"
// fullDetail is what we expect the problem detail to look like when it
// contains both the error message and the detail message
fullDetail := fmt.Sprintf("%s :: %s", errMsg, detailMsg)
testCases := []struct {
err error
statusCode int
problem probs.ProblemType
detail string
}{
// boulder/core error types:
// Internal server errors expect just the `errMsg` in detail.
{core.InternalServerError(detailMsg), 500, probs.ServerInternalProblem, errMsg},
// Other errors expect the full detail message
{core.NotSupportedError(detailMsg), 501, probs.ServerInternalProblem, fullDetail},
{core.MalformedRequestError(detailMsg), 400, probs.MalformedProblem, fullDetail},
{core.UnauthorizedError(detailMsg), 403, probs.UnauthorizedProblem, fullDetail},
{core.NotFoundError(detailMsg), 404, probs.MalformedProblem, fullDetail},
{signatureValidationError(detailMsg), 400, probs.MalformedProblem, fullDetail},
{core.RateLimitedError(detailMsg), 429, probs.RateLimitedProblem, fullDetail},
{core.BadNonceError(detailMsg), 400, probs.BadNonceProblem, fullDetail},
// The content length error has its own specific detail message
{core.LengthRequiredError(detailMsg), 411, probs.MalformedProblem, "missing Content-Length header"},
// boulder/errors error types
// Internal server errors expect just the `errMsg` in detail.
{berrors.InternalServerError(detailMsg), 500, probs.ServerInternalProblem, errMsg},
// Other errors expect the full detail message
{berrors.NotSupportedError(detailMsg), 501, probs.ServerInternalProblem, fullDetail},
{berrors.MalformedError(detailMsg), 400, probs.MalformedProblem, fullDetail},
{berrors.UnauthorizedError(detailMsg), 403, probs.UnauthorizedProblem, fullDetail},
{berrors.NotFoundError(detailMsg), 404, probs.MalformedProblem, fullDetail},
{berrors.RateLimitError(detailMsg), 429, probs.RateLimitedProblem, fullDetail},
{berrors.InvalidEmailError(detailMsg), 400, probs.InvalidEmailProblem, fullDetail},
{berrors.RejectedIdentifierError(detailMsg), 400, probs.RejectedIdentifierProblem, fullDetail},
}
for _, c := range testCases {
p := problemDetailsForError(c.err, errMsg)
if p.HTTPStatus != c.statusCode {
t.Errorf("Incorrect status code for %s. Expected %d, got %d", reflect.TypeOf(c.err).Name(), c.statusCode, p.HTTPStatus)
}
if probs.ProblemType(p.Type) != c.problem {
t.Errorf("Expected problem urn %#v, got %#v", c.problem, p.Type)
}
if p.Detail != c.detail {
t.Errorf("Expected detailed message %q, got %q", c.detail, p.Detail)
}
}
expected := &probs.ProblemDetails{
Type: probs.MalformedProblem,
HTTPStatus: 200,
Detail: "gotcha",
}
p := problemDetailsForError(expected, "k")
test.AssertDeepEquals(t, expected, p)
}

19
wfe2/test/178.crt Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfmgAwIBAgICALIwDQYJKoZIhvcNAQELBQAwDjEMMAoGA1UEAxMDMTc4
MB4XDTE3MDIwMzAzNDcyNloXDTE4MDIwMzAzNDcyNlowDjEMMAoGA1UEAxMDMTc4
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuXUn5n4NBLK6CqJXBR+t
dM4SVY911FKAwxI13k3aQvtBjaIPe6/CDiG7ZxGDsEB96pI6yYauhDQg6ELXcPN0
dmRX4qxVMQ/ngS7bSc7FmlN1qkq9p1AxNmesCmsWg9/4yJNCmlTdGu2Mo60Iosxx
CnQP3faG7ZPrGwzYvX9rwNedD3GlrFarQuU8VzD91fSQIzbDBtlP/+bY4FUbDtzw
WGpuAorrSOeDxC0Y3Tmd6IJLczof+vFP3EYjX+fwjnSWe75zz3z2DhVYu0tiid3k
UFDLaI5pY9JYYG3/D59lVKxg48PQP5q4qqWzmFnuUW/GOFHJABFnmOoD9j4t2YLk
GwIDAQABo3kwdzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
DAYDVR0TAQH/BAIwADBCBggrBgEFBQcBAQQ2MDQwMgYIKwYBBQUHMAKGJmh0dHA6
Ly9sb2NhbGhvc3Q6NDAwMC9hY21lL2lzc3Vlci1jZXJ0MA0GCSqGSIb3DQEBCwUA
A4IBAQBCeU7UGIDKqVJ3fG0GOGlz1JHDh51UIQ2w/KK3NRlqdtlQ3tcqBHYspVMz
YjliJuiVXi/hLEd9IyaTEfxqPnpla7rYo0PgChQ/Eg+IPMJm5t3HwnuNTsvJucX+
gCA/vGKsqSZU58JeilBVo4jl6btUc1LYMCWQ1QRfBpei/9sV0EF3f3HosqYA5I0L
VYzmsLBd8uyttFazgQKfM7Y/h1FcWGGJkH0rsZI7h4OOl0dn2aM9SCHiergJj4Sz
S6hUp2+RR70GSuZejYc7NGqk7/g624c6jJETEqJEBPy6tvxSq+DlVT1K3gWmM+Mc
yJjiZCq2Lifrn0KxkKuxsqWEW2tO
-----END CERTIFICATE-----

27
wfe2/test/178.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAuXUn5n4NBLK6CqJXBR+tdM4SVY911FKAwxI13k3aQvtBjaIP
e6/CDiG7ZxGDsEB96pI6yYauhDQg6ELXcPN0dmRX4qxVMQ/ngS7bSc7FmlN1qkq9
p1AxNmesCmsWg9/4yJNCmlTdGu2Mo60IosxxCnQP3faG7ZPrGwzYvX9rwNedD3Gl
rFarQuU8VzD91fSQIzbDBtlP/+bY4FUbDtzwWGpuAorrSOeDxC0Y3Tmd6IJLczof
+vFP3EYjX+fwjnSWe75zz3z2DhVYu0tiid3kUFDLaI5pY9JYYG3/D59lVKxg48PQ
P5q4qqWzmFnuUW/GOFHJABFnmOoD9j4t2YLkGwIDAQABAoIBAQCZ5kfbNUU2Xd6X
DoqqHNSDdrKuP+Om82QY/RaoyPBT309R6mdw27Rsp79tU5J1g786FmkkbViLKvsX
4sgH2nAOA00PNLVphmo1wJ2HTUibvaCKVYW2v4xnOncBGkbP3uAECngdvEjTnMe1
19SvzHoOE6xLJNZpdvOGOg3uizvvBVJbLg2osrJXacoulOjjpd5YCVJQT9vDKhUa
aq7CmInYfOM2flcAo7nLHWP7Jr4FX4me50lrYuzBOaJWLHcQH/mZriTgI8cXCoJx
fk0Lav38z6BgYumREa0OOGDVkNuxde5KSdcFUUEEfvtPSnruVwdNHja3z3d4Y1Bx
ca1khx4JAoGBAMMFbbFRQwtxTED75dr8qXZE8kJNmIvPC1og3zRjsN0aVaUltRFl
pj+/HZXOAxU+uc4ac7vzD/5ysSHhp2rrzVglBveSlM7U88KcEYI+Yu2KzM12UMkT
lIWQDtfIpnvXPwnMsde9JzQvXeeyEhy6IBDwtRg3UaNdday/+V9bKUznAoGBAPNy
NPsFtdQuT0FU8W3ehPw7dkBZl8YGy3YQxMh8IcThx4NRJHkK0yTV3/zg8owW6WvN
EPhEIWQ4u9szf4zPoCHbckEScLeDYlc/hyf2hmQdlYQTyZXE+nou1VYawlv9mJJ1
88Sct0ygmVcCcdCsi68abOijs+TJrGsI+cjKzTStAoGAd45vZeIMeQpXFguXKT31
4aR44/7QAv3F1tYKIALxnUqUsK7CJ00qsy/Fwl3OdArFO73pr5Jd/r5vKvc8fIbc
lynz8HhzM61HVsn4zeDTIw8RaPAcrHiNd6gOAWln7snRQn+zky/Jxes35V+8TNIp
8FiwnIzlRoJ4LpRuG3A2jIcCgYEAqRroGoa4647Plv4+RqePkPZtCf4yI2iM5JJ5
Xxp7CpwbTuiKgVo3mRrH4I0RbqZrtmpYI1yQJWITfAylyVZgUaRyFSmOCqvFH/4N
EIF6kQjL11c3bEXMCBuILaug3u2lkfdFQYnq+duFKJ+WF/IDhbrBdEhiqcY9coxl
lkjpinECgYEAkZO0XS0Z7KfFgSMQ2uFK8MO7naQM5kg+8H26HPuNWRmlkRDR7tOI
gQcwSbx7vqT5JJM/bSWI49b2Q19QncFJ0A8/P3/3dncFQ8QMsfMVzmhHvcn2SthU
Eh0aoOwi7rPYNiCTd/3y04x42a/hmo8rmcXOodZvnewOFbDu/s/m7ig=
-----END RSA PRIVATE KEY-----

19
wfe2/test/238.crt Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgICAO4wDQYJKoZIhvcNAQELBQAwDjEMMAoGA1UEAwwDMjM4
MB4XDTE1MDYxMzAwMTU1NVoXDTE2MDYxMjAwMTU1NVowDjEMMAoGA1UEAwwDMjM4
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvP9z1YFDa1WD9hVI9W3K
lWQmUGfLW35x6xkgDm8o3OTWR2QoxjXratacKhm2VevV22QjCBvHXeHx3fxSp5w/
p4CH+Ul76wCq3+WAPidO42YCP7SZdqYUR4GHKQ/oOyistRAKEamg4aPAbIs7l1Kn
T5YHFdSzCWpe6F2+ceoluvKEn6vFVloXKghaeEyTDKnnJKs3/04TdtZjVM5OObvQ
CGFlQlysDJxWahtVM93gylB8WYgyiekDAx1I3lCd3Vv0hF+x04xT3fwVRzmaKNzT
wN+znae643Qfg2oSSLV066K2WYepgzqKwv3IUdrLbes331AMs+FbdxHanMrOU1i+
OQIDAQABo28wbTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSOiDuzx4mEC9Io
y+7rEdnE+eeOyDAfBgNVHSMEGDAWgBSOiDuzx4mEC9Ioy+7rEdnE+eeOyDAaBgNV
HREEEzARgg9iYWQuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAJQA/7+n
S9AiB9YduVEs2TB7+62N59yACxd1y5qnmSLEeI9yXZnqQGugNxw7cl3CgFDWLNxB
8Q3hH5B0fYh2Ydqf8lrEYNH3ilsmqCQB3mHUlYtLLnVarzSPrFgxaBrRaGsAAaVd
neC5QCaxLFzzQI9gmyp6n7T2CATOk94vrrZJmfzpCMMRPHY7XgM15HDefXeH1+/Z
GESSM/YAD6rdojZVLwxTuzVVRm5+6NfnFG938SYir0aqYvFd0bxrdgTl1XR3sAip
iwuI3ku943Thbmyp/fEBUE2unvf+wbX+3Vzq52NadPcUrsNwJAR/kGdmTzcsiCIA
UL+BLF470rQo29w=
-----END CERTIFICATE-----

28
wfe2/test/238.key Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8/3PVgUNrVYP2
FUj1bcqVZCZQZ8tbfnHrGSAObyjc5NZHZCjGNetq1pwqGbZV69XbZCMIG8dd4fHd
/FKnnD+ngIf5SXvrAKrf5YA+J07jZgI/tJl2phRHgYcpD+g7KKy1EAoRqaDho8Bs
izuXUqdPlgcV1LMJal7oXb5x6iW68oSfq8VWWhcqCFp4TJMMqeckqzf/ThN21mNU
zk45u9AIYWVCXKwMnFZqG1Uz3eDKUHxZiDKJ6QMDHUjeUJ3dW/SEX7HTjFPd/BVH
OZoo3NPA37Odp7rjdB+DahJItXTrorZZh6mDOorC/chR2stt6zffUAyz4Vt3Edqc
ys5TWL45AgMBAAECggEAc1PSJCt/r2R8ZNJyNclsQCLfulrL3aXX7+TiCczM+5Xs
J543v1Oxtv0ESDBuchm54ulE8zK4QlKYm6PX8A1JTnYBAx5TLoC2xG8wBT1JRzu9
DZCvwJXxc/zXNDhPtqHIWahS7Jo84NNinRmNIHbAP7FF241yPsGY7mQdzTdbFKrR
JH0l7VPCY4OG+CjxUJqoNuwkfrNh0hRh02IHU/rFlgR2Q7JP0XBwuufW1M6j7fYM
7PGZRA+6Ry72UcaCEVuOtGlz3wLrFq6CGTGWlUehQqch+nrTri0jMSH4Bd83mLz2
8+X0y/EONQlirbHbJxXq+mLASHrp3KCtdpCiLKcX8QKBgQDr+TNqLa7PIOhlw29A
RftunKwEdsi9uAg3jFSpHC/jLxR4/fUiz2XZrAfHNxn7mOK72V/9pj9zshLnxeSm
jEelEB2bABX8RhD38SUxoHoiWmqpPVOtBSXvMSQEO0F/1hGlxndHwe9mE2Zyq3eV
9MoJVeExkCP3Bxk9tjZfj4WC9QKBgQDNCab2WjLy7T9Bfmh2RmWXckzUMphYCLpX
CGG2O5nH2zOPAOxUpyLFDq3/WkzPnCdWOveI/LlZmkcjdslWp3tizk5kE1zgaFbO
s+7o/cYVrU5J3+kIq563ba7/xZ7wpfkg58milUWStpjQrB0H5tSlUEoC7fJ/GjHd
5j1raKQrtQKBgF9elSgJlIgD/cj7JqBsaET5LxCSzWjX0wJYRfMfAD+qTHTl9sf9
2GUUAQTDwU2NKb3QCdqi8SwaQUfJFDM3qNEOZVi6vSf7TWpX3Ldk61etAUSrE4Fu
/jjgvHS1WjCHXRSJ1LV8rPutRY98u1Uw3OLPAbedUNvK06m8VddjUwttAoGAAmca
jciA0Ff3Zc0VbE1m419zhwkQv/daN6rhekE4jB8Fe6eHHXbX8Xc6ksN8IvKxg1Et
lW1gvqwQKVo7Acj0qTPBt2qCrB6M5d817YULzTU6taLqGC/qrDuc0WJ/elJ3mOse
cclOB2ocYFWkAXOzCjzmoSIotVSZQQBxt9CCHAECgYEA01w8tKVCG2ucbC1GoCl0
t2MRmLqiRqRrn53fJ6j56fDbdLmnRAaaD1slZ0jpLk7JoDKGmNG2Rl9UXuydPaNZ
8h1Lu+CnhG50uOF3A/OIXsBiRsAgI2ez4/Jb+lNe3l3UcPV5gyGejAiymqRigbkn
bcixOm4jdOWV5Bpfv65AivQ=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,29 @@
Produced by:
js test.js --agree --email jsha@newview.org --domains not-an-example.com --certFile cert.der --certKey ../../wfe/test/178.key
openssl x509 -text -inform der -in cert.der -outform pem -out ../../wfe/test/not-an-example.com.crt
-----BEGIN CERTIFICATE-----
MIIEYzCCA0ugAwIBAgIRAP8AAAAAAAAOS09n2G6BjEYwDQYJKoZIhvcNAQELBQAw
HzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwHhcNMTUwOTA5MjI1NjAw
WhcNMTUxMjA4MjI1NjAwWjAdMRswGQYDVQQDExJub3QtYW4tZXhhbXBsZS5jb20w
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaqzue57mgXEoGTZZoVkkC
ZraebWgXI8irX2BgQB1A3iZa9onxGPMcWQMxhSuUisbEJi4UkMcVST12HX01rUwh
j41UuBxJvI1w4wvdstssTAaa9c9tsQ5+UED2bFRL1MsyBdbmCF/+pu3i+ZIYqWgi
KbjVBe3nlAVbo77zizwp3Y4Tp1/TBOwTAuFkHePmkNT63uPm9My/hNzsSm1o+Q51
9Cf7ry+JQmOVgz/jIgFVGFYJ17EV3KUIpUuDShuyCFATBQspgJSN2DoXRUlQjXXk
NTj23OxxdT/cVLcLJjytyG6e5izME2R2aCkDBWIc1a4/sRJ0R396auPXG6KhJ7o/
AgMBAAGjggGaMIIBljAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUH
AwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBGkJtghSl97/bTR
dyy0TaneZhnDMB8GA1UdIwQYMBaAFPt4TxL5YBWDLJ8XfzQZsy426kGJMGoGCCsG
AQUFBwEBBF4wXDAmBggrBgEFBQcwAYYaaHR0cDovL2xvY2FsaG9zdDo0MDAyL29j
c3AwMgYIKwYBBQUHMAKGJmh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC9hY21lL2lzc3Vl
ci1jZXJ0MB0GA1UdEQQWMBSCEm5vdC1hbi1leGFtcGxlLmNvbTAnBgNVHR8EIDAe
MBygGqAYhhZodHRwOi8vZXhhbXBsZS5jb20vY3JsMGMGA1UdIARcMFowCgYGZ4EM
AQIBMAAwTAYDKgMEMEUwIgYIKwYBBQUHAgEWFmh0dHA6Ly9leGFtcGxlLmNvbS9j
cHMwHwYIKwYBBQUHAgIwEwwRRG8gV2hhdCBUaG91IFdpbHQwDQYJKoZIhvcNAQEL
BQADggEBAJTSscrGO1ymwZ+rMF+mfVeHfplfyMzZ/6SZyvaYgO9DLr42KIETdHBg
Y9AZ6aOKboN/hY98kb9mQ0BpOCsSaCkgTsqCjw3szsRd/FMgUSVn36vFpbX2f5oD
gF40N/51EN5Efbe7aN4Oxmcgijh4IY2sczcskJixAd9T/hjVtv160LJ0xcHRrfji
u/Tc2E0q+E5k4V91D2HajwU6qcGbap02JI+pX/Oq4S36yfggIUyowmXQw4nm1cb0
cFXwrMzg+XtDHj+Ex+yBlauq+MP1rjXiHrNIO2hIiyRU9jdxfITAE4DmqEzEBZKY
NORfB6suv4wLnAlsLbPJEdsraq4/IiU=
-----END CERTIFICATE-----

1599
wfe2/wfe.go Normal file

File diff suppressed because it is too large Load Diff

2224
wfe2/wfe_test.go Normal file

File diff suppressed because it is too large Load Diff