Merge pull request #2626 from letsencrypt/master

Merge master to staging
This commit is contained in:
Daniel McCarney 2017-03-27 11:21:30 -04:00 committed by GitHub
commit b8319e7fa3
49 changed files with 1055 additions and 530 deletions

View File

@ -83,13 +83,9 @@ We recommend setting git's [fsckObjects
setting](https://groups.google.com/forum/#!topic/binary-transparency/f-BI4o8HZW0/discussion) setting](https://groups.google.com/forum/#!topic/binary-transparency/f-BI4o8HZW0/discussion)
for better integrity guarantees when getting updates. for better integrity guarantees when getting updates.
Boulder requires an installation of RabbitMQ, libtool-ltdl, goose, and Boulder requires an installation of libtool-ltdl, goose, SoftHSM, and MariaDB 10.1 to work correctly. If you want to save some trouble installing MariaDB and SoftHSM you can run them using Docker:
MariaDB 10.1 to work correctly. On Ubuntu and CentOS, you may have to
install RabbitMQ from https://rabbitmq.com/download.html to get a
recent version. If you want to save some trouble installing MariaDB and RabbitMQ
you can run them using Docker:
docker-compose up -d bmysql brabbitmq bhsm docker-compose up -d bmysql bhsm
Also, Boulder requires Go 1.5. As of September 2015 this version is not yet Also, Boulder requires Go 1.5. As of September 2015 this version is not yet
available in OS repositories, so you will have to install from https://golang.org/dl/. available in OS repositories, so you will have to install from https://golang.org/dl/.
@ -121,7 +117,7 @@ Edit /etc/hosts to add this line:
127.0.0.1 boulder boulder-rabbitmq boulder-mysql 127.0.0.1 boulder boulder-rabbitmq boulder-mysql
Resolve Go-dependencies, set up a database and RabbitMQ: Resolve Go-dependencies, set up a database:
./test/setup.sh ./test/setup.sh
@ -198,7 +194,7 @@ Requests from ACME clients result in new objects and changes to objects. The St
Objects are also passed from one component to another on change events. For example, when a client provides a successful response to a validation challenge, it results in a change to the corresponding validation object. The Validation Authority forwards the new validation object to the Storage Authority for storage, and to the Registration Authority for any updates to a related Authorization object. Objects are also passed from one component to another on change events. For example, when a client provides a successful response to a validation challenge, it results in a change to the corresponding validation object. The Validation Authority forwards the new validation object to the Storage Authority for storage, and to the Registration Authority for any updates to a related Authorization object.
Boulder uses AMQP as a message bus. For components that you want to be remote, it is necessary to instantiate a "client" and "server" for that component. The client implements the component's Go interface, while the server has the actual logic for the component. More details in `amqp-rpc.go`. Boulder uses gRPC for inter-component communication. For components that you want to be remote, it is necessary to instantiate a "client" and "server" for that component. The client implements the component's Go interface, while the server has the actual logic for the component. More details on this communication model can be found in the [gRPC documentation](http://www.grpc.io/docs/).
The full details of how the various ACME operations happen in Boulder are laid out in [DESIGN.md](https://github.com/letsencrypt/boulder/blob/master/DESIGN.md) The full details of how the various ACME operations happen in Boulder are laid out in [DESIGN.md](https://github.com/letsencrypt/boulder/blob/master/DESIGN.md)
@ -208,9 +204,7 @@ Dependencies
All Go dependencies are vendored under the vendor directory, All Go dependencies are vendored under the vendor directory,
to [make dependency management easier](https://golang.org/cmd/go/#hdr-Vendor_Directories). to [make dependency management easier](https://golang.org/cmd/go/#hdr-Vendor_Directories).
Local development also requires a RabbitMQ installation and MariaDB Local development also requires a MariaDB 10 installation. MariaDB should be run on port 3306 for the default integration tests.
10 installation (see above). MariaDB should be run on port 3306 for the
default integration tests.
To update the Go dependencies: To update the Go dependencies:
@ -253,7 +247,6 @@ you will get conflicting types between our vendored version and the cfssl vendor
Adding RPCs Adding RPCs
----------- -----------
Boulder is moving towards using gRPC for all RPCs. To add a new RPC method, add Boulder uses gRPC for all RPCs. To add a new RPC method, add it to the relevant .proto file, then run:
it to the relevant .proto file, then run:
docker-compose run boulder go generate ./path/to/pkg/... docker-compose run boulder go generate ./path/to/pkg/...

View File

@ -134,7 +134,7 @@ func (mock *MockDNSResolver) LookupCAA(_ context.Context, domain string) ([]*dns
record.Value = ";" record.Value = ";"
results = append(results, &record) results = append(results, &record)
case "bad-local-resolver.com": case "bad-local-resolver.com":
return nil, DNSError{underlying: MockTimeoutError()} return nil, &DNSError{dns.TypeCAA, domain, MockTimeoutError(), -1}
} }
return results, nil return results, nil
} }

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"net" "net"
"github.com/letsencrypt/boulder/probs"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -56,15 +55,3 @@ func (d DNSError) Timeout() bool {
const detailDNSTimeout = "query timed out" const detailDNSTimeout = "query timed out"
const detailDNSNetFailure = "networking error" const detailDNSNetFailure = "networking error"
const detailServerFailure = "server failure at resolver" const detailServerFailure = "server failure at resolver"
// ProblemDetailsFromDNSError checks the error returned from Lookup... methods
// and tests if the error was an underlying net.OpError or an error caused by
// resolver returning SERVFAIL or other invalid Rcodes and returns the relevant
// core.ProblemDetails. The detail string will contain a mention of the DNS
// record type and domain given.
func ProblemDetailsFromDNSError(err error) *probs.ProblemDetails {
if dnsErr, ok := err.(*DNSError); ok {
return probs.ConnectionFailure(dnsErr.Error())
}
return probs.ConnectionFailure(detailServerFailure)
}

View File

@ -7,11 +7,9 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/context" "golang.org/x/net/context"
"github.com/letsencrypt/boulder/probs"
) )
func TestProblemDetailsFromDNSError(t *testing.T) { func TestDNSError(t *testing.T) {
testCases := []struct { testCases := []struct {
err error err error
expected string expected string
@ -19,9 +17,6 @@ func TestProblemDetailsFromDNSError(t *testing.T) {
{ {
&DNSError{dns.TypeA, "hostname", MockTimeoutError(), -1}, &DNSError{dns.TypeA, "hostname", MockTimeoutError(), -1},
"DNS problem: query timed out looking up A for hostname", "DNS problem: query timed out looking up A for hostname",
}, {
errors.New("other failure"),
detailServerFailure,
}, { }, {
&DNSError{dns.TypeMX, "hostname", &net.OpError{Err: errors.New("some net error")}, -1}, &DNSError{dns.TypeMX, "hostname", &net.OpError{Err: errors.New("some net error")}, -1},
"DNS problem: networking error looking up MX for hostname", "DNS problem: networking error looking up MX for hostname",
@ -37,12 +32,8 @@ func TestProblemDetailsFromDNSError(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
err := ProblemDetailsFromDNSError(tc.err) if tc.err.Error() != tc.expected {
if err.Type != probs.ConnectionProblem { t.Errorf("got %q, expected %q", tc.err.Error(), tc.expected)
t.Errorf("ProblemDetailsFromDNSError(%q).Type = %q, expected %q", tc.err, err.Type, probs.ConnectionProblem)
}
if err.Detail != tc.expected {
t.Errorf("ProblemDetailsFromDNSError(%q).Detail = %q, expected %q", tc.err, err.Detail, tc.expected)
} }
} }
} }

View File

@ -29,6 +29,7 @@ import (
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
csrlib "github.com/letsencrypt/boulder/csr" csrlib "github.com/letsencrypt/boulder/csr"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics"
@ -295,12 +296,10 @@ func (ca *CertificateAuthorityImpl) extensionsFromCSR(csr *x509.CertificateReque
ca.stats.Inc(metricCSRExtensionTLSFeature, 1) ca.stats.Inc(metricCSRExtensionTLSFeature, 1)
value, ok := ext.Value.([]byte) value, ok := ext.Value.([]byte)
if !ok { if !ok {
msg := fmt.Sprintf("Malformed extension with OID %v", ext.Type) return nil, berrors.MalformedError("malformed extension with OID %v", ext.Type)
return nil, core.MalformedRequestError(msg)
} else if !bytes.Equal(value, mustStapleFeatureValue) { } else if !bytes.Equal(value, mustStapleFeatureValue) {
msg := fmt.Sprintf("Unsupported value for extension with OID %v", ext.Type)
ca.stats.Inc(metricCSRExtensionTLSFeatureInvalid, 1) ca.stats.Inc(metricCSRExtensionTLSFeatureInvalid, 1)
return nil, core.MalformedRequestError(msg) return nil, berrors.MalformedError("unsupported value for extension with OID %v", ext.Type)
} }
if ca.enableMustStaple { if ca.enableMustStaple {
@ -386,7 +385,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(ctx context.Context, csr x5
regID, regID,
); err != nil { ); err != nil {
ca.log.AuditErr(err.Error()) ca.log.AuditErr(err.Error())
return emptyCert, core.MalformedRequestError(err.Error()) return emptyCert, berrors.MalformedError(err.Error())
} }
requestedExtensions, err := ca.extensionsFromCSR(&csr) requestedExtensions, err := ca.extensionsFromCSR(&csr)
@ -398,7 +397,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(ctx context.Context, csr x5
notAfter := ca.clk.Now().Add(ca.validityPeriod) notAfter := ca.clk.Now().Add(ca.validityPeriod)
if issuer.cert.NotAfter.Before(notAfter) { if issuer.cert.NotAfter.Before(notAfter) {
err = core.InternalServerError("Cannot issue a certificate that expires after the issuer certificate.") err = berrors.InternalServerError("cannot issue a certificate that expires after the issuer certificate")
ca.log.AuditErr(err.Error()) ca.log.AuditErr(err.Error())
return emptyCert, err return emptyCert, err
} }
@ -415,7 +414,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(ctx context.Context, csr x5
serialBytes[0] = byte(ca.prefix) serialBytes[0] = byte(ca.prefix)
_, err = rand.Read(serialBytes[1:]) _, err = rand.Read(serialBytes[1:])
if err != nil { if err != nil {
err = core.InternalServerError(err.Error()) err = berrors.InternalServerError("failed to generate serial: %s", err)
ca.log.AuditErr(fmt.Sprintf("Serial randomness failed, err=[%v]", err)) ca.log.AuditErr(fmt.Sprintf("Serial randomness failed, err=[%v]", err))
return emptyCert, err return emptyCert, err
} }
@ -430,7 +429,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(ctx context.Context, csr x5
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
profile = ca.ecdsaProfile profile = ca.ecdsaProfile
default: default:
err = core.InternalServerError(fmt.Sprintf("unsupported key type %T", csr.PublicKey)) err = berrors.InternalServerError("unsupported key type %T", csr.PublicKey)
ca.log.AuditErr(err.Error()) ca.log.AuditErr(err.Error())
return emptyCert, err return emptyCert, err
} }
@ -456,21 +455,21 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(ctx context.Context, csr x5
certPEM, err := issuer.eeSigner.Sign(req) certPEM, err := issuer.eeSigner.Sign(req)
ca.noteSignError(err) ca.noteSignError(err)
if err != nil { if err != nil {
err = core.InternalServerError(err.Error()) err = berrors.InternalServerError("failed to sign certificate: %s", err)
ca.log.AuditErr(fmt.Sprintf("Signing failed: serial=[%s] err=[%v]", serialHex, err)) ca.log.AuditErr(fmt.Sprintf("Signing failed: serial=[%s] err=[%v]", serialHex, err))
return emptyCert, err return emptyCert, err
} }
ca.stats.Inc("Signatures.Certificate", 1) ca.stats.Inc("Signatures.Certificate", 1)
if len(certPEM) == 0 { if len(certPEM) == 0 {
err = core.InternalServerError("No certificate returned by server") err = berrors.InternalServerError("no certificate returned by server")
ca.log.AuditErr(fmt.Sprintf("PEM empty from Signer: serial=[%s] err=[%v]", serialHex, err)) ca.log.AuditErr(fmt.Sprintf("PEM empty from Signer: serial=[%s] err=[%v]", serialHex, err))
return emptyCert, err return emptyCert, err
} }
block, _ := pem.Decode(certPEM) block, _ := pem.Decode(certPEM)
if block == nil || block.Type != "CERTIFICATE" { if block == nil || block.Type != "CERTIFICATE" {
err = core.InternalServerError("Invalid certificate value returned") err = berrors.InternalServerError("invalid certificate value returned")
ca.log.AuditErr(fmt.Sprintf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", ca.log.AuditErr(fmt.Sprintf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]",
serialHex, certPEM, err)) serialHex, certPEM, err))
return emptyCert, err return emptyCert, err
@ -487,7 +486,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(ctx context.Context, csr x5
// This is one last check for uncaught errors // This is one last check for uncaught errors
if err != nil { if err != nil {
err = core.InternalServerError(err.Error()) err = berrors.InternalServerError(err.Error())
ca.log.AuditErr(fmt.Sprintf("Uncaught error, aborting: serial=[%s] cert=[%s] err=[%v]", ca.log.AuditErr(fmt.Sprintf("Uncaught error, aborting: serial=[%s] cert=[%s] err=[%v]",
serialHex, hex.EncodeToString(certDER), err)) serialHex, hex.EncodeToString(certDER), err))
return emptyCert, err return emptyCert, err
@ -496,7 +495,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(ctx context.Context, csr x5
// Store the cert with the certificate authority, if provided // Store the cert with the certificate authority, if provided
_, err = ca.SA.AddCertificate(ctx, certDER, regID) _, err = ca.SA.AddCertificate(ctx, certDER, regID)
if err != nil { if err != nil {
err = core.InternalServerError(err.Error()) err = berrors.InternalServerError(err.Error())
// Note: This log line is parsed by cmd/orphan-finder. If you make any // Note: This log line is parsed by cmd/orphan-finder. If you make any
// changes here, you should make sure they are reflected in orphan-finder. // changes here, you should make sure they are reflected in orphan-finder.
ca.log.AuditErr(fmt.Sprintf( ca.log.AuditErr(fmt.Sprintf(

View File

@ -21,6 +21,7 @@ import (
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics"
@ -471,8 +472,7 @@ func TestNoHostnames(t *testing.T) {
csr, _ := x509.ParseCertificateRequest(NoNamesCSR) csr, _ := x509.ParseCertificateRequest(NoNamesCSR)
_, err = ca.IssueCertificate(ctx, *csr, 1001) _, err = ca.IssueCertificate(ctx, *csr, 1001)
test.AssertError(t, err, "Issued certificate with no names") test.AssertError(t, err, "Issued certificate with no names")
_, ok := err.(core.MalformedRequestError) test.Assert(t, berrors.Is(err, berrors.Malformed), "Incorrect error type returned")
test.Assert(t, ok, "Incorrect error type returned")
} }
func TestRejectTooManyNames(t *testing.T) { func TestRejectTooManyNames(t *testing.T) {
@ -493,8 +493,7 @@ func TestRejectTooManyNames(t *testing.T) {
csr, _ := x509.ParseCertificateRequest(TooManyNameCSR) csr, _ := x509.ParseCertificateRequest(TooManyNameCSR)
_, err = ca.IssueCertificate(ctx, *csr, 1001) _, err = ca.IssueCertificate(ctx, *csr, 1001)
test.AssertError(t, err, "Issued certificate with too many names") test.AssertError(t, err, "Issued certificate with too many names")
_, ok := err.(core.MalformedRequestError) test.Assert(t, berrors.Is(err, berrors.Malformed), "Incorrect error type returned")
test.Assert(t, ok, "Incorrect error type returned")
} }
func TestRejectValidityTooLong(t *testing.T) { func TestRejectValidityTooLong(t *testing.T) {
@ -520,8 +519,7 @@ func TestRejectValidityTooLong(t *testing.T) {
csr, _ := x509.ParseCertificateRequest(NoCNCSR) csr, _ := x509.ParseCertificateRequest(NoCNCSR)
_, err = ca.IssueCertificate(ctx, *csr, 1) _, err = ca.IssueCertificate(ctx, *csr, 1)
test.AssertError(t, err, "Cannot issue a certificate that expires after the intermediate certificate") test.AssertError(t, err, "Cannot issue a certificate that expires after the intermediate certificate")
_, ok := err.(core.InternalServerError) test.Assert(t, berrors.Is(err, berrors.InternalServer), "Incorrect error type returned")
test.Assert(t, ok, "Incorrect error type returned")
} }
func TestShortKey(t *testing.T) { func TestShortKey(t *testing.T) {
@ -541,8 +539,7 @@ func TestShortKey(t *testing.T) {
csr, _ := x509.ParseCertificateRequest(ShortKeyCSR) csr, _ := x509.ParseCertificateRequest(ShortKeyCSR)
_, err = ca.IssueCertificate(ctx, *csr, 1001) _, err = ca.IssueCertificate(ctx, *csr, 1001)
test.AssertError(t, err, "Issued a certificate with too short a key.") test.AssertError(t, err, "Issued a certificate with too short a key.")
_, ok := err.(core.MalformedRequestError) test.Assert(t, berrors.Is(err, berrors.Malformed), "Incorrect error type returned")
test.Assert(t, ok, "Incorrect error type returned")
} }
func TestAllowNoCN(t *testing.T) { func TestAllowNoCN(t *testing.T) {
@ -603,8 +600,7 @@ func TestLongCommonName(t *testing.T) {
csr, _ := x509.ParseCertificateRequest(LongCNCSR) csr, _ := x509.ParseCertificateRequest(LongCNCSR)
_, err = ca.IssueCertificate(ctx, *csr, 1001) _, err = ca.IssueCertificate(ctx, *csr, 1001)
test.AssertError(t, err, "Issued a certificate with a CN over 64 bytes.") test.AssertError(t, err, "Issued a certificate with a CN over 64 bytes.")
_, ok := err.(core.MalformedRequestError) test.Assert(t, berrors.Is(err, berrors.Malformed), "Incorrect error type returned")
test.Assert(t, ok, "Incorrect error type returned")
} }
func TestWrongSignature(t *testing.T) { func TestWrongSignature(t *testing.T) {
@ -746,9 +742,7 @@ func TestExtensions(t *testing.T) {
stats.EXPECT().Inc(metricCSRExtensionTLSFeatureInvalid, int64(1)).Return(nil) stats.EXPECT().Inc(metricCSRExtensionTLSFeatureInvalid, int64(1)).Return(nil)
_, err = ca.IssueCertificate(ctx, *tlsFeatureUnknownCSR, 1001) _, err = ca.IssueCertificate(ctx, *tlsFeatureUnknownCSR, 1001)
test.AssertError(t, err, "Allowed a CSR with an empty TLS feature extension") test.AssertError(t, err, "Allowed a CSR with an empty TLS feature extension")
if _, ok := err.(core.MalformedRequestError); !ok { test.Assert(t, berrors.Is(err, berrors.Malformed), "Wrong error type when rejecting a CSR with empty TLS feature extension")
t.Errorf("Wrong error type when rejecting a CSR with empty TLS feature extension")
}
// Unsupported extensions should be silently ignored, having the same // Unsupported extensions should be silently ignored, having the same
// extensions as the TLS Feature cert above, minus the TLS Feature Extension // extensions as the TLS Feature cert above, minus the TLS Feature Extension

View File

@ -16,6 +16,7 @@ import (
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc" bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
@ -117,7 +118,7 @@ func revokeBySerial(ctx context.Context, serial string, reasonCode revocation.Re
certObj, err := sa.SelectCertificate(tx, "WHERE serial = ?", serial) certObj, err := sa.SelectCertificate(tx, "WHERE serial = ?", serial)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return core.NotFoundError(fmt.Sprintf("No certificate found for %s", serial)) return berrors.NotFoundError("certificate with serial %q not found", serial)
} }
if err != nil { if err != nil {
return err return err

View File

@ -34,13 +34,10 @@ import (
sapb "github.com/letsencrypt/boulder/sa/proto" sapb "github.com/letsencrypt/boulder/sa/proto"
) )
const defaultNagCheckInterval = 24 * time.Hour const (
defaultNagCheckInterval = 24 * time.Hour
type emailContent struct { defaultExpirationSubject = "Let's Encrypt certificate expiration notice for domain {{.ExpirationSubject}}"
ExpirationDate string )
DaysToExpiration int
DNSNames string
}
type regStore interface { type regStore interface {
GetRegistration(context.Context, int64) (core.Registration, error) GetRegistration(context.Context, int64) (core.Registration, error)
@ -53,7 +50,7 @@ type mailer struct {
rs regStore rs regStore
mailer bmail.Mailer mailer bmail.Mailer
emailTemplate *template.Template emailTemplate *template.Template
subject string subjectTemplate *template.Template
nagTimes []time.Duration nagTimes []time.Duration
limit int limit int
clk clock.Clock clk clock.Clock
@ -101,33 +98,42 @@ func (m *mailer) sendNags(contacts []string, certs []*x509.Certificate) error {
sort.Strings(domains) sort.Strings(domains)
m.log.Debug(fmt.Sprintf("Sending mail for %s (%s)", strings.Join(domains, ", "), strings.Join(serials, ", "))) m.log.Debug(fmt.Sprintf("Sending mail for %s (%s)", strings.Join(domains, ", "), strings.Join(serials, ", ")))
var subject string // Construct the information about the expiring certificates for use in the
if m.subject != "" { // subject template
// If there is a subject from the configuration file, we should use it as-is expiringSubject := fmt.Sprintf("%q", domains[0])
// to preserve the "classic" behaviour before we added a domain name.
subject = m.subject
} else {
// Otherwise, when no subject is configured we should make one using the
// domain names in the expiring certificate
subject = fmt.Sprintf("Certificate expiration notice for domain %q", domains[0])
if len(domains) > 1 { if len(domains) > 1 {
subject += fmt.Sprintf(" (and %d more)", len(domains)-1) expiringSubject += fmt.Sprintf(" (and %d more)", len(domains)-1)
}
} }
email := emailContent{ // Execute the subjectTemplate by filling in the ExpirationSubject
subjBuf := new(bytes.Buffer)
err := m.subjectTemplate.Execute(subjBuf, struct {
ExpirationSubject string
}{
ExpirationSubject: expiringSubject,
})
if err != nil {
m.stats.Inc("Errors.SendingNag.SubjectTemplateFailure", 1)
return err
}
email := struct {
ExpirationDate string
DaysToExpiration int
DNSNames string
}{
ExpirationDate: expDate.UTC().Format(time.RFC822Z), ExpirationDate: expDate.UTC().Format(time.RFC822Z),
DaysToExpiration: int(expiresIn.Hours() / 24), DaysToExpiration: int(expiresIn.Hours() / 24),
DNSNames: strings.Join(domains, "\n"), DNSNames: strings.Join(domains, "\n"),
} }
msgBuf := new(bytes.Buffer) msgBuf := new(bytes.Buffer)
err := m.emailTemplate.Execute(msgBuf, email) err = m.emailTemplate.Execute(msgBuf, email)
if err != nil { if err != nil {
m.stats.Inc("Errors.SendingNag.TemplateFailure", 1) m.stats.Inc("Errors.SendingNag.TemplateFailure", 1)
return err return err
} }
startSending := m.clk.Now() startSending := m.clk.Now()
err = m.mailer.SendMail(emails, subject, msgBuf.String()) err = m.mailer.SendMail(emails, subjBuf.String(), msgBuf.String())
if err != nil { if err != nil {
return err return err
} }
@ -444,6 +450,14 @@ func main() {
tmpl, err := template.New("expiry-email").Parse(string(emailTmpl)) tmpl, err := template.New("expiry-email").Parse(string(emailTmpl))
cmd.FailOnError(err, "Could not parse email template") cmd.FailOnError(err, "Could not parse email template")
// If there is no configured subject template, use a default
if c.Mailer.Subject == "" {
c.Mailer.Subject = defaultExpirationSubject
}
// Load subject template
subjTmpl, err := template.New("expiry-email-subject").Parse(c.Mailer.Subject)
cmd.FailOnError(err, fmt.Sprintf("Could not parse email subject template"))
fromAddress, err := netmail.ParseAddress(c.Mailer.From) fromAddress, err := netmail.ParseAddress(c.Mailer.From)
cmd.FailOnError(err, fmt.Sprintf("Could not parse from address: %s", c.Mailer.From)) cmd.FailOnError(err, fmt.Sprintf("Could not parse from address: %s", c.Mailer.From))
@ -483,11 +497,11 @@ func main() {
m := mailer{ m := mailer{
stats: scope, stats: scope,
subject: c.Mailer.Subject,
log: logger, log: logger,
dbMap: dbMap, dbMap: dbMap,
rs: sac, rs: sac,
mailer: mailClient, mailer: mailClient,
subjectTemplate: subjTmpl,
emailTemplate: tmpl, emailTemplate: tmpl,
nagTimes: nags, nagTimes: nags,
limit: c.Mailer.CertLimit, limit: c.Mailer.CertLimit,

View File

@ -23,6 +23,7 @@ import (
"gopkg.in/square/go-jose.v1" "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/mocks" "github.com/letsencrypt/boulder/mocks"
@ -50,8 +51,7 @@ type fakeRegStore struct {
func (f fakeRegStore) GetRegistration(ctx context.Context, id int64) (core.Registration, error) { func (f fakeRegStore) GetRegistration(ctx context.Context, id int64) (core.Registration, error) {
r, ok := f.RegByID[id] r, ok := f.RegByID[id]
if !ok { if !ok {
msg := fmt.Sprintf("no such registration %d", id) return r, berrors.NotFoundError("no registration found for %q", id)
return r, core.NoSuchRegistrationError(msg)
} }
return r, nil return r, nil
} }
@ -96,6 +96,7 @@ var (
}`) }`)
log = blog.UseMock() log = blog.UseMock()
tmpl = template.Must(template.New("expiry-email").Parse(testTmpl)) tmpl = template.Must(template.New("expiry-email").Parse(testTmpl))
subjTmpl = template.Must(template.New("expiry-email-subject").Parse("Testing: " + defaultExpirationSubject))
ctx = context.Background() ctx = context.Background()
) )
@ -105,13 +106,15 @@ func TestSendNags(t *testing.T) {
rs := newFakeRegStore() rs := newFakeRegStore()
fc := newFakeClock(t) fc := newFakeClock(t)
staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
m := mailer{ m := mailer{
stats: stats, stats: stats,
log: log, log: log,
mailer: &mc, mailer: &mc,
emailTemplate: tmpl, emailTemplate: tmpl,
// Explicitly override the default subject to use testEmailSubject // Explicitly override the default subject to use testEmailSubject
subject: testEmailSubject, subjectTemplate: staticTmpl,
rs: rs, rs: rs,
clk: fc, clk: fc,
} }
@ -222,14 +225,14 @@ func TestFindExpiringCertificates(t *testing.T) {
To: emailARaw, To: emailARaw,
// A certificate with only one domain should have only one domain listed in // A certificate with only one domain should have only one domain listed in
// the subject // the subject
Subject: "Certificate expiration notice for domain \"example-a.com\"", Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\"",
Body: "hi, cert for DNS names example-a.com is going to expire in 0 days (03 Jan 06 14:04 +0000)", Body: "hi, cert for DNS names example-a.com is going to expire in 0 days (03 Jan 06 14:04 +0000)",
}, testCtx.mc.Messages[0]) }, testCtx.mc.Messages[0])
test.AssertEquals(t, mocks.MailerMessage{ test.AssertEquals(t, mocks.MailerMessage{
To: emailBRaw, To: emailBRaw,
// A certificate with two domains should have only one domain listed and an // A certificate with two domains should have only one domain listed and an
// additional count included // additional count included
Subject: "Certificate expiration notice for domain \"another.example-c.com\" (and 1 more)", Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"another.example-c.com\" (and 1 more)",
Body: "hi, cert for DNS names another.example-c.com\nexample-c.com is going to expire in 7 days (09 Jan 06 16:04 +0000)", Body: "hi, cert for DNS names another.example-c.com\nexample-c.com is going to expire in 7 days (09 Jan 06 16:04 +0000)",
}, testCtx.mc.Messages[1]) }, testCtx.mc.Messages[1])
@ -838,7 +841,7 @@ func TestDedupOnRegistration(t *testing.T) {
To: emailARaw, To: emailARaw,
// A certificate with three domain names should have one in the subject and // A certificate with three domain names should have one in the subject and
// a count of '2 more' at the end // a count of '2 more' at the end
Subject: "Certificate expiration notice for domain \"example-a.com\" (and 2 more)", Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\" (and 2 more)",
Body: fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 1 days (%s)`, Body: fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 1 days (%s)`,
domains, domains,
rawCertB.NotAfter.Format(time.RFC822Z)), rawCertB.NotAfter.Format(time.RFC822Z)),
@ -882,6 +885,7 @@ func setup(t *testing.T, nagTimes []time.Duration) *testCtx {
stats: stats, stats: stats,
mailer: mc, mailer: mc,
emailTemplate: tmpl, emailTemplate: tmpl,
subjectTemplate: subjTmpl,
dbMap: dbMap, dbMap: dbMap,
rs: ssa, rs: ssa,
nagTimes: offsetNags, nagTimes: offsetNags,

View File

@ -45,7 +45,7 @@ func TestSendEarliestCertInfo(t *testing.T) {
} }
domains := "example-a.com\nexample-b.com\nshared-example.com" domains := "example-a.com\nexample-b.com\nshared-example.com"
expected := mocks.MailerMessage{ expected := mocks.MailerMessage{
Subject: "Certificate expiration notice for domain \"example-a.com\" (and 2 more)", Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\" (and 2 more)",
Body: fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 2 days (%s)`, Body: fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 2 days (%s)`,
domains, domains,
rawCertB.NotAfter.Format(time.RFC822Z)), rawCertB.NotAfter.Format(time.RFC822Z)),

View File

@ -2,6 +2,7 @@ package main
import ( import (
"bufio" "bufio"
"database/sql"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
@ -45,19 +46,45 @@ type expiredAuthzPurger struct {
batchSize int64 batchSize int64
} }
func (p *expiredAuthzPurger) purgeAuthzs(purgeBefore time.Time, yes bool) (int64, error) { func (p *expiredAuthzPurger) purge(table string, yes bool, purgeBefore time.Time) error {
if !yes { var ids []string
var count int for {
err := p.db.SelectOne(&count, `SELECT COUNT(1) FROM pendingAuthorizations AS pa WHERE expires <= ?`, purgeBefore) var idBatch []string
if err != nil { var query string
return 0, err switch table {
case "pendingAuthorizations":
query = "SELECT id FROM pendingAuthorizations WHERE expires <= ? LIMIT ? OFFSET ?"
case "authz":
query = "SELECT id FROM authz WHERE expires <= ? LIMIT ? OFFSET ?"
} }
_, err := p.db.Select(
&idBatch,
query,
purgeBefore,
p.batchSize,
len(ids),
)
if err != nil && err != sql.ErrNoRows {
return err
}
if len(idBatch) == 0 {
break
}
ids = append(ids, idBatch...)
}
if !yes {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
fmt.Fprintf(os.Stdout, "\nAbout to purge %d pending authorizations, proceed? [y/N]: ", count) fmt.Fprintf(
os.Stdout,
"\nAbout to purge %d authorizations from %s and all associated challenges, proceed? [y/N]: ",
len(ids),
table,
)
text, err := reader.ReadString('\n') text, err := reader.ReadString('\n')
if err != nil { if err != nil {
return 0, err return err
} }
text = strings.ToLower(text) text = strings.ToLower(text)
if text != "y\n" && text != "n\n" && text != "\n" { if text != "y\n" && text != "n\n" && text != "\n" {
@ -71,33 +98,39 @@ func (p *expiredAuthzPurger) purgeAuthzs(purgeBefore time.Time, yes bool) (int64
} }
} }
rowsAffected := int64(0) for _, id := range ids {
for { // Delete challenges + authorization. We delete challenges first and fail out
result, err := p.db.Exec(` // if that doesn't succeed so that we don't ever orphan challenges which would
DELETE FROM pendingAuthorizations // require a relatively expensive join to then find.
WHERE expires <= ? _, err := p.db.Exec("DELETE FROM challenges WHERE authorizationID = ?", id)
LIMIT ?
`,
purgeBefore,
p.batchSize,
)
if err != nil { if err != nil {
return rowsAffected, err return err
} }
rows, err := result.RowsAffected() var query string
switch table {
case "pendingAuthorizations":
query = "DELETE FROM pendingAuthorizations WHERE id = ?"
case "authz":
query = "DELETE FROM authz WHERE id = ?"
}
_, err = p.db.Exec(query, id)
if err != nil { if err != nil {
return rowsAffected, err return err
}
} }
p.stats.Inc("PendingAuthzDeleted", rows) p.log.Info(fmt.Sprintf("Deleted a total of %d expired authorizations from %s", len(ids), table))
rowsAffected += rows return nil
p.log.Info(fmt.Sprintf("Progress: Deleted %d (%d total) expired pending authorizations", rows, rowsAffected)) }
if rows < p.batchSize { func (p *expiredAuthzPurger) purgeAuthzs(purgeBefore time.Time, yes bool) error {
p.log.Info(fmt.Sprintf("Deleted a total of %d expired pending authorizations", rowsAffected)) for _, table := range []string{"pendingAuthorizations", "authz"} {
return rowsAffected, nil err := p.purge(table, yes, purgeBefore)
if err != nil {
return err
} }
} }
return nil
} }
func main() { func main() {
@ -144,6 +177,6 @@ func main() {
os.Exit(1) os.Exit(1)
} }
purgeBefore := purger.clk.Now().Add(-config.ExpiredAuthzPurger.GracePeriod.Duration) purgeBefore := purger.clk.Now().Add(-config.ExpiredAuthzPurger.GracePeriod.Duration)
_, err = purger.purgeAuthzs(purgeBefore, *yes) err = purger.purgeAuthzs(purgeBefore, *yes)
cmd.FailOnError(err, "Failed to purge authorizations") cmd.FailOnError(err, "Failed to purge authorizations")
} }

View File

@ -34,24 +34,47 @@ func TestPurgeAuthzs(t *testing.T) {
p := expiredAuthzPurger{stats, log, fc, dbMap, 1} p := expiredAuthzPurger{stats, log, fc, dbMap, 1}
rows, err := p.purgeAuthzs(time.Time{}, true) err = p.purgeAuthzs(time.Time{}, true)
test.AssertNotError(t, err, "purgeAuthzs failed") test.AssertNotError(t, err, "purgeAuthzs failed")
test.AssertEquals(t, rows, int64(0))
old, new := fc.Now().Add(-time.Hour), fc.Now().Add(time.Hour) old, new := fc.Now().Add(-time.Hour), fc.Now().Add(time.Hour)
reg := satest.CreateWorkingRegistration(t, ssa) reg := satest.CreateWorkingRegistration(t, ssa)
_, err = ssa.NewPendingAuthorization(context.Background(), core.Authorization{RegistrationID: reg.ID, Expires: &old}) _, err = ssa.NewPendingAuthorization(context.Background(), core.Authorization{
RegistrationID: reg.ID,
Expires: &old,
Challenges: []core.Challenge{{ID: 1}},
})
test.AssertNotError(t, err, "NewPendingAuthorization failed") test.AssertNotError(t, err, "NewPendingAuthorization failed")
_, err = ssa.NewPendingAuthorization(context.Background(), core.Authorization{RegistrationID: reg.ID, Expires: &old}) _, err = ssa.NewPendingAuthorization(context.Background(), core.Authorization{
RegistrationID: reg.ID,
Expires: &old,
Challenges: []core.Challenge{{ID: 2}},
})
test.AssertNotError(t, err, "NewPendingAuthorization failed") test.AssertNotError(t, err, "NewPendingAuthorization failed")
_, err = ssa.NewPendingAuthorization(context.Background(), core.Authorization{RegistrationID: reg.ID, Expires: &new}) _, err = ssa.NewPendingAuthorization(context.Background(), core.Authorization{
RegistrationID: reg.ID,
Expires: &new,
Challenges: []core.Challenge{{ID: 3}},
})
test.AssertNotError(t, err, "NewPendingAuthorization failed") test.AssertNotError(t, err, "NewPendingAuthorization failed")
rows, err = p.purgeAuthzs(fc.Now(), true) err = p.purgeAuthzs(fc.Now(), true)
test.AssertNotError(t, err, "purgeAuthzs failed") test.AssertNotError(t, err, "purgeAuthzs failed")
test.AssertEquals(t, rows, int64(2)) count, err := dbMap.SelectInt("SELECT COUNT(1) FROM pendingAuthorizations")
rows, err = p.purgeAuthzs(fc.Now().Add(time.Hour), true) test.AssertNotError(t, err, "dbMap.SelectInt failed")
test.AssertEquals(t, count, int64(1))
count, err = dbMap.SelectInt("SELECT COUNT(1) FROM challenges")
test.AssertNotError(t, err, "dbMap.SelectInt failed")
test.AssertEquals(t, count, int64(1))
err = p.purgeAuthzs(fc.Now().Add(time.Hour), true)
test.AssertNotError(t, err, "purgeAuthzs failed") test.AssertNotError(t, err, "purgeAuthzs failed")
test.AssertEquals(t, rows, int64(1)) count, err = dbMap.SelectInt("SELECT COUNT(1) FROM pendingAuthorizations")
test.AssertNotError(t, err, "dbMap.SelectInt failed")
test.AssertEquals(t, count, int64(0))
count, err = dbMap.SelectInt("SELECT COUNT(1) FROM challenges")
test.AssertNotError(t, err, "dbMap.SelectInt failed")
test.AssertEquals(t, count, int64(0))
} }

View File

@ -17,6 +17,7 @@ import (
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc" bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
@ -68,7 +69,9 @@ func checkDER(sai certificateStorage, der []byte) error {
if err == nil { if err == nil {
return errAlreadyExists return errAlreadyExists
} }
if _, ok := err.(core.NotFoundError); ok { // TODO(#2600): Remove core.NotFoundError check once boulder/errors
// code is deployed
if _, ok := err.(core.NotFoundError); ok || berrors.Is(err, berrors.NotFound) {
return nil return nil
} }
return fmt.Errorf("Existing certificate lookup failed: %s", err) return fmt.Errorf("Existing certificate lookup failed: %s", err)

View File

@ -7,8 +7,9 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
) )
@ -28,7 +29,7 @@ func (m *mockSA) GetCertificate(ctx context.Context, s string) (core.Certificate
if m.certificate.DER != nil { if m.certificate.DER != nil {
return m.certificate, nil return m.certificate, nil
} }
return core.Certificate{}, core.NotFoundError("no cert stored") return core.Certificate{}, berrors.NotFoundError("no cert stored")
} }
func checkNoErrors(t *testing.T) { func checkNoErrors(t *testing.T) {

View File

@ -13,12 +13,17 @@ func HTTPChallenge01() Challenge {
return newChallenge(ChallengeTypeHTTP01) return newChallenge(ChallengeTypeHTTP01)
} }
// TLSSNIChallenge01 constructs a random tls-sni-00 challenge // TLSSNIChallenge01 constructs a random tls-sni-01 challenge
func TLSSNIChallenge01() Challenge { func TLSSNIChallenge01() Challenge {
return newChallenge(ChallengeTypeTLSSNI01) return newChallenge(ChallengeTypeTLSSNI01)
} }
// DNSChallenge01 constructs a random DNS challenge // TLSSNIChallenge02 constructs a random tls-sni-02 challenge
func TLSSNIChallenge02() Challenge {
return newChallenge(ChallengeTypeTLSSNI02)
}
// DNSChallenge01 constructs a random dns-01 challenge
func DNSChallenge01() Challenge { func DNSChallenge01() Challenge {
return newChallenge(ChallengeTypeDNS01) return newChallenge(ChallengeTypeDNS01)
} }

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"testing" "testing"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
"gopkg.in/square/go-jose.v1" "gopkg.in/square/go-jose.v1"
) )
@ -34,6 +35,11 @@ func TestChallenges(t *testing.T) {
t.Errorf("New tls-sni-01 challenge is not sane: %v", tlssni01) t.Errorf("New tls-sni-01 challenge is not sane: %v", tlssni01)
} }
tlssni02 := TLSSNIChallenge02()
if !tlssni02.IsSane(false) {
t.Errorf("New tls-sni-02 challenge is not sane: %v", tlssni02)
}
dns01 := DNSChallenge01() dns01 := DNSChallenge01()
if !dns01.IsSane(false) { if !dns01.IsSane(false) {
t.Errorf("New dns-01 challenge is not sane: %v", dns01) t.Errorf("New dns-01 challenge is not sane: %v", dns01)
@ -43,6 +49,13 @@ func TestChallenges(t *testing.T) {
test.Assert(t, ValidChallenge(ChallengeTypeTLSSNI01), "Refused valid challenge") test.Assert(t, ValidChallenge(ChallengeTypeTLSSNI01), "Refused valid challenge")
test.Assert(t, ValidChallenge(ChallengeTypeDNS01), "Refused valid challenge") test.Assert(t, ValidChallenge(ChallengeTypeDNS01), "Refused valid challenge")
test.Assert(t, !ValidChallenge("nonsense-71"), "Accepted invalid challenge") test.Assert(t, !ValidChallenge("nonsense-71"), "Accepted invalid challenge")
test.Assert(t, !ValidChallenge(ChallengeTypeTLSSNI02), "Accepted invalid challenge")
_ = features.Set(map[string]bool{"AllowTLS02Challenges": true})
defer features.Reset()
test.Assert(t, ValidChallenge(ChallengeTypeTLSSNI02), "Refused valid challenge")
} }
// objects.go // objects.go

View File

@ -12,6 +12,7 @@ import (
"gopkg.in/square/go-jose.v1" "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/revocation"
) )
@ -69,6 +70,7 @@ const (
const ( const (
ChallengeTypeHTTP01 = "http-01" ChallengeTypeHTTP01 = "http-01"
ChallengeTypeTLSSNI01 = "tls-sni-01" ChallengeTypeTLSSNI01 = "tls-sni-01"
ChallengeTypeTLSSNI02 = "tls-sni-02"
ChallengeTypeDNS01 = "dns-01" ChallengeTypeDNS01 = "dns-01"
) )
@ -81,6 +83,8 @@ func ValidChallenge(name string) bool {
fallthrough fallthrough
case ChallengeTypeDNS01: case ChallengeTypeDNS01:
return true return true
case ChallengeTypeTLSSNI02:
return features.Enabled(features.AllowTLS02Challenges)
default: default:
return false return false
@ -261,6 +265,8 @@ func (ch Challenge) RecordsSane() bool {
} }
} }
case ChallengeTypeTLSSNI01: case ChallengeTypeTLSSNI01:
fallthrough
case ChallengeTypeTLSSNI02:
if len(ch.ValidationRecord) > 1 { if len(ch.ValidationRecord) > 1 {
return false return false
} }

View File

@ -57,7 +57,7 @@ func TestChallengeSanityCheck(t *testing.T) {
}`), &accountKey) }`), &accountKey)
test.AssertNotError(t, err, "Error unmarshaling JWK") test.AssertNotError(t, err, "Error unmarshaling JWK")
types := []string{ChallengeTypeHTTP01, ChallengeTypeTLSSNI01, ChallengeTypeDNS01} types := []string{ChallengeTypeHTTP01, ChallengeTypeTLSSNI01, ChallengeTypeTLSSNI02, ChallengeTypeDNS01}
for _, challengeType := range types { for _, challengeType := range types {
chall := Challenge{ chall := Challenge{
Type: challengeType, Type: challengeType,

View File

@ -16,16 +16,15 @@ import (
"io/ioutil" "io/ioutil"
"math/big" "math/big"
mrand "math/rand" mrand "math/rand"
"net/http"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
"unicode" "unicode"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/probs"
jose "gopkg.in/square/go-jose.v1" jose "gopkg.in/square/go-jose.v1"
blog "github.com/letsencrypt/boulder/log"
) )
// Package Variables Variables // Package Variables Variables
@ -100,47 +99,6 @@ func (e RateLimitedError) Error() string { return string(e) }
func (e TooManyRPCRequestsError) Error() string { return string(e) } func (e TooManyRPCRequestsError) Error() string { return string(e) }
func (e BadNonceError) Error() string { return string(e) } func (e BadNonceError) Error() string { return string(e) }
// statusTooManyRequests is the HTTP status code meant for rate limiting
// errors. It's not currently in the net/http library so we add it here.
const statusTooManyRequests = 429
// 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 MalformedRequestError:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
case NotSupportedError:
return &probs.ProblemDetails{
Type: probs.ServerInternalProblem,
Detail: fmt.Sprintf("%s :: %s", msg, err),
HTTPStatus: http.StatusNotImplemented,
}
case UnauthorizedError:
return probs.Unauthorized(fmt.Sprintf("%s :: %s", msg, err))
case NotFoundError:
return probs.NotFound(fmt.Sprintf("%s :: %s", msg, err))
case LengthRequiredError:
prob := probs.Malformed("missing Content-Length header")
prob.HTTPStatus = http.StatusLengthRequired
return prob
case SignatureValidationError:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
case RateLimitedError:
return probs.RateLimited(fmt.Sprintf("%s :: %s", msg, err))
case 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)
}
}
// Random stuff // Random stuff
// RandomString returns a randomly generated string of the requested length. // RandomString returns a randomly generated string of the requested length.

View File

@ -5,13 +5,11 @@ import (
"fmt" "fmt"
"math" "math"
"math/big" "math/big"
"reflect"
"sort" "sort"
"testing" "testing"
"gopkg.in/square/go-jose.v1" "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
) )
@ -110,38 +108,3 @@ func TestUniqueLowerNames(t *testing.T) {
sort.Strings(u) sort.Strings(u)
test.AssertDeepEquals(t, []string{"a.com", "bar.com", "baz.com", "foobar.com"}, u) test.AssertDeepEquals(t, []string{"a.com", "bar.com", "baz.com", "foobar.com"}, u)
} }
func TestProblemDetailsFromError(t *testing.T) {
testCases := []struct {
err error
statusCode int
problem probs.ProblemType
}{
{InternalServerError("foo"), 500, probs.ServerInternalProblem},
{NotSupportedError("foo"), 501, probs.ServerInternalProblem},
{MalformedRequestError("foo"), 400, probs.MalformedProblem},
{UnauthorizedError("foo"), 403, probs.UnauthorizedProblem},
{NotFoundError("foo"), 404, probs.MalformedProblem},
{SignatureValidationError("foo"), 400, probs.MalformedProblem},
{RateLimitedError("foo"), 429, probs.RateLimitedProblem},
{LengthRequiredError("foo"), 411, probs.MalformedProblem},
{BadNonceError("foo"), 400, probs.BadNonceProblem},
}
for _, c := range testCases {
p := ProblemDetailsForError(c.err, "k")
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)
}
}
expected := &probs.ProblemDetails{
Type: probs.MalformedProblem,
HTTPStatus: 200,
Detail: "gotcha",
}
p := ProblemDetailsForError(expected, "k")
test.AssertDeepEquals(t, expected, p)
}

11
docs/error-handling.md Normal file
View File

@ -0,0 +1,11 @@
# Error Handling Guidance
Previously Boulder has used a mix of various error types to represent errors internally, mainly the `core.XXXError` types and `probs.ProblemDetails`, without any guidance on which should be used when or where.
We have switched away from this to using a single unified internal error type, `boulder/errors.BoulderError` which should be used anywhere we need to pass errors between components and need to be able to indicate and test the type of the error that was passed. `probs.ProblemDetails` should only be used in the WFE when creating a problem document to pass directly back to the user client.
A mapping exists in the WFE to map all of the available `boulder/errors.ErrorType`s to the relevant `probs.ProblemType`s. Internally errors should be wrapped when doing so provides some further context to the error that aides in debugging or will be passed back to the user client. An error may be unwrapped, or a simple stdlib `error` may be used, but doing so means the `probs.ProblemType` mapping will always be `probs.ServerInternalProblem` so should only be used for errors that do not need to be presented back to the user client.
`boulder/errors.BoulderError`s have two components: an internal type, `boulder/errors.ErrorType`, and a detail string. The internal type should be used for a. allowing the receiver to determine what caused the error, e.g. by using `boulder/errors.NotFound` to indicate a DB operation couldn't find the requested resource, and b. allowing the WFE to convert the error to the relevant `probs.ProblemType` for display to the user. The detail string should provide a user readable explanation of the issue to be presented to the user; the only exception to this is when the internal type is `boulder/errors.InternalServer` in which case the detail of the error will be stripped by the WFE and the only message presented to the user will be provided by the caller in the WFE.
Error type testing should be done with `boulder/errors.Is` instead of locally doing a type cast test.

96
errors/errors.go Normal file
View File

@ -0,0 +1,96 @@
package errors
import "fmt"
// ErrorType provides a coarse category for BoulderErrors
type ErrorType int
const (
InternalServer ErrorType = iota
NotSupported
Malformed
Unauthorized
NotFound
SignatureValidation
RateLimit
TooManyRequests
RejectedIdentifier
UnsupportedIdentifier
InvalidEmail
ConnectionFailure
)
// BoulderError represents internal Boulder errors
type BoulderError struct {
Type ErrorType
Detail string
}
func (be *BoulderError) Error() string {
return be.Detail
}
// New is a convenience function for creating a new BoulderError
func New(errType ErrorType, msg string, args ...interface{}) error {
return &BoulderError{
Type: errType,
Detail: fmt.Sprintf(msg, args...),
}
}
// Is is a convenience function for testing the internal type of an BoulderError
func Is(err error, errType ErrorType) bool {
bErr, ok := err.(*BoulderError)
if !ok {
return false
}
return bErr.Type == errType
}
func InternalServerError(msg string, args ...interface{}) error {
return New(InternalServer, msg, args...)
}
func NotSupportedError(msg string, args ...interface{}) error {
return New(NotSupported, msg, args...)
}
func MalformedError(msg string, args ...interface{}) error {
return New(Malformed, msg, args...)
}
func UnauthorizedError(msg string, args ...interface{}) error {
return New(Unauthorized, msg, args...)
}
func NotFoundError(msg string, args ...interface{}) error {
return New(NotFound, msg, args...)
}
func SignatureValidationError(msg string, args ...interface{}) error {
return New(SignatureValidation, msg, args...)
}
func RateLimitError(msg string, args ...interface{}) error {
return New(RateLimit, msg, args...)
}
func TooManyRequestsError(msg string, args ...interface{}) error {
return New(TooManyRequests, msg, args...)
}
func RejectedIdentifierError(msg string, args ...interface{}) error {
return New(RejectedIdentifier, msg, args...)
}
func UnsupportedIdentifierError(msg string, args ...interface{}) error {
return New(UnsupportedIdentifier, msg, args...)
}
func InvalidEmailError(msg string, args ...interface{}) error {
return New(InvalidEmail, msg, args...)
}
func ConnectionFailureError(msg string, args ...interface{}) error {
return New(ConnectionFailure, msg, args...)
}

View File

@ -4,9 +4,9 @@ package features
import "fmt" import "fmt"
const _FeatureFlag_name = "unusedIDNASupportAllowAccountDeactivationAllowKeyRolloverResubmitMissingSCTsOnlyGoogleSafeBrowsingV4UseAIAIssuerURL" const _FeatureFlag_name = "unusedIDNASupportAllowAccountDeactivationAllowKeyRolloverResubmitMissingSCTsOnlyGoogleSafeBrowsingV4UseAIAIssuerURLAllowTLS02Challenges"
var _FeatureFlag_index = [...]uint8{0, 6, 17, 41, 57, 80, 100, 115} var _FeatureFlag_index = [...]uint8{0, 6, 17, 41, 57, 80, 100, 115, 135}
func (i FeatureFlag) String() string { func (i FeatureFlag) String() string {
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) { if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {

View File

@ -18,6 +18,7 @@ const (
ResubmitMissingSCTsOnly ResubmitMissingSCTsOnly
GoogleSafeBrowsingV4 GoogleSafeBrowsingV4
UseAIAIssuerURL UseAIAIssuerURL
AllowTLS02Challenges
) )
// List of features and their default value, protected by fMu // List of features and their default value, protected by fMu
@ -29,6 +30,7 @@ var features = map[FeatureFlag]bool{
ResubmitMissingSCTsOnly: false, ResubmitMissingSCTsOnly: false,
GoogleSafeBrowsingV4: false, GoogleSafeBrowsingV4: false,
UseAIAIssuerURL: false, UseAIAIssuerURL: false,
AllowTLS02Challenges: false,
} }
var fMu = new(sync.RWMutex) var fMu = new(sync.RWMutex)

View File

@ -5,12 +5,11 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rsa" "crypto/rsa"
"fmt"
"math/big" "math/big"
"reflect" "reflect"
"sync" "sync"
"github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors"
) )
// To generate, run: primes 2 752 | tr '\n' , // To generate, run: primes 2 752 | tr '\n' ,
@ -67,7 +66,7 @@ func (policy *KeyPolicy) GoodKey(key crypto.PublicKey) error {
case *ecdsa.PublicKey: case *ecdsa.PublicKey:
return policy.goodKeyECDSA(*t) return policy.goodKeyECDSA(*t)
default: default:
return core.MalformedRequestError(fmt.Sprintf("Unknown key type %s", reflect.TypeOf(key))) return berrors.MalformedError("unknown key type %s", reflect.TypeOf(key))
} }
} }
@ -97,7 +96,7 @@ func (policy *KeyPolicy) goodKeyECDSA(key ecdsa.PublicKey) (err error) {
// This code assumes that the point at infinity is (0,0), which is the // This code assumes that the point at infinity is (0,0), which is the
// case for all supported curves. // case for all supported curves.
if isPointAtInfinityNISTP(key.X, key.Y) { if isPointAtInfinityNISTP(key.X, key.Y) {
return core.MalformedRequestError("Key x, y must not be the point at infinity") return berrors.MalformedError("key x, y must not be the point at infinity")
} }
// SP800-56A § 5.6.2.3.2 Step 2. // SP800-56A § 5.6.2.3.2 Step 2.
@ -114,11 +113,11 @@ func (policy *KeyPolicy) goodKeyECDSA(key ecdsa.PublicKey) (err error) {
// correct representation of an element in the underlying field by verifying // correct representation of an element in the underlying field by verifying
// that x and y are integers in [0, p-1]. // that x and y are integers in [0, p-1].
if key.X.Sign() < 0 || key.Y.Sign() < 0 { if key.X.Sign() < 0 || key.Y.Sign() < 0 {
return core.MalformedRequestError("Key x, y must not be negative") return berrors.MalformedError("key x, y must not be negative")
} }
if key.X.Cmp(params.P) >= 0 || key.Y.Cmp(params.P) >= 0 { if key.X.Cmp(params.P) >= 0 || key.Y.Cmp(params.P) >= 0 {
return core.MalformedRequestError("Key x, y must not exceed P-1") return berrors.MalformedError("key x, y must not exceed P-1")
} }
// SP800-56A § 5.6.2.3.2 Step 3. // SP800-56A § 5.6.2.3.2 Step 3.
@ -136,7 +135,7 @@ func (policy *KeyPolicy) goodKeyECDSA(key ecdsa.PublicKey) (err error) {
// This proves that the public key is on the correct elliptic curve. // This proves that the public key is on the correct elliptic curve.
// But in practice, this test is provided by crypto/elliptic, so use that. // But in practice, this test is provided by crypto/elliptic, so use that.
if !key.Curve.IsOnCurve(key.X, key.Y) { if !key.Curve.IsOnCurve(key.X, key.Y) {
return core.MalformedRequestError("Key point is not on the curve") return berrors.MalformedError("key point is not on the curve")
} }
// SP800-56A § 5.6.2.3.2 Step 4. // SP800-56A § 5.6.2.3.2 Step 4.
@ -152,7 +151,7 @@ func (policy *KeyPolicy) goodKeyECDSA(key ecdsa.PublicKey) (err error) {
// n*Q = O iff n*Q is the point at infinity (see step 1). // n*Q = O iff n*Q is the point at infinity (see step 1).
ox, oy := key.Curve.ScalarMult(key.X, key.Y, params.N.Bytes()) ox, oy := key.Curve.ScalarMult(key.X, key.Y, params.N.Bytes())
if !isPointAtInfinityNISTP(ox, oy) { if !isPointAtInfinityNISTP(ox, oy) {
return core.MalformedRequestError("Public key does not have correct order") return berrors.MalformedError("public key does not have correct order")
} }
// End of SP800-56A § 5.6.2.3.2 Public Key Validation Routine. // End of SP800-56A § 5.6.2.3.2 Public Key Validation Routine.
@ -178,14 +177,14 @@ func (policy *KeyPolicy) goodCurve(c elliptic.Curve) (err error) {
case policy.AllowECDSANISTP384 && params == elliptic.P384().Params(): case policy.AllowECDSANISTP384 && params == elliptic.P384().Params():
return nil return nil
default: default:
return core.MalformedRequestError(fmt.Sprintf("ECDSA curve %v not allowed", params.Name)) return berrors.MalformedError("ECDSA curve %v not allowed", params.Name)
} }
} }
// GoodKeyRSA determines if a RSA pubkey meets our requirements // GoodKeyRSA determines if a RSA pubkey meets our requirements
func (policy *KeyPolicy) goodKeyRSA(key rsa.PublicKey) (err error) { func (policy *KeyPolicy) goodKeyRSA(key rsa.PublicKey) (err error) {
if !policy.AllowRSA { if !policy.AllowRSA {
return core.MalformedRequestError("RSA keys are not allowed") return berrors.MalformedError("RSA keys are not allowed")
} }
// Baseline Requirements Appendix A // Baseline Requirements Appendix A
@ -194,15 +193,15 @@ func (policy *KeyPolicy) goodKeyRSA(key rsa.PublicKey) (err error) {
modulusBitLen := modulus.BitLen() modulusBitLen := modulus.BitLen()
const maxKeySize = 4096 const maxKeySize = 4096
if modulusBitLen < 2048 { if modulusBitLen < 2048 {
return core.MalformedRequestError(fmt.Sprintf("Key too small: %d", modulusBitLen)) return berrors.MalformedError("key too small: %d", modulusBitLen)
} }
if modulusBitLen > maxKeySize { if modulusBitLen > maxKeySize {
return core.MalformedRequestError(fmt.Sprintf("Key too large: %d > %d", modulusBitLen, maxKeySize)) return berrors.MalformedError("key too large: %d > %d", modulusBitLen, maxKeySize)
} }
// Bit lengths that are not a multiple of 8 may cause problems on some // Bit lengths that are not a multiple of 8 may cause problems on some
// client implementations. // client implementations.
if modulusBitLen%8 != 0 { if modulusBitLen%8 != 0 {
return core.MalformedRequestError(fmt.Sprintf("Key length wasn't a multiple of 8: %d", modulusBitLen)) return berrors.MalformedError("key length wasn't a multiple of 8: %d", modulusBitLen)
} }
// The CA SHALL confirm that the value of the public exponent is an // The CA SHALL confirm that the value of the public exponent is an
// odd number equal to 3 or more. Additionally, the public exponent // odd number equal to 3 or more. Additionally, the public exponent
@ -211,13 +210,13 @@ func (policy *KeyPolicy) goodKeyRSA(key rsa.PublicKey) (err error) {
// 2^32 - 1 or 2^64 - 1, because it stores E as an integer. So we // 2^32 - 1 or 2^64 - 1, because it stores E as an integer. So we
// don't need to check the upper bound. // don't need to check the upper bound.
if (key.E%2) == 0 || key.E < ((1<<16)+1) { if (key.E%2) == 0 || key.E < ((1<<16)+1) {
return core.MalformedRequestError(fmt.Sprintf("Key exponent should be odd and >2^16: %d", key.E)) return berrors.MalformedError("key exponent should be odd and >2^16: %d", key.E)
} }
// The modulus SHOULD also have the following characteristics: an odd // The modulus SHOULD also have the following characteristics: an odd
// number, not the power of a prime, and have no factors smaller than 752. // number, not the power of a prime, and have no factors smaller than 752.
// TODO: We don't yet check for "power of a prime." // TODO: We don't yet check for "power of a prime."
if checkSmallPrimes(modulus) { if checkSmallPrimes(modulus) {
return core.MalformedRequestError("Key divisible by small prime") return berrors.MalformedError("key divisible by small prime")
} }
return nil return nil

View File

@ -3,17 +3,22 @@ package grpc
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"strconv"
"golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/probs"
) )
// gRPC error codes used by Boulder. While the gRPC codes // gRPC error codes used by Boulder. While the gRPC codes
// end at 16 we start at 100 to provide a little leeway // end at 16 we start at 100 to provide a little leeway
// in case they ever decide to add more // in case they ever decide to add more
// TODO(#2507): Deprecated, remove once boulder/errors code is deployed
const ( const (
MalformedRequestError = iota + 100 MalformedRequestError = iota + 100
NotSupportedError NotSupportedError
@ -62,10 +67,25 @@ func errorToCode(err error) codes.Code {
} }
} }
func wrapError(err error) error { // wrapError wraps the internal error types we use for transport across the gRPC
// layer and appends an appropriate errortype to the gRPC trailer via the provided
// context. core.XXXError and probs.ProblemDetails error types are encoded using the gRPC
// error status code which has been deprecated (#2507). errors.BoulderError error types
// are encoded using the grpc/metadata in the context.Context for the RPC which is
// considered to be the 'proper' method of encoding custom error types (grpc/grpc#4543
// and grpc/grpc-go#478)
func wrapError(ctx context.Context, err error) error {
if err == nil { if err == nil {
return nil return nil
} }
if berr, ok := err.(*berrors.BoulderError); ok {
// Ignoring the error return here is safe because if setting the metadata
// fails, we'll still return an error, but it will be interpreted on the
// other side as an InternalServerError instead of a more specific one.
_ = grpc.SetTrailer(ctx, metadata.Pairs("errortype", strconv.Itoa(int(berr.Type))))
return grpc.Errorf(codes.Unknown, err.Error())
}
// TODO(2589): deprecated, remove once boulder/errors code has been deployed
code := errorToCode(err) code := errorToCode(err)
var body string var body string
if code == ProblemDetails { if code == ProblemDetails {
@ -83,10 +103,34 @@ func wrapError(err error) error {
return grpc.Errorf(code, body) return grpc.Errorf(code, body)
} }
func unwrapError(err error) error { // unwrapError unwraps errors returned from gRPC client calls which were wrapped
// with wrapError to their proper internal error type. If the provided metadata
// object has an "errortype" field, that will be used to set the type of the
// error. If the error is a core.XXXError or a probs.ProblemDetails the type
// is determined using the gRPC error code which has been deprecated (#2507).
func unwrapError(err error, md metadata.MD) error {
if err == nil { if err == nil {
return nil return nil
} }
if errTypeStrs, ok := md["errortype"]; ok {
unwrappedErr := grpc.ErrorDesc(err)
if len(errTypeStrs) != 1 {
return berrors.InternalServerError(
"multiple errorType metadata, wrapped error %q",
unwrappedErr,
)
}
errType, decErr := strconv.Atoi(errTypeStrs[0])
if decErr != nil {
return berrors.InternalServerError(
"failed to decode error type, decoding error %q, wrapped error %q",
decErr,
unwrappedErr,
)
}
return berrors.New(berrors.ErrorType(errType), unwrappedErr)
}
// TODO(2589): deprecated, remove once boulder/errors code has been deployed
code := grpc.Code(err) code := grpc.Code(err)
errBody := grpc.ErrorDesc(err) errBody := grpc.ErrorDesc(err)
switch code { switch code {

View File

@ -30,11 +30,11 @@ func TestErrors(t *testing.T) {
} }
for _, tc := range testcases { for _, tc := range testcases {
wrappedErr := wrapError(tc.err) wrappedErr := wrapError(nil, tc.err)
test.AssertEquals(t, grpc.Code(wrappedErr), tc.expectedCode) test.AssertEquals(t, grpc.Code(wrappedErr), tc.expectedCode)
test.AssertDeepEquals(t, tc.err, unwrapError(wrappedErr)) test.AssertDeepEquals(t, tc.err, unwrapError(wrappedErr, nil))
} }
test.AssertEquals(t, wrapError(nil), nil) test.AssertEquals(t, wrapError(nil, nil), nil)
test.AssertEquals(t, unwrapError(nil), nil) test.AssertEquals(t, unwrapError(nil, nil), nil)
} }

View File

@ -4,12 +4,16 @@ import (
"fmt" "fmt"
"net" "net"
"testing" "testing"
"time"
"github.com/jmhodges/clock"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
testproto "github.com/letsencrypt/boulder/grpc/test_proto" testproto "github.com/letsencrypt/boulder/grpc/test_proto"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
) )
@ -19,11 +23,15 @@ type errorServer struct {
} }
func (s *errorServer) Chill(_ context.Context, _ *testproto.Time) (*testproto.Time, error) { func (s *errorServer) Chill(_ context.Context, _ *testproto.Time) (*testproto.Time, error) {
return nil, wrapError(s.err) return nil, s.err
} }
func TestErrorWrapping(t *testing.T) { func TestErrorWrapping(t *testing.T) {
srv := grpc.NewServer() fc := clock.NewFake()
stats := metrics.NewNoopScope()
si := serverInterceptor{stats, fc}
ci := clientInterceptor{stats, fc, time.Second}
srv := grpc.NewServer(grpc.UnaryInterceptor(si.intercept))
es := &errorServer{} es := &errorServer{}
testproto.RegisterChillerServer(srv, es) testproto.RegisterChillerServer(srv, es)
lis, err := net.Listen("tcp", ":") lis, err := net.Listen("tcp", ":")
@ -34,6 +42,7 @@ func TestErrorWrapping(t *testing.T) {
conn, err := grpc.Dial( conn, err := grpc.Dial(
lis.Addr().String(), lis.Addr().String(),
grpc.WithInsecure(), grpc.WithInsecure(),
grpc.WithUnaryInterceptor(ci.intercept),
) )
test.AssertNotError(t, err, "Failed to dial grpc test server") test.AssertNotError(t, err, "Failed to dial grpc test server")
client := testproto.NewChillerClient(conn) client := testproto.NewChillerClient(conn)
@ -41,10 +50,11 @@ func TestErrorWrapping(t *testing.T) {
for _, tc := range []error{ for _, tc := range []error{
core.MalformedRequestError("yup"), core.MalformedRequestError("yup"),
&probs.ProblemDetails{Type: probs.MalformedProblem, Detail: "yup"}, &probs.ProblemDetails{Type: probs.MalformedProblem, Detail: "yup"},
berrors.MalformedError("yup"),
} { } {
es.err = tc es.err = tc
_, err := client.Chill(context.Background(), &testproto.Time{}) _, err := client.Chill(context.Background(), &testproto.Time{})
test.Assert(t, err != nil, fmt.Sprintf("nil error returned, expected: %s", err)) test.Assert(t, err != nil, fmt.Sprintf("nil error returned, expected: %s", err))
test.AssertDeepEquals(t, unwrapError(err), tc) test.AssertDeepEquals(t, err, tc)
} }
} }

View File

@ -1,7 +1,6 @@
package grpc package grpc
import ( import (
"errors"
"strings" "strings"
"time" "time"
@ -9,7 +8,9 @@ import (
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/metadata"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics"
) )
@ -36,7 +37,7 @@ func cleanMethod(m string, trimService bool) string {
func (si *serverInterceptor) intercept(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { func (si *serverInterceptor) intercept(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if info == nil { if info == nil {
si.stats.Inc("NoInfo", 1) si.stats.Inc("NoInfo", 1)
return nil, errors.New("passed nil *grpc.UnaryServerInfo") return nil, berrors.InternalServerError("passed nil *grpc.UnaryServerInfo")
} }
s := si.clk.Now() s := si.clk.Now()
methodScope := si.stats.NewScope(cleanMethod(info.FullMethod, true)) methodScope := si.stats.NewScope(cleanMethod(info.FullMethod, true))
@ -47,7 +48,7 @@ func (si *serverInterceptor) intercept(ctx context.Context, req interface{}, inf
methodScope.GaugeDelta("InProgress", -1) methodScope.GaugeDelta("InProgress", -1)
if err != nil { if err != nil {
methodScope.Inc("Failed", 1) methodScope.Inc("Failed", 1)
err = wrapError(err) err = wrapError(ctx, err)
} }
return resp, err return resp, err
} }
@ -84,12 +85,15 @@ func (ci *clientInterceptor) intercept(
// Disable fail-fast so RPCs will retry until deadline, even if all backends // Disable fail-fast so RPCs will retry until deadline, even if all backends
// are down. // are down.
opts = append(opts, grpc.FailFast(false)) opts = append(opts, grpc.FailFast(false))
// Create grpc/metadata.Metadata to encode internal error type if one is returned
md := metadata.New(nil)
opts = append(opts, grpc.Trailer(&md))
err := grpc_prometheus.UnaryClientInterceptor(localCtx, method, req, reply, cc, invoker, opts...) err := grpc_prometheus.UnaryClientInterceptor(localCtx, method, req, reply, cc, invoker, opts...)
methodScope.TimingDuration("Latency", ci.clk.Since(s)) methodScope.TimingDuration("Latency", ci.clk.Since(s))
methodScope.GaugeDelta("InProgress", -1) methodScope.GaugeDelta("InProgress", -1)
if err != nil { if err != nil {
methodScope.Inc("Failed", 1) methodScope.Inc("Failed", 1)
err = unwrapError(err) err = unwrapError(err, md)
} }
return err return err
} }

View File

@ -19,6 +19,7 @@ import (
"gopkg.in/square/go-jose.v1" "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/revocation"
) )
@ -145,12 +146,12 @@ func (sa *StorageAuthority) GetRegistrationByKey(_ context.Context, jwk *jose.Js
if core.KeyDigestEquals(jwk, test2KeyPublic) { if core.KeyDigestEquals(jwk, test2KeyPublic) {
// No key found // No key found
return core.Registration{ID: 2}, core.NoSuchRegistrationError("reg not found") return core.Registration{ID: 2}, berrors.NotFoundError("reg not found")
} }
if core.KeyDigestEquals(jwk, test4KeyPublic) { if core.KeyDigestEquals(jwk, test4KeyPublic) {
// No key found // No key found
return core.Registration{ID: 5}, core.NoSuchRegistrationError("reg not found") return core.Registration{ID: 5}, berrors.NotFoundError("reg not found")
} }
if core.KeyDigestEquals(jwk, testE1KeyPublic) { if core.KeyDigestEquals(jwk, testE1KeyPublic) {
@ -158,7 +159,7 @@ func (sa *StorageAuthority) GetRegistrationByKey(_ context.Context, jwk *jose.Js
} }
if core.KeyDigestEquals(jwk, testE2KeyPublic) { if core.KeyDigestEquals(jwk, testE2KeyPublic) {
return core.Registration{ID: 4}, core.NoSuchRegistrationError("reg not found") return core.Registration{ID: 4}, berrors.NotFoundError("reg not found")
} }
if core.KeyDigestEquals(jwk, test3KeyPublic) { if core.KeyDigestEquals(jwk, test3KeyPublic) {

View File

@ -15,9 +15,9 @@ import (
"golang.org/x/net/idna" "golang.org/x/net/idna"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/reloader" "github.com/letsencrypt/boulder/reloader"
) )
@ -127,22 +127,22 @@ func suffixMatch(labels []string, suffixSet map[string]bool, properSuffix bool)
} }
var ( var (
errInvalidIdentifier = probs.Malformed("Invalid identifier type") errInvalidIdentifier = berrors.MalformedError("Invalid identifier type")
errNonPublic = probs.Malformed("Name does not end in a public suffix") errNonPublic = berrors.MalformedError("Name does not end in a public suffix")
errICANNTLD = probs.Malformed("Name is an ICANN TLD") errICANNTLD = berrors.MalformedError("Name is an ICANN TLD")
errBlacklisted = probs.RejectedIdentifier("Policy forbids issuing for name") errBlacklisted = berrors.RejectedIdentifierError("Policy forbids issuing for name")
errNotWhitelisted = probs.Malformed("Name is not whitelisted") errNotWhitelisted = berrors.MalformedError("Name is not whitelisted")
errInvalidDNSCharacter = probs.Malformed("Invalid character in DNS name") errInvalidDNSCharacter = berrors.MalformedError("Invalid character in DNS name")
errNameTooLong = probs.Malformed("DNS name too long") errNameTooLong = berrors.MalformedError("DNS name too long")
errIPAddress = probs.Malformed("Issuance for IP addresses not supported") errIPAddress = berrors.MalformedError("Issuance for IP addresses not supported")
errTooManyLabels = probs.Malformed("DNS name has too many labels") errTooManyLabels = berrors.MalformedError("DNS name has too many labels")
errEmptyName = probs.Malformed("DNS name was empty") errEmptyName = berrors.MalformedError("DNS name was empty")
errNameEndsInDot = probs.Malformed("DNS name ends in a period") errNameEndsInDot = berrors.MalformedError("DNS name ends in a period")
errTooFewLabels = probs.Malformed("DNS name does not have enough labels") errTooFewLabels = berrors.MalformedError("DNS name does not have enough labels")
errLabelTooShort = probs.Malformed("DNS label is too short") errLabelTooShort = berrors.MalformedError("DNS label is too short")
errLabelTooLong = probs.Malformed("DNS label is too long") errLabelTooLong = berrors.MalformedError("DNS label is too long")
errIDNNotSupported = probs.UnsupportedIdentifier("Internationalized domain names (starting with xn--) not yet supported") errIDNNotSupported = berrors.UnsupportedIdentifierError("Internationalized domain names (starting with xn--) not yet supported")
errMalformedIDN = probs.Malformed("DNS label contains malformed punycode") errMalformedIDN = berrors.MalformedError("DNS label contains malformed punycode")
) )
// WillingToIssue determines whether the CA is willing to issue for the provided // WillingToIssue determines whether the CA is willing to issue for the provided
@ -286,6 +286,10 @@ func (pa *AuthorityImpl) ChallengesFor(identifier core.AcmeIdentifier) ([]core.C
challenges = append(challenges, core.TLSSNIChallenge01()) challenges = append(challenges, core.TLSSNIChallenge01())
} }
if features.Enabled(features.AllowTLS02Challenges) && pa.enabledChallenges[core.ChallengeTypeTLSSNI02] {
challenges = append(challenges, core.TLSSNIChallenge02())
}
if pa.enabledChallenges[core.ChallengeTypeDNS01] { if pa.enabledChallenges[core.ChallengeTypeDNS01] {
challenges = append(challenges, core.DNSChallenge01()) challenges = append(challenges, core.DNSChallenge01())
} }

190
ra/ra.go
View File

@ -21,6 +21,7 @@ import (
"github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
csrlib "github.com/letsencrypt/boulder/csr" csrlib "github.com/letsencrypt/boulder/csr"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/grpc"
@ -163,10 +164,10 @@ func (ra *RegistrationAuthorityImpl) updateIssuedCount() error {
return nil return nil
} }
const ( var (
unparseableEmailDetail = "not a valid e-mail address" unparseableEmailError = berrors.InvalidEmailError("not a valid e-mail address")
emptyDNSResponseDetail = "empty DNS response" emptyDNSResponseError = berrors.InvalidEmailError("empty DNS response")
multipleAddressDetail = "more than one e-mail address" multipleAddressError = berrors.InvalidEmailError("more than one e-mail address")
) )
func problemIsTimeout(err error) bool { func problemIsTimeout(err error) bool {
@ -177,13 +178,13 @@ func problemIsTimeout(err error) bool {
return false return false
} }
func validateEmail(ctx context.Context, address string, resolver bdns.DNSResolver) (prob *probs.ProblemDetails) { func validateEmail(ctx context.Context, address string, resolver bdns.DNSResolver) error {
emails, err := mail.ParseAddressList(address) emails, err := mail.ParseAddressList(address)
if err != nil { if err != nil {
return probs.InvalidEmail(unparseableEmailDetail) return unparseableEmailError
} }
if len(emails) > 1 { if len(emails) > 1 {
return probs.InvalidEmail(multipleAddressDetail) return multipleAddressError
} }
splitEmail := strings.SplitN(emails[0].Address, "@", -1) splitEmail := strings.SplitN(emails[0].Address, "@", -1)
domain := strings.ToLower(splitEmail[len(splitEmail)-1]) domain := strings.ToLower(splitEmail[len(splitEmail)-1])
@ -209,21 +210,17 @@ func validateEmail(ctx context.Context, address string, resolver bdns.DNSResolve
} }
if errMX != nil { if errMX != nil {
prob := bdns.ProblemDetailsFromDNSError(errMX) return berrors.InvalidEmailError(errMX.Error())
prob.Type = probs.InvalidEmailProblem
return prob
} else if len(resultMX) > 0 { } else if len(resultMX) > 0 {
return nil return nil
} }
if errA != nil { if errA != nil {
prob := bdns.ProblemDetailsFromDNSError(errA) return berrors.InvalidEmailError(errA.Error())
prob.Type = probs.InvalidEmailProblem
return prob
} else if len(resultA) > 0 { } else if len(resultA) > 0 {
return nil return nil
} }
return probs.InvalidEmail(emptyDNSResponseDetail) return emptyDNSResponseError
} }
type certificateRequestEvent struct { type certificateRequestEvent struct {
@ -258,7 +255,7 @@ func (ra *RegistrationAuthorityImpl) checkRegistrationLimit(ctx context.Context,
if count >= limit.GetThreshold(ip.String(), noRegistrationID) { if count >= limit.GetThreshold(ip.String(), noRegistrationID) {
ra.regByIPStats.Inc("Exceeded", 1) ra.regByIPStats.Inc("Exceeded", 1)
ra.log.Info(fmt.Sprintf("Rate limit exceeded, RegistrationsByIP, IP: %s", ip)) ra.log.Info(fmt.Sprintf("Rate limit exceeded, RegistrationsByIP, IP: %s", ip))
return core.RateLimitedError("Too many registrations from this IP") return berrors.RateLimitError("too many registrations for this IP")
} }
ra.regByIPStats.Inc("Pass", 1) ra.regByIPStats.Inc("Pass", 1)
} }
@ -268,7 +265,7 @@ func (ra *RegistrationAuthorityImpl) checkRegistrationLimit(ctx context.Context,
// NewRegistration constructs a new Registration from a request. // NewRegistration constructs a new Registration from a request.
func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init core.Registration) (reg core.Registration, err error) { func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init core.Registration) (reg core.Registration, err error) {
if err = ra.keyPolicy.GoodKey(init.Key.Key); err != nil { if err = ra.keyPolicy.GoodKey(init.Key.Key); err != nil {
return core.Registration{}, core.MalformedRequestError(fmt.Sprintf("Invalid public key: %s", err.Error())) return core.Registration{}, berrors.MalformedError("invalid public key: %s", err.Error())
} }
if err = ra.checkRegistrationLimit(ctx, init.InitialIP); err != nil { if err = ra.checkRegistrationLimit(ctx, init.InitialIP); err != nil {
return core.Registration{}, err return core.Registration{}, err
@ -292,9 +289,9 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init c
// Store the authorization object, then return it // Store the authorization object, then return it
reg, err = ra.SA.NewRegistration(ctx, reg) reg, err = ra.SA.NewRegistration(ctx, reg)
if err != nil { if err != nil {
// InternalServerError since the user-data was validated before being // berrors.InternalServerError since the user-data was validated before being
// passed to the SA. // passed to the SA.
err = core.InternalServerError(err.Error()) err = berrors.InternalServerError(err.Error())
} }
ra.stats.Inc("NewRegistrations", 1) ra.stats.Inc("NewRegistrations", 1)
@ -306,33 +303,38 @@ func (ra *RegistrationAuthorityImpl) validateContacts(ctx context.Context, conta
return nil // Nothing to validate return nil // Nothing to validate
} }
if ra.maxContactsPerReg > 0 && len(*contacts) > ra.maxContactsPerReg { if ra.maxContactsPerReg > 0 && len(*contacts) > ra.maxContactsPerReg {
return core.MalformedRequestError(fmt.Sprintf("Too many contacts provided: %d > %d", return berrors.MalformedError(
len(*contacts), ra.maxContactsPerReg)) "too many contacts provided: %d > %d",
len(*contacts),
ra.maxContactsPerReg,
)
} }
for _, contact := range *contacts { for _, contact := range *contacts {
if contact == "" { if contact == "" {
return core.MalformedRequestError("Empty contact") return berrors.MalformedError("empty contact")
} }
parsed, err := url.Parse(contact) parsed, err := url.Parse(contact)
if err != nil { if err != nil {
return core.MalformedRequestError("Invalid contact") return berrors.MalformedError("invalid contact")
} }
if parsed.Scheme != "mailto" { if parsed.Scheme != "mailto" {
return core.MalformedRequestError(fmt.Sprintf("Contact method %s is not supported", parsed.Scheme)) return berrors.MalformedError("contact method %s is not supported", parsed.Scheme)
} }
if !core.IsASCII(contact) { if !core.IsASCII(contact) {
return core.MalformedRequestError( return berrors.MalformedError(
fmt.Sprintf("Contact email [%s] contains non-ASCII characters", contact)) "contact email [%s] contains non-ASCII characters",
contact,
)
} }
start := ra.clk.Now() start := ra.clk.Now()
ra.stats.Inc("ValidateEmail.Calls", 1) ra.stats.Inc("ValidateEmail.Calls", 1)
problem := validateEmail(ctx, parsed.Opaque, ra.DNSResolver) err = validateEmail(ctx, parsed.Opaque, ra.DNSResolver)
ra.stats.TimingDuration("ValidateEmail.Latency", ra.clk.Now().Sub(start)) ra.stats.TimingDuration("ValidateEmail.Latency", ra.clk.Now().Sub(start))
if problem != nil { if err != nil {
ra.stats.Inc("ValidateEmail.Errors", 1) ra.stats.Inc("ValidateEmail.Errors", 1)
return problem return err
} }
ra.stats.Inc("ValidateEmail.Successes", 1) ra.stats.Inc("ValidateEmail.Successes", 1)
} }
@ -353,7 +355,7 @@ func (ra *RegistrationAuthorityImpl) checkPendingAuthorizationLimit(ctx context.
if count >= limit.GetThreshold(noKey, regID) { if count >= limit.GetThreshold(noKey, regID) {
ra.pendAuthByRegIDStats.Inc("Exceeded", 1) ra.pendAuthByRegIDStats.Inc("Exceeded", 1)
ra.log.Info(fmt.Sprintf("Rate limit exceeded, PendingAuthorizationsByRegID, regID: %d", regID)) ra.log.Info(fmt.Sprintf("Rate limit exceeded, PendingAuthorizationsByRegID, regID: %d", regID))
return core.RateLimitedError("Too many currently pending authorizations.") return berrors.RateLimitError("too many currently pending authorizations")
} }
ra.pendAuthByRegIDStats.Inc("Pass", 1) ra.pendAuthByRegIDStats.Inc("Pass", 1)
} }
@ -420,22 +422,27 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
if identifier.Type == core.IdentifierDNS { if identifier.Type == core.IdentifierDNS {
isSafeResp, err := ra.VA.IsSafeDomain(ctx, &vaPB.IsSafeDomainRequest{Domain: &identifier.Value}) isSafeResp, err := ra.VA.IsSafeDomain(ctx, &vaPB.IsSafeDomainRequest{Domain: &identifier.Value})
if err != nil { if err != nil {
outErr := core.InternalServerError("unable to determine if domain was safe") outErr := berrors.InternalServerError("unable to determine if domain was safe")
ra.log.Warning(fmt.Sprintf("%s: %s", string(outErr), err)) ra.log.Warning(fmt.Sprintf("%s: %s", outErr, err))
return authz, outErr return authz, outErr
} }
if !isSafeResp.GetIsSafe() { if !isSafeResp.GetIsSafe() {
return authz, core.UnauthorizedError(fmt.Sprintf("%#v was considered an unsafe domain by a third-party API", identifier.Value)) return authz, berrors.UnauthorizedError(
"%q was considered an unsafe domain by a third-party API",
identifier.Value,
)
} }
} }
if ra.reuseValidAuthz { if ra.reuseValidAuthz {
auths, err := ra.SA.GetValidAuthorizations(ctx, regID, []string{identifier.Value}, ra.clk.Now()) auths, err := ra.SA.GetValidAuthorizations(ctx, regID, []string{identifier.Value}, ra.clk.Now())
if err != nil { if err != nil {
outErr := core.InternalServerError( outErr := berrors.InternalServerError(
fmt.Sprintf("unable to get existing validations for regID: %d, identifier: %s", "unable to get existing validations for regID: %d, identifier: %s",
regID, identifier.Value)) regID,
ra.log.Warning(string(outErr)) identifier.Value,
)
ra.log.Warning(outErr.Error())
return authz, outErr return authz, outErr
} }
@ -445,10 +452,11 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
// `Challenge` values that the client expects in the result. // `Challenge` values that the client expects in the result.
populatedAuthz, err := ra.SA.GetAuthorization(ctx, existingAuthz.ID) populatedAuthz, err := ra.SA.GetAuthorization(ctx, existingAuthz.ID)
if err != nil { if err != nil {
outErr := core.InternalServerError( outErr := berrors.InternalServerError(
fmt.Sprintf("unable to get existing authorization for auth ID: %s", "unable to get existing authorization for auth ID: %s",
existingAuthz.ID)) existingAuthz.ID,
ra.log.Warning(fmt.Sprintf("%s: %s", string(outErr), existingAuthz.ID)) )
ra.log.Warning(fmt.Sprintf("%s: %s", outErr.Error(), existingAuthz.ID))
return authz, outErr return authz, outErr
} }
@ -480,18 +488,18 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
// Get a pending Auth first so we can get our ID back, then update with challenges // Get a pending Auth first so we can get our ID back, then update with challenges
authz, err = ra.SA.NewPendingAuthorization(ctx, authz) authz, err = ra.SA.NewPendingAuthorization(ctx, authz)
if err != nil { if err != nil {
// InternalServerError since the user-data was validated before being // berrors.InternalServerError since the user-data was validated before being
// passed to the SA. // passed to the SA.
err = core.InternalServerError(fmt.Sprintf("Invalid authorization request: %s", err)) err = berrors.InternalServerError("invalid authorization request: %s", err)
return core.Authorization{}, err return core.Authorization{}, err
} }
// Check each challenge for sanity. // Check each challenge for sanity.
for _, challenge := range authz.Challenges { for _, challenge := range authz.Challenges {
if !challenge.IsSaneForClientOffer() { if !challenge.IsSaneForClientOffer() {
// InternalServerError because we generated these challenges, they should // berrors.InternalServerError because we generated these challenges, they should
// be OK. // be OK.
err = core.InternalServerError(fmt.Sprintf("Challenge didn't pass sanity check: %+v", challenge)) err = berrors.InternalServerError("challenge didn't pass sanity check: %+v", challenge)
return core.Authorization{}, err return core.Authorization{}, err
} }
} }
@ -523,12 +531,12 @@ func (ra *RegistrationAuthorityImpl) MatchesCSR(cert core.Certificate, csr *x509
hostNames = core.UniqueLowerNames(hostNames) hostNames = core.UniqueLowerNames(hostNames)
if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) { if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) {
err = core.InternalServerError("Generated certificate public key doesn't match CSR public key") err = berrors.InternalServerError("generated certificate public key doesn't match CSR public key")
return return
} }
if !ra.forceCNFromSAN && len(csr.Subject.CommonName) > 0 && if !ra.forceCNFromSAN && len(csr.Subject.CommonName) > 0 &&
parsedCertificate.Subject.CommonName != strings.ToLower(csr.Subject.CommonName) { parsedCertificate.Subject.CommonName != strings.ToLower(csr.Subject.CommonName) {
err = core.InternalServerError("Generated certificate CommonName doesn't match CSR CommonName") err = berrors.InternalServerError("generated certificate CommonName doesn't match CSR CommonName")
return return
} }
// Sort both slices of names before comparison. // Sort both slices of names before comparison.
@ -536,39 +544,39 @@ func (ra *RegistrationAuthorityImpl) MatchesCSR(cert core.Certificate, csr *x509
sort.Strings(parsedNames) sort.Strings(parsedNames)
sort.Strings(hostNames) sort.Strings(hostNames)
if !reflect.DeepEqual(parsedNames, hostNames) { if !reflect.DeepEqual(parsedNames, hostNames) {
err = core.InternalServerError("Generated certificate DNSNames don't match CSR DNSNames") err = berrors.InternalServerError("generated certificate DNSNames don't match CSR DNSNames")
return return
} }
if !reflect.DeepEqual(parsedCertificate.IPAddresses, csr.IPAddresses) { if !reflect.DeepEqual(parsedCertificate.IPAddresses, csr.IPAddresses) {
err = core.InternalServerError("Generated certificate IPAddresses don't match CSR IPAddresses") err = berrors.InternalServerError("generated certificate IPAddresses don't match CSR IPAddresses")
return return
} }
if !reflect.DeepEqual(parsedCertificate.EmailAddresses, csr.EmailAddresses) { if !reflect.DeepEqual(parsedCertificate.EmailAddresses, csr.EmailAddresses) {
err = core.InternalServerError("Generated certificate EmailAddresses don't match CSR EmailAddresses") err = berrors.InternalServerError("generated certificate EmailAddresses don't match CSR EmailAddresses")
return return
} }
if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 || if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 ||
len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 || len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 ||
len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 || len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 ||
len(parsedCertificate.Subject.PostalCode) > 0 { len(parsedCertificate.Subject.PostalCode) > 0 {
err = core.InternalServerError("Generated certificate Subject contains fields other than CommonName, or SerialNumber") err = berrors.InternalServerError("generated certificate Subject contains fields other than CommonName, or SerialNumber")
return return
} }
now := ra.clk.Now() now := ra.clk.Now()
if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 { if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 {
err = core.InternalServerError(fmt.Sprintf("Generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore))) err = berrors.InternalServerError("generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore))
return return
} }
if !parsedCertificate.BasicConstraintsValid { if !parsedCertificate.BasicConstraintsValid {
err = core.InternalServerError("Generated certificate doesn't have basic constraints set") err = berrors.InternalServerError("generated certificate doesn't have basic constraints set")
return return
} }
if parsedCertificate.IsCA { if parsedCertificate.IsCA {
err = core.InternalServerError("Generated certificate can sign other certificates") err = berrors.InternalServerError("generated certificate can sign other certificates")
return return
} }
if !reflect.DeepEqual(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) { if !reflect.DeepEqual(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
err = core.InternalServerError("Generated certificate doesn't have correct key usage extensions") err = berrors.InternalServerError("generated certificate doesn't have correct key usage extensions")
return return
} }
@ -592,16 +600,17 @@ func (ra *RegistrationAuthorityImpl) checkAuthorizations(ctx context.Context, na
if authz == nil { if authz == nil {
badNames = append(badNames, name) badNames = append(badNames, name)
} else if authz.Expires == nil { } else if authz.Expires == nil {
return fmt.Errorf("Found an authorization with a nil Expires field: id %s", authz.ID) return berrors.InternalServerError("found an authorization with a nil Expires field: id %s", authz.ID)
} else if authz.Expires.Before(now) { } else if authz.Expires.Before(now) {
badNames = append(badNames, name) badNames = append(badNames, name)
} }
} }
if len(badNames) > 0 { if len(badNames) > 0 {
return core.UnauthorizedError(fmt.Sprintf( return berrors.UnauthorizedError(
"Authorizations for these names not found or expired: %s", "authorizations for these names not found or expired: %s",
strings.Join(badNames, ", "))) strings.Join(badNames, ", "),
)
} }
return nil return nil
} }
@ -628,7 +637,7 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req cor
}() }()
if regID <= 0 { if regID <= 0 {
err = core.MalformedRequestError(fmt.Sprintf("Invalid registration ID: %d", regID)) err = berrors.MalformedError("invalid registration ID: %d", regID)
return emptyCert, err return emptyCert, err
} }
@ -641,8 +650,7 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req cor
// Verify the CSR // Verify the CSR
csr := req.CSR csr := req.CSR
if err := csrlib.VerifyCSR(csr, ra.maxNames, &ra.keyPolicy, ra.PA, ra.forceCNFromSAN, regID); err != nil { if err := csrlib.VerifyCSR(csr, ra.maxNames, &ra.keyPolicy, ra.PA, ra.forceCNFromSAN, regID); err != nil {
err = core.MalformedRequestError(err.Error()) return emptyCert, berrors.MalformedError(err.Error())
return emptyCert, err
} }
logEvent.CommonName = csr.Subject.CommonName logEvent.CommonName = csr.Subject.CommonName
@ -653,13 +661,13 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req cor
copy(names, csr.DNSNames) copy(names, csr.DNSNames)
if len(names) == 0 { if len(names) == 0 {
err = core.UnauthorizedError("CSR has no names in it") err = berrors.UnauthorizedError("CSR has no names in it")
logEvent.Error = err.Error() logEvent.Error = err.Error()
return emptyCert, err return emptyCert, err
} }
if core.KeyDigestEquals(csr.PublicKey, registration.Key) { if core.KeyDigestEquals(csr.PublicKey, registration.Key) {
err = core.MalformedRequestError("Certificate public key must be different than account key") err = berrors.MalformedError("certificate public key must be different than account key")
return emptyCert, err return emptyCert, err
} }
@ -703,9 +711,9 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req cor
parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER)) parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
if err != nil { if err != nil {
// InternalServerError because the certificate from the CA should be // berrors.InternalServerError because the certificate from the CA should be
// parseable. // parseable.
err = core.InternalServerError(err.Error()) err = berrors.InternalServerError("failed to parse certificate: %s", err.Error())
logEvent.Error = err.Error() logEvent.Error = err.Error()
return emptyCert, err return emptyCert, err
} }
@ -785,8 +793,10 @@ func (ra *RegistrationAuthorityImpl) checkCertificatesPerNameLimit(ctx context.C
domains := strings.Join(badNames, ", ") domains := strings.Join(badNames, ", ")
ra.certsForDomainStats.Inc("Exceeded", 1) ra.certsForDomainStats.Inc("Exceeded", 1)
ra.log.Info(fmt.Sprintf("Rate limit exceeded, CertificatesForDomain, regID: %d, domains: %s", regID, domains)) ra.log.Info(fmt.Sprintf("Rate limit exceeded, CertificatesForDomain, regID: %d, domains: %s", regID, domains))
return core.RateLimitedError(fmt.Sprintf( return berrors.RateLimitError(
"Too many certificates already issued for: %s", domains)) "too many certificates already issued for: %s",
domains,
)
} }
ra.certsForDomainStats.Inc("Pass", 1) ra.certsForDomainStats.Inc("Pass", 1)
@ -801,10 +811,10 @@ func (ra *RegistrationAuthorityImpl) checkCertificatesPerFQDNSetLimit(ctx contex
} }
names = core.UniqueLowerNames(names) names = core.UniqueLowerNames(names)
if int(count) > limit.GetThreshold(strings.Join(names, ","), regID) { if int(count) > limit.GetThreshold(strings.Join(names, ","), regID) {
return core.RateLimitedError(fmt.Sprintf( return berrors.RateLimitError(
"Too many certificates already issued for exact set of domains: %s", "too many certificates already issued for exact set of domains: %s",
strings.Join(names, ","), strings.Join(names, ","),
)) )
} }
return nil return nil
} }
@ -817,12 +827,15 @@ func (ra *RegistrationAuthorityImpl) checkTotalCertificatesLimit() error {
// or not yet updated, fail. // or not yet updated, fail.
if ra.clk.Now().After(ra.totalIssuedLastUpdate.Add(5*time.Minute)) || if ra.clk.Now().After(ra.totalIssuedLastUpdate.Add(5*time.Minute)) ||
ra.totalIssuedLastUpdate.IsZero() { ra.totalIssuedLastUpdate.IsZero() {
return core.InternalServerError(fmt.Sprintf("Total certificate count out of date: updated %s", ra.totalIssuedLastUpdate)) return berrors.InternalServerError(
"Total certificate count out of date: updated %s",
ra.totalIssuedLastUpdate,
)
} }
if ra.totalIssuedCount >= totalCertLimits.Threshold { if ra.totalIssuedCount >= totalCertLimits.Threshold {
ra.totalCertsStats.Inc("Exceeded", 1) ra.totalCertsStats.Inc("Exceeded", 1)
ra.log.Info(fmt.Sprintf("Rate limit exceeded, TotalCertificates, totalIssued: %d, lastUpdated %s", ra.totalIssuedCount, ra.totalIssuedLastUpdate)) ra.log.Info(fmt.Sprintf("Rate limit exceeded, TotalCertificates, totalIssued: %d, lastUpdated %s", ra.totalIssuedCount, ra.totalIssuedLastUpdate))
return core.RateLimitedError("Global certificate issuance limit reached. Try again in an hour.") return berrors.RateLimitError("global certificate issuance limit reached. Try again in an hour")
} }
ra.totalCertsStats.Inc("Pass", 1) ra.totalCertsStats.Inc("Pass", 1)
return nil return nil
@ -873,9 +886,9 @@ func (ra *RegistrationAuthorityImpl) UpdateRegistration(ctx context.Context, bas
err = ra.SA.UpdateRegistration(ctx, base) err = ra.SA.UpdateRegistration(ctx, base)
if err != nil { if err != nil {
// InternalServerError since the user-data was validated before being // berrors.InternalServerError since the user-data was validated before being
// passed to the SA. // passed to the SA.
err = core.InternalServerError(fmt.Sprintf("Could not update registration: %s", err)) err = berrors.InternalServerError("Could not update registration: %s", err)
return core.Registration{}, err return core.Registration{}, err
} }
@ -948,13 +961,13 @@ func mergeUpdate(r *core.Registration, input core.Registration) bool {
func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, base core.Authorization, challengeIndex int, response core.Challenge) (authz core.Authorization, err error) { func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, base core.Authorization, challengeIndex int, response core.Challenge) (authz core.Authorization, err error) {
// Refuse to update expired authorizations // Refuse to update expired authorizations
if base.Expires == nil || base.Expires.Before(ra.clk.Now()) { if base.Expires == nil || base.Expires.Before(ra.clk.Now()) {
err = core.NotFoundError("Expired authorization") err = berrors.MalformedError("expired authorization")
return return
} }
authz = base authz = base
if challengeIndex >= len(authz.Challenges) { if challengeIndex >= len(authz.Challenges) {
err = core.MalformedRequestError(fmt.Sprintf("Invalid challenge index: %d", challengeIndex)) err = berrors.MalformedError("invalid challenge index '%d'", challengeIndex)
return return
} }
@ -963,8 +976,11 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
if response.Type != "" && ch.Type != response.Type { if response.Type != "" && ch.Type != response.Type {
// TODO(riking): Check the rate on this, uncomment error return if negligible // TODO(riking): Check the rate on this, uncomment error return if negligible
ra.stats.Inc("StartChallengeWrongType", 1) ra.stats.Inc("StartChallengeWrongType", 1)
// err = core.MalformedRequestError(fmt.Sprintf("Invalid update to challenge - provided type was %s but actual type is %s", response.Type, ch.Type)) // return authz, berrors.MalformedError(
// return // "invalid challenge update: provided type was %s but actual type is %s",
// response.Type,
// ch.Type,
// )
} }
// When configured with `reuseValidAuthz` we can expect some clients to try // When configured with `reuseValidAuthz` we can expect some clients to try
@ -980,7 +996,7 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
// Look up the account key for this authorization // Look up the account key for this authorization
reg, err := ra.SA.GetRegistration(ctx, authz.RegistrationID) reg, err := ra.SA.GetRegistration(ctx, authz.RegistrationID)
if err != nil { if err != nil {
err = core.InternalServerError(err.Error()) err = berrors.InternalServerError(err.Error())
return return
} }
@ -988,11 +1004,11 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
// check it against the value provided // check it against the value provided
expectedKeyAuthorization, err := ch.ExpectedKeyAuthorization(reg.Key) expectedKeyAuthorization, err := ch.ExpectedKeyAuthorization(reg.Key)
if err != nil { if err != nil {
err = core.InternalServerError("Could not compute expected key authorization value") err = berrors.InternalServerError("could not compute expected key authorization value")
return return
} }
if expectedKeyAuthorization != response.ProvidedKeyAuthorization { if expectedKeyAuthorization != response.ProvidedKeyAuthorization {
err = core.MalformedRequestError("Provided key authorization was incorrect") err = berrors.MalformedError("provided key authorization was incorrect")
return return
} }
@ -1001,7 +1017,7 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
// Double check before sending to VA // Double check before sending to VA
if !ch.IsSaneForValidation() { if !ch.IsSaneForValidation() {
err = core.MalformedRequestError("Response does not complete challenge") err = berrors.MalformedError("response does not complete challenge")
return return
} }
@ -1009,7 +1025,7 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
if err = ra.SA.UpdatePendingAuthorization(ctx, authz); err != nil { if err = ra.SA.UpdatePendingAuthorization(ctx, authz); err != nil {
ra.log.Warning(fmt.Sprintf( ra.log.Warning(fmt.Sprintf(
"Error calling ra.SA.UpdatePendingAuthorization: %s\n", err.Error())) "Error calling ra.SA.UpdatePendingAuthorization: %s\n", err.Error()))
err = core.InternalServerError("Could not update pending authorization") err = berrors.InternalServerError("could not update pending authorization")
return return
} }
ra.stats.Inc("NewPendingAuthorizations", 1) ra.stats.Inc("NewPendingAuthorizations", 1)
@ -1172,11 +1188,11 @@ func (ra *RegistrationAuthorityImpl) onValidationUpdate(ctx context.Context, aut
// DeactivateRegistration deactivates a valid registration // DeactivateRegistration deactivates a valid registration
func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context, reg core.Registration) error { func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context, reg core.Registration) error {
if reg.Status != core.StatusValid { if reg.Status != core.StatusValid {
return core.MalformedRequestError("Only valid registrations can be deactivated") return berrors.MalformedError("only valid registrations can be deactivated")
} }
err := ra.SA.DeactivateRegistration(ctx, reg.ID) err := ra.SA.DeactivateRegistration(ctx, reg.ID)
if err != nil { if err != nil {
return core.InternalServerError(err.Error()) return berrors.InternalServerError(err.Error())
} }
return nil return nil
} }
@ -1184,11 +1200,11 @@ func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context,
// DeactivateAuthorization deactivates a currently valid authorization // DeactivateAuthorization deactivates a currently valid authorization
func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context, auth core.Authorization) error { func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context, auth core.Authorization) error {
if auth.Status != core.StatusValid && auth.Status != core.StatusPending { if auth.Status != core.StatusValid && auth.Status != core.StatusPending {
return core.MalformedRequestError("Only valid and pending authorizations can be deactivated") return berrors.MalformedError("only valid and pending authorizations can be deactivated")
} }
err := ra.SA.DeactivateAuthorization(ctx, auth.ID) err := ra.SA.DeactivateAuthorization(ctx, auth.ID)
if err != nil { if err != nil {
return core.InternalServerError(err.Error()) return berrors.InternalServerError(err.Error())
} }
return nil return nil
} }

View File

@ -23,6 +23,7 @@ import (
"github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
@ -324,9 +325,9 @@ func TestValidateEmail(t *testing.T) {
input string input string
expected string expected string
}{ }{
{"an email`", unparseableEmailDetail}, {"an email`", unparseableEmailError.Error()},
{"a@always.invalid", emptyDNSResponseDetail}, {"a@always.invalid", emptyDNSResponseError.Error()},
{"a@email.com, b@email.com", multipleAddressDetail}, {"a@email.com, b@email.com", multipleAddressError.Error()},
{"a@always.error", "DNS problem: networking error looking up A for always.error"}, {"a@always.error", "DNS problem: networking error looking up A for always.error"},
} }
testSuccesses := []string{ testSuccesses := []string{
@ -339,20 +340,21 @@ func TestValidateEmail(t *testing.T) {
} }
for _, tc := range testFailures { for _, tc := range testFailures {
problem := validateEmail(context.Background(), tc.input, &bdns.MockDNSResolver{}) err := validateEmail(context.Background(), tc.input, &bdns.MockDNSResolver{})
if problem.Type != probs.InvalidEmailProblem { if !berrors.Is(err, berrors.InvalidEmail) {
t.Errorf("validateEmail(%q): got problem type %#v, expected %#v", tc.input, problem.Type, probs.InvalidEmailProblem) t.Errorf("validateEmail(%q): got error %#v, expected type berrors.InvalidEmail", tc.input, err)
} }
if problem.Detail != tc.expected {
if err.Error() != tc.expected {
t.Errorf("validateEmail(%q): got %#v, expected %#v", t.Errorf("validateEmail(%q): got %#v, expected %#v",
tc.input, problem.Detail, tc.expected) tc.input, err.Error(), tc.expected)
} }
} }
for _, addr := range testSuccesses { for _, addr := range testSuccesses {
if prob := validateEmail(context.Background(), addr, &bdns.MockDNSResolver{}); prob != nil { if err := validateEmail(context.Background(), addr, &bdns.MockDNSResolver{}); err != nil {
t.Errorf("validateEmail(%q): expected success, but it failed: %s", t.Errorf("validateEmail(%q): expected success, but it failed: %#v",
addr, prob) addr, err)
} }
} }
} }
@ -680,11 +682,8 @@ func TestNewAuthorizationInvalidName(t *testing.T) {
if err == nil { if err == nil {
t.Fatalf("NewAuthorization succeeded for 127.0.0.1, should have failed") t.Fatalf("NewAuthorization succeeded for 127.0.0.1, should have failed")
} }
if _, ok := err.(*probs.ProblemDetails); !ok { if !berrors.Is(err, berrors.Malformed) {
t.Errorf("Wrong type for NewAuthorization error: expected *probs.ProblemDetails, got %T", err) t.Errorf("expected berrors.BoulderError with internal type berrors.Malformed, got %T", err)
}
if err.(*probs.ProblemDetails).Type != probs.MalformedProblem {
t.Errorf("Incorrect problem type. Expected %s got %s", probs.MalformedProblem, err.(*probs.ProblemDetails).Type)
} }
} }
@ -806,7 +805,7 @@ func TestCertificateKeyNotEqualAccountKey(t *testing.T) {
// Registration has key == AccountKeyA // Registration has key == AccountKeyA
_, err = ra.NewCertificate(ctx, certRequest, Registration.ID) _, err = ra.NewCertificate(ctx, certRequest, Registration.ID)
test.AssertError(t, err, "Should have rejected cert with key = account key") test.AssertError(t, err, "Should have rejected cert with key = account key")
test.AssertEquals(t, err.Error(), "Certificate public key must be different than account key") test.AssertEquals(t, err.Error(), "certificate public key must be different than account key")
t.Log("DONE TestCertificateKeyNotEqualAccountKey") t.Log("DONE TestCertificateKeyNotEqualAccountKey")
} }
@ -1108,7 +1107,7 @@ func TestCheckCertificatesPerNameLimit(t *testing.T) {
mockSA.nameCounts["example.com"] = 10 mockSA.nameCounts["example.com"] = 10
err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "example.com"}, rlp, 99) err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "example.com"}, rlp, 99)
test.AssertError(t, err, "incorrectly failed to rate limit example.com") test.AssertError(t, err, "incorrectly failed to rate limit example.com")
if _, ok := err.(core.RateLimitedError); !ok { if !berrors.Is(err, berrors.RateLimit) {
t.Errorf("Incorrect error type %#v", err) t.Errorf("Incorrect error type %#v", err)
} }
@ -1127,7 +1126,7 @@ func TestCheckCertificatesPerNameLimit(t *testing.T) {
mockSA.nameCounts["bigissuer.com"] = 100 mockSA.nameCounts["bigissuer.com"] = 100
err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "subdomain.bigissuer.com"}, rlp, 99) err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.example.com", "subdomain.bigissuer.com"}, rlp, 99)
test.AssertError(t, err, "incorrectly failed to rate limit bigissuer") test.AssertError(t, err, "incorrectly failed to rate limit bigissuer")
if _, ok := err.(core.RateLimitedError); !ok { if !berrors.Is(err, berrors.RateLimit) {
t.Errorf("Incorrect error type") t.Errorf("Incorrect error type")
} }
@ -1135,7 +1134,7 @@ func TestCheckCertificatesPerNameLimit(t *testing.T) {
mockSA.nameCounts["smallissuer.co.uk"] = 1 mockSA.nameCounts["smallissuer.co.uk"] = 1
err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.smallissuer.co.uk"}, rlp, 99) err = ra.checkCertificatesPerNameLimit(ctx, []string{"www.smallissuer.co.uk"}, rlp, 99)
test.AssertError(t, err, "incorrectly failed to rate limit smallissuer") test.AssertError(t, err, "incorrectly failed to rate limit smallissuer")
if _, ok := err.(core.RateLimitedError); !ok { if !berrors.Is(err, berrors.RateLimit) {
t.Errorf("Incorrect error type %#v", err) t.Errorf("Incorrect error type %#v", err)
} }
} }

View File

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -21,6 +22,7 @@ import (
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/probs"
@ -200,6 +202,9 @@ func wrapError(err error) *rpcError {
wrapped.Type = string(terr.Type) wrapped.Type = string(terr.Type)
wrapped.Value = terr.Detail wrapped.Value = terr.Detail
wrapped.HTTPStatus = terr.HTTPStatus wrapped.HTTPStatus = terr.HTTPStatus
case *berrors.BoulderError:
wrapped.Type = fmt.Sprintf("berr:%d", terr.Type)
wrapped.Value = terr.Detail
} }
return wrapped return wrapped
} }
@ -236,6 +241,17 @@ func unwrapError(rpcError *rpcError) error {
HTTPStatus: rpcError.HTTPStatus, HTTPStatus: rpcError.HTTPStatus,
} }
} }
if strings.HasPrefix(rpcError.Type, "berr:") {
errType, decErr := strconv.Atoi(rpcError.Type[5:])
if decErr != nil {
return berrors.InternalServerError(
"failed to decode error type, decoding error %q, wrapped error %q",
decErr,
rpcError.Value,
)
}
return berrors.New(berrors.ErrorType(errType), rpcError.Value)
}
return errors.New(rpcError.Value) return errors.New(rpcError.Value)
} }
} }
@ -388,7 +404,7 @@ func (rpc *AmqpRPCServer) replyTooManyRequests(msg amqp.Delivery) error {
// remaining messages are processed. // remaining messages are processed.
func (rpc *AmqpRPCServer) Start(c *cmd.AMQPConfig) error { func (rpc *AmqpRPCServer) Start(c *cmd.AMQPConfig) error {
tooManyGoroutines := rpcResponse{ tooManyGoroutines := rpcResponse{
Error: wrapError(core.TooManyRPCRequestsError("RPC server has spawned too many Goroutines")), Error: wrapError(berrors.TooManyRequestsError("RPC server has spawned too many Goroutines")),
} }
tooManyRequestsResponse, err := json.Marshal(tooManyGoroutines) tooManyRequestsResponse, err := json.Marshal(tooManyGoroutines)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
) )
@ -56,6 +57,10 @@ func TestWrapError(t *testing.T) {
errors.New(""), errors.New(""),
errors.New(""), errors.New(""),
}, },
{
berrors.MalformedError("foo"),
berrors.MalformedError("foo"),
},
} }
for i, tc := range complicated { for i, tc := range complicated {
actual := unwrapError(wrapError(tc.given)) actual := unwrapError(wrapError(tc.given))

View File

@ -5,7 +5,6 @@ import (
"crypto/x509" "crypto/x509"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
@ -18,6 +17,7 @@ import (
jose "gopkg.in/square/go-jose.v1" jose "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/revocation"
@ -122,9 +122,7 @@ func (ssa *SQLStorageAuthority) GetRegistration(ctx context.Context, id int64) (
model, err = selectRegistration(ssa.dbMap, query, id) model, err = selectRegistration(ssa.dbMap, query, id)
} }
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return core.Registration{}, core.NoSuchRegistrationError( return core.Registration{}, berrors.NotFoundError("registration with ID '%d' not found", id)
fmt.Sprintf("No registrations with ID %d", id),
)
} }
if err != nil { if err != nil {
return core.Registration{}, err return core.Registration{}, err
@ -150,8 +148,7 @@ func (ssa *SQLStorageAuthority) GetRegistrationByKey(ctx context.Context, key *j
model, err = selectRegistration(ssa.dbMap, query, sha) model, err = selectRegistration(ssa.dbMap, query, sha)
} }
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
msg := fmt.Sprintf("No registrations with public key sha256 %s", sha) return core.Registration{}, berrors.NotFoundError("no registrations with public key sha256 %q", sha)
return core.Registration{}, core.NoSuchRegistrationError(msg)
} }
if err != nil { if err != nil {
return core.Registration{}, err return core.Registration{}, err
@ -218,7 +215,7 @@ func (ssa *SQLStorageAuthority) GetAuthorization(ctx context.Context, id string)
// domain names from the parameters that the account has authorizations for. // domain names from the parameters that the account has authorizations for.
func (ssa *SQLStorageAuthority) GetValidAuthorizations(ctx context.Context, registrationID int64, names []string, now time.Time) (map[string]*core.Authorization, error) { func (ssa *SQLStorageAuthority) GetValidAuthorizations(ctx context.Context, registrationID int64, names []string, now time.Time) (map[string]*core.Authorization, error) {
if len(names) == 0 { if len(names) == 0 {
return nil, errors.New("GetValidAuthorizations: no names received") return nil, berrors.InternalServerError("no names received")
} }
params := make([]interface{}, len(names)) params := make([]interface{}, len(names))
@ -421,7 +418,7 @@ func (ssa *SQLStorageAuthority) GetCertificate(ctx context.Context, serial strin
cert, err := SelectCertificate(ssa.dbMap, "WHERE serial = ?", serial) cert, err := SelectCertificate(ssa.dbMap, "WHERE serial = ?", serial)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return core.Certificate{}, core.NotFoundError(fmt.Sprintf("No certificate found for %s", serial)) return core.Certificate{}, berrors.NotFoundError("certificate with serial %q not found", serial)
} }
if err != nil { if err != nil {
return core.Certificate{}, err return core.Certificate{}, err
@ -520,7 +517,7 @@ func (ssa *SQLStorageAuthority) MarkCertificateRevoked(ctx context.Context, seri
return err return err
} }
if n == 0 { if n == 0 {
err = errors.New("No certificate updated. Maybe the lock column was off?") err = berrors.InternalServerError("no certificate updated")
err = Rollback(tx, err) err = Rollback(tx, err)
return err return err
} }
@ -539,8 +536,7 @@ func (ssa *SQLStorageAuthority) UpdateRegistration(ctx context.Context, reg core
model, err = selectRegistration(ssa.dbMap, query, reg.ID) model, err = selectRegistration(ssa.dbMap, query, reg.ID)
} }
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
msg := fmt.Sprintf("No registrations with ID %d", reg.ID) return berrors.NotFoundError("registration with ID '%d' not found", reg.ID)
return core.NoSuchRegistrationError(msg)
} }
updatedRegModel, err := registrationToModel(&reg) updatedRegModel, err := registrationToModel(&reg)
@ -569,8 +565,7 @@ func (ssa *SQLStorageAuthority) UpdateRegistration(ctx context.Context, reg core
return err return err
} }
if n == 0 { if n == 0 {
msg := fmt.Sprintf("Requested registration not found %d", reg.ID) return berrors.NotFoundError("registration with ID '%d' not found", reg.ID)
return core.NoSuchRegistrationError(msg)
} }
return nil return nil
@ -636,23 +631,24 @@ func (ssa *SQLStorageAuthority) UpdatePendingAuthorization(ctx context.Context,
} }
if !statusIsPending(authz.Status) { if !statusIsPending(authz.Status) {
err = errors.New("Use FinalizeAuthorization() to update to a final status") err = berrors.InternalServerError("authorization is not pending")
return Rollback(tx, err) return Rollback(tx, err)
} }
if existingFinal(tx, authz.ID) { if existingFinal(tx, authz.ID) {
err = errors.New("Cannot update a final authorization") err = berrors.InternalServerError("cannot update a finalized authorization")
return Rollback(tx, err) return Rollback(tx, err)
} }
if !existingPending(tx, authz.ID) { if !existingPending(tx, authz.ID) {
err = errors.New("Requested authorization not found " + authz.ID) err = berrors.InternalServerError("authorization with ID '%d' not found", authz.ID)
return Rollback(tx, err) return Rollback(tx, err)
} }
pa, err := selectPendingAuthz(tx, "WHERE id = ?", authz.ID) pa, err := selectPendingAuthz(tx, "WHERE id = ?", authz.ID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return Rollback(tx, fmt.Errorf("No pending authorization with ID %s", authz.ID)) err = berrors.InternalServerError("authorization with ID '%d' not found", authz.ID)
return Rollback(tx, err)
} }
if err != nil { if err != nil {
return Rollback(tx, err) return Rollback(tx, err)
@ -680,18 +676,18 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization(ctx context.Context, authz
// Check that a pending authz exists // Check that a pending authz exists
if !existingPending(tx, authz.ID) { if !existingPending(tx, authz.ID) {
err = errors.New("Cannot finalize an authorization that is not pending") err = berrors.InternalServerError("authorization with ID %q not found", authz.ID)
return Rollback(tx, err) return Rollback(tx, err)
} }
if statusIsPending(authz.Status) { if statusIsPending(authz.Status) {
err = errors.New("Cannot finalize to a non-final status") err = berrors.InternalServerError("authorization with ID %q is not pending", authz.ID)
return Rollback(tx, err) return Rollback(tx, err)
} }
auth := &authzModel{authz} auth := &authzModel{authz}
pa, err := selectPendingAuthz(tx, "WHERE id = ?", authz.ID) pa, err := selectPendingAuthz(tx, "WHERE id = ?", authz.ID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return Rollback(tx, fmt.Errorf("No pending authorization with ID %s", authz.ID)) return Rollback(tx, berrors.InternalServerError("authorization with ID %q not found", authz.ID))
} }
if err != nil { if err != nil {
return Rollback(tx, err) return Rollback(tx, err)

View File

@ -21,6 +21,7 @@ import (
jose "gopkg.in/square/go-jose.v1" jose "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/revocation"
@ -122,19 +123,19 @@ func TestNoSuchRegistrationErrors(t *testing.T) {
defer cleanUp() defer cleanUp()
_, err := sa.GetRegistration(ctx, 100) _, err := sa.GetRegistration(ctx, 100)
if _, ok := err.(core.NoSuchRegistrationError); !ok { if !berrors.Is(err, berrors.NotFound) {
t.Errorf("GetRegistration: expected NoSuchRegistrationError, got %T type error (%s)", err, err) t.Errorf("GetRegistration: expected a berrors.NotFound type error, got %T type error (%s)", err, err)
} }
jwk := satest.GoodJWK() jwk := satest.GoodJWK()
_, err = sa.GetRegistrationByKey(ctx, jwk) _, err = sa.GetRegistrationByKey(ctx, jwk)
if _, ok := err.(core.NoSuchRegistrationError); !ok { if !berrors.Is(err, berrors.NotFound) {
t.Errorf("GetRegistrationByKey: expected a NoSuchRegistrationError, got %T type error (%s)", err, err) t.Errorf("GetRegistrationByKey: expected a berrors.NotFound type error, got %T type error (%s)", err, err)
} }
err = sa.UpdateRegistration(ctx, core.Registration{ID: 100, Key: jwk}) err = sa.UpdateRegistration(ctx, core.Registration{ID: 100, Key: jwk})
if _, ok := err.(core.NoSuchRegistrationError); !ok { if !berrors.Is(err, berrors.NotFound) {
t.Errorf("UpdateRegistration: expected a NoSuchRegistrationError, got %T type error (%v)", err, err) t.Errorf("UpdateRegistration: expected a berrors.NotFound type error, got %T type error (%v)", err, err)
} }
} }

View File

@ -134,7 +134,8 @@
"serviceQueue": "CA.server" "serviceQueue": "CA.server"
}, },
"features": { "features": {
"IDNASupport": true "IDNASupport": true,
"AllowTLS02Challenges": true
} }
}, },

View File

@ -3,7 +3,8 @@
"dbConnectFile": "test/secrets/cert_checker_dburl", "dbConnectFile": "test/secrets/cert_checker_dburl",
"maxDBConns": 10, "maxDBConns": 10,
"features": { "features": {
"IDNASupport": true "IDNASupport": true,
"AllowTLS02Challenges": true
}, },
"hostnamePolicyFile": "test/hostname-policy.json" "hostnamePolicyFile": "test/hostname-policy.json"
}, },
@ -12,7 +13,8 @@
"challenges": { "challenges": {
"http-01": true, "http-01": true,
"tls-sni-01": true, "tls-sni-01": true,
"dns-01": true "dns-01": true,
"tls-sni-02": true
} }
}, },

View File

@ -46,7 +46,8 @@
}, },
"features": { "features": {
"IDNASupport": true, "IDNASupport": true,
"AllowKeyRollover": true "AllowKeyRollover": true,
"AllowTLS02Challenges": true
} }
}, },

View File

@ -281,14 +281,13 @@ def get_future_output(cmd, date):
return run(cmd, env={'FAKECLOCK': date.strftime("%a %b %d %H:%M:%S UTC %Y")}) return run(cmd, env={'FAKECLOCK': date.strftime("%a %b %d %H:%M:%S UTC %Y")})
def test_expired_authz_purger(): def test_expired_authz_purger():
def expect(target_time, num): def expect(target_time, num, table):
expected_output = ''
if num is not None:
expected_output = 'Deleted a total of %d expired pending authorizations' % num
out = get_future_output("./bin/expired-authz-purger --config cmd/expired-authz-purger/config.json --yes", target_time) out = get_future_output("./bin/expired-authz-purger --config cmd/expired-authz-purger/config.json --yes", target_time)
if 'via FAKECLOCK' not in out: if 'via FAKECLOCK' not in out:
raise Exception("expired-authz-purger was not built with `integration` build tag") raise Exception("expired-authz-purger was not built with `integration` build tag")
if num is None:
return
expected_output = 'Deleted a total of %d expired authorizations from %s' % (num, table)
if expected_output not in out: if expected_output not in out:
raise Exception("expired-authz-purger did not print '%s'. Output:\n%s" % ( raise Exception("expired-authz-purger did not print '%s'. Output:\n%s" % (
expected_output, out)) expected_output, out))
@ -296,7 +295,7 @@ def test_expired_authz_purger():
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
# Run the purger once to clear out any backlog so we have a clean slate. # Run the purger once to clear out any backlog so we have a clean slate.
expect(now, None) expect(now, None, "")
# Make an authz, but don't attempt its challenges. # Make an authz, but don't attempt its challenges.
chisel.make_client().request_domain_challenges("eap-test.com") chisel.make_client().request_domain_challenges("eap-test.com")
@ -304,8 +303,13 @@ def test_expired_authz_purger():
# Run the authz twice: Once immediate, expecting nothing to be purged, and # Run the authz twice: Once immediate, expecting nothing to be purged, and
# once as if it were the future, expecting one purged authz. # once as if it were the future, expecting one purged authz.
after_grace_period = now + datetime.timedelta(days=+14, minutes=+3) after_grace_period = now + datetime.timedelta(days=+14, minutes=+3)
expect(now, 0) expect(now, 0, "pendingAuthorizations")
expect(after_grace_period, 1) expect(after_grace_period, 1, "pendingAuthorizations")
auth_and_issue([random_domain()])
after_grace_period = now + datetime.timedelta(days=+67, minutes=+3)
expect(now, 0, "authz")
expect(after_grace_period, 1, "authz")
def test_certificates_per_name(): def test_certificates_per_name():
chisel.expect_problem("urn:acme:error:rateLimited", chisel.expect_problem("urn:acme:error:rateLimited",
@ -394,9 +398,9 @@ def main():
def run_chisel(): def run_chisel():
# TODO(https://github.com/letsencrypt/boulder/issues/2521): Add TLS-SNI test. # TODO(https://github.com/letsencrypt/boulder/issues/2521): Add TLS-SNI test.
test_expired_authz_purger()
test_ct_submission() test_ct_submission()
test_gsb_lookups() test_gsb_lookups()
test_expired_authz_purger()
test_multidomain() test_multidomain()
test_expiration_mailer() test_expiration_mailer()
test_caa() test_caa()

View File

@ -69,6 +69,8 @@ GRANT SELECT ON certificates TO 'cert_checker'@'localhost';
-- Expired authorization purger -- Expired authorization purger
GRANT SELECT,DELETE ON pendingAuthorizations TO 'purger'@'localhost'; GRANT SELECT,DELETE ON pendingAuthorizations TO 'purger'@'localhost';
GRANT SELECT,DELETE ON authz TO 'purger'@'localhost';
GRANT SELECT,DELETE ON challenges TO 'purger'@'localhost';
-- Test setup and teardown -- Test setup and teardown
GRANT ALL PRIVILEGES ON * to 'test_setup'@'localhost'; GRANT ALL PRIVILEGES ON * to 'test_setup'@'localhost';

161
va/va.go
View File

@ -105,7 +105,7 @@ func (va ValidationAuthorityImpl) getAddr(ctx context.Context, hostname string)
addrs, err := va.dnsResolver.LookupHost(ctx, hostname) addrs, err := va.dnsResolver.LookupHost(ctx, hostname)
if err != nil { if err != nil {
va.log.Debug(fmt.Sprintf("%s DNS failure: %s", hostname, err)) va.log.Debug(fmt.Sprintf("%s DNS failure: %s", hostname, err))
problem := bdns.ProblemDetailsFromDNSError(err) problem := probs.ConnectionFailure(err.Error())
return net.IP{}, nil, problem return net.IP{}, nil, problem
} }
@ -303,7 +303,7 @@ func certNames(cert *x509.Certificate) []string {
return names return names
} }
func (va *ValidationAuthorityImpl) validateTLSWithZName(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge, zName string) ([]core.ValidationRecord, *probs.ProblemDetails) { func (va *ValidationAuthorityImpl) validateTLSSNI01WithZName(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge, zName string) ([]core.ValidationRecord, *probs.ProblemDetails) {
addr, allAddrs, problem := va.getAddr(ctx, identifier.Value) addr, allAddrs, problem := va.getAddr(ctx, identifier.Value)
validationRecords := []core.ValidationRecord{ validationRecords := []core.ValidationRecord{
{ {
@ -320,32 +320,12 @@ func (va *ValidationAuthorityImpl) validateTLSWithZName(ctx context.Context, ide
portString := strconv.Itoa(va.tlsPort) portString := strconv.Itoa(va.tlsPort)
hostPort := net.JoinHostPort(addr.String(), portString) hostPort := net.JoinHostPort(addr.String(), portString)
validationRecords[0].Port = portString validationRecords[0].Port = portString
va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", challenge.Type, identifier, hostPort, zName))
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: validationTimeout}, "tcp", hostPort, &tls.Config{
ServerName: zName,
InsecureSkipVerify: true,
})
if err != nil { certs, problem := va.getTLSSNICerts(hostPort, identifier, challenge, zName)
va.log.Info(fmt.Sprintf("TLS-01 connection failure for %s. err=[%#v] errStr=[%s]", identifier, err, err)) if problem != nil {
return validationRecords, return validationRecords, problem
parseHTTPConnError(fmt.Sprintf("Failed to connect to %s for TLS-SNI-01 challenge", hostPort), err)
} }
// close errors are not important here
defer func() {
_ = conn.Close()
}()
// Check that zName is a dNSName SAN in the server's certificate
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
va.log.Info(fmt.Sprintf("TLS-SNI-01 challenge for %s resulted in no certificates", identifier.Value))
return validationRecords, probs.Unauthorized("No certs presented for TLS SNI challenge")
}
for i, cert := range certs {
va.log.AuditInfo(fmt.Sprintf("TLS-SNI-01 challenge for %s received certificate (%d of %d): cert=[%s]",
identifier.Value, i+1, len(certs), hex.EncodeToString(cert.Raw)))
}
leafCert := certs[0] leafCert := certs[0]
for _, name := range leafCert.DNSNames { for _, name := range leafCert.DNSNames {
if subtle.ConstantTimeCompare([]byte(name), []byte(zName)) == 1 { if subtle.ConstantTimeCompare([]byte(name), []byte(zName)) == 1 {
@ -355,14 +335,100 @@ func (va *ValidationAuthorityImpl) validateTLSWithZName(ctx context.Context, ide
names := certNames(leafCert) names := certNames(leafCert)
errText := fmt.Sprintf( errText := fmt.Sprintf(
"Incorrect validation certificate for TLS-SNI-01 challenge. "+ "Incorrect validation certificate for %s challenge. "+
"Requested %s from %s. Received %d certificate(s), "+ "Requested %s from %s. Received %d certificate(s), "+
"first certificate had names %q", "first certificate had names %q",
zName, hostPort, len(certs), strings.Join(names, ", ")) challenge.Type, zName, hostPort, len(certs), strings.Join(names, ", "))
va.log.Info(fmt.Sprintf("Remote host failed to give TLS-01 challenge name. host: %s", identifier)) va.log.Info(fmt.Sprintf("Remote host failed to give %s challenge name. host: %s", challenge.Type, identifier))
return validationRecords, probs.Unauthorized(errText) return validationRecords, probs.Unauthorized(errText)
} }
func (va *ValidationAuthorityImpl) validateTLSSNI02WithZNames(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge, sanAName, sanBName string) ([]core.ValidationRecord, *probs.ProblemDetails) {
addr, allAddrs, problem := va.getAddr(ctx, identifier.Value)
validationRecords := []core.ValidationRecord{
{
Hostname: identifier.Value,
AddressesResolved: allAddrs,
AddressUsed: addr,
},
}
if problem != nil {
return validationRecords, problem
}
// Make a connection with SNI = nonceName
portString := strconv.Itoa(va.tlsPort)
hostPort := net.JoinHostPort(addr.String(), portString)
validationRecords[0].Port = portString
certs, problem := va.getTLSSNICerts(hostPort, identifier, challenge, sanAName)
if problem != nil {
return validationRecords, problem
}
leafCert := certs[0]
if len(leafCert.DNSNames) != 2 {
names := strings.Join(certNames(leafCert), ", ")
msg := fmt.Sprintf("%s challenge certificate doesn't include exactly 2 DNSName entries. Received %d certificate(s), first certificate had names %q", challenge.Type, len(certs), names)
return validationRecords, probs.Malformed(msg)
}
var validSanAName, validSanBName bool
for _, name := range leafCert.DNSNames {
// Note: ConstantTimeCompare is not strictly necessary here, but can't hurt.
if subtle.ConstantTimeCompare([]byte(name), []byte(sanAName)) == 1 {
validSanAName = true
}
if subtle.ConstantTimeCompare([]byte(name), []byte(sanBName)) == 1 {
validSanBName = true
}
}
if validSanAName && validSanBName {
return validationRecords, nil
}
names := certNames(leafCert)
errText := fmt.Sprintf(
"Incorrect validation certificate for %s challenge. "+
"Requested %s from %s. Received %d certificate(s), "+
"first certificate had names %q",
challenge.Type, sanAName, hostPort, len(certs), strings.Join(names, ", "))
va.log.Info(fmt.Sprintf("Remote host failed to give %s challenge name. host: %s", challenge.Type, identifier))
return validationRecords, probs.Unauthorized(errText)
}
func (va *ValidationAuthorityImpl) getTLSSNICerts(hostPort string, identifier core.AcmeIdentifier, challenge core.Challenge, zName string) ([]*x509.Certificate, *probs.ProblemDetails) {
va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", challenge.Type, identifier, hostPort, zName))
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: validationTimeout}, "tcp", hostPort, &tls.Config{
ServerName: zName,
InsecureSkipVerify: true,
})
if err != nil {
va.log.Info(fmt.Sprintf("%s connection failure for %s. err=[%#v] errStr=[%s]", challenge.Type, identifier, err, err))
return nil,
parseHTTPConnError(fmt.Sprintf("Failed to connect to %s for %s challenge", hostPort, challenge.Type), err)
}
// close errors are not important here
defer func() {
_ = conn.Close()
}()
// Check that zName is a dNSName SAN in the server's certificate
certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
va.log.Info(fmt.Sprintf("%s challenge for %s resulted in no certificates", challenge.Type, identifier.Value))
return nil, probs.Unauthorized(fmt.Sprintf("No certs presented for %s challenge", challenge.Type))
}
for i, cert := range certs {
va.log.AuditInfo(fmt.Sprintf("%s challenge for %s received certificate (%d of %d): cert=[%s]",
challenge.Type, identifier.Value, i+1, len(certs), hex.EncodeToString(cert.Raw)))
}
return certs, nil
}
func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) { func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) {
if identifier.Type != core.IdentifierDNS { if identifier.Type != core.IdentifierDNS {
va.log.Info(fmt.Sprintf("Got non-DNS identifier for HTTP validation: %s", identifier)) va.log.Info(fmt.Sprintf("Got non-DNS identifier for HTTP validation: %s", identifier))
@ -390,17 +456,38 @@ func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, identifie
func (va *ValidationAuthorityImpl) validateTLSSNI01(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) { func (va *ValidationAuthorityImpl) validateTLSSNI01(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) {
if identifier.Type != "dns" { if identifier.Type != "dns" {
va.log.Info(fmt.Sprintf("Identifier type for TLS-SNI was not DNS: %s", identifier)) va.log.Info(fmt.Sprintf("Identifier type for TLS-SNI-01 was not DNS: %s", identifier))
return nil, probs.Malformed("Identifier type for TLS-SNI was not DNS") return nil, probs.Malformed("Identifier type for TLS-SNI-01 was not DNS")
} }
// Compute the digest that will appear in the certificate // Compute the digest that will appear in the certificate
h := sha256.New() h := sha256.Sum256([]byte(challenge.ProvidedKeyAuthorization))
h.Write([]byte(challenge.ProvidedKeyAuthorization)) Z := hex.EncodeToString(h[:])
Z := hex.EncodeToString(h.Sum(nil))
ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.TLSSNISuffix) ZName := fmt.Sprintf("%s.%s.%s", Z[:32], Z[32:], core.TLSSNISuffix)
return va.validateTLSWithZName(ctx, identifier, challenge, ZName) return va.validateTLSSNI01WithZName(ctx, identifier, challenge, ZName)
}
func (va *ValidationAuthorityImpl) validateTLSSNI02(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) {
if identifier.Type != "dns" {
va.log.Info(fmt.Sprintf("Identifier type for TLS-SNI-02 was not DNS: %s", identifier))
return nil, probs.Malformed("Identifier type for TLS-SNI-02 was not DNS")
}
const tlsSNITokenID = "token"
const tlsSNIKaID = "ka"
// Compute the digest for the SAN b that will appear in the certificate
ha := sha256.Sum256([]byte(challenge.Token))
za := hex.EncodeToString(ha[:])
sanAName := fmt.Sprintf("%s.%s.%s.%s", za[:32], za[32:], tlsSNITokenID, core.TLSSNISuffix)
// Compute the digest for the SAN B that will appear in the certificate
hb := sha256.Sum256([]byte(challenge.ProvidedKeyAuthorization))
zb := hex.EncodeToString(hb[:])
sanBName := fmt.Sprintf("%s.%s.%s.%s", zb[:32], zb[32:], tlsSNIKaID, core.TLSSNISuffix)
return va.validateTLSSNI02WithZNames(ctx, identifier, challenge, sanAName, sanBName)
} }
// badTLSHeader contains the string 'HTTP /' which is returned when // badTLSHeader contains the string 'HTTP /' which is returned when
@ -451,7 +538,7 @@ func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, identifier
if err != nil { if err != nil {
va.log.Info(fmt.Sprintf("Failed to lookup txt records for %s. err=[%#v] errStr=[%s]", identifier, err, err)) va.log.Info(fmt.Sprintf("Failed to lookup txt records for %s. err=[%#v] errStr=[%s]", identifier, err, err))
return nil, bdns.ProblemDetailsFromDNSError(err) return nil, probs.ConnectionFailure(err.Error())
} }
// If there weren't any TXT records return a distinct error message to allow // If there weren't any TXT records return a distinct error message to allow
@ -485,7 +572,7 @@ func (va *ValidationAuthorityImpl) checkCAA(ctx context.Context, identifier core
func (va *ValidationAuthorityImpl) checkCAAInternal(ctx context.Context, ident core.AcmeIdentifier) *probs.ProblemDetails { func (va *ValidationAuthorityImpl) checkCAAInternal(ctx context.Context, ident core.AcmeIdentifier) *probs.ProblemDetails {
present, valid, err := va.checkCAARecords(ctx, ident) present, valid, err := va.checkCAARecords(ctx, ident)
if err != nil { if err != nil {
return bdns.ProblemDetailsFromDNSError(err) return probs.ConnectionFailure(err.Error())
} }
va.log.AuditInfo(fmt.Sprintf( va.log.AuditInfo(fmt.Sprintf(
"Checked CAA records for %s, [Present: %t, Valid for issuance: %t]", "Checked CAA records for %s, [Present: %t, Valid for issuance: %t]",
@ -549,6 +636,8 @@ func (va *ValidationAuthorityImpl) validateChallenge(ctx context.Context, identi
return va.validateHTTP01(ctx, identifier, challenge) return va.validateHTTP01(ctx, identifier, challenge)
case core.ChallengeTypeTLSSNI01: case core.ChallengeTypeTLSSNI01:
return va.validateTLSSNI01(ctx, identifier, challenge) return va.validateTLSSNI01(ctx, identifier, challenge)
case core.ChallengeTypeTLSSNI02:
return va.validateTLSSNI02(ctx, identifier, challenge)
case core.ChallengeTypeDNS01: case core.ChallengeTypeDNS01:
return va.validateDNS01(ctx, identifier, challenge) return va.validateDNS01(ctx, identifier, challenge)
} }

View File

@ -159,12 +159,27 @@ func httpSrv(t *testing.T, token string) *httptest.Server {
return server return server
} }
func tlssniSrv(t *testing.T, chall core.Challenge) *httptest.Server { func tlssni01Srv(t *testing.T, chall core.Challenge) *httptest.Server {
h := sha256.New() h := sha256.Sum256([]byte(chall.ProvidedKeyAuthorization))
h.Write([]byte(chall.ProvidedKeyAuthorization)) Z := hex.EncodeToString(h[:])
Z := hex.EncodeToString(h.Sum(nil))
ZName := fmt.Sprintf("%s.%s.acme.invalid", Z[:32], Z[32:]) ZName := fmt.Sprintf("%s.%s.acme.invalid", Z[:32], Z[32:])
return tlssniSrvWithNames(t, chall, ZName)
}
func tlssni02Srv(t *testing.T, chall core.Challenge) *httptest.Server {
ha := sha256.Sum256([]byte(chall.Token))
za := hex.EncodeToString(ha[:])
sanAName := fmt.Sprintf("%s.%s.token.acme.invalid", za[:32], za[32:])
hb := sha256.Sum256([]byte(chall.ProvidedKeyAuthorization))
zb := hex.EncodeToString(hb[:])
sanBName := fmt.Sprintf("%s.%s.ka.acme.invalid", zb[:32], zb[32:])
return tlssniSrvWithNames(t, chall, sanAName, sanBName)
}
func tlssniSrvWithNames(t *testing.T, chall core.Challenge, names ...string) *httptest.Server {
template := &x509.Certificate{ template := &x509.Certificate{
SerialNumber: big.NewInt(1337), SerialNumber: big.NewInt(1337),
Subject: pkix.Name{ Subject: pkix.Name{
@ -177,7 +192,7 @@ func tlssniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true, BasicConstraintsValid: true,
DNSNames: []string{ZName}, DNSNames: names,
} }
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey) certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
@ -190,7 +205,7 @@ func tlssniSrv(t *testing.T, chall core.Challenge) *httptest.Server {
Certificates: []tls.Certificate{*cert}, Certificates: []tls.Certificate{*cert},
ClientAuth: tls.NoClientCert, ClientAuth: tls.NoClientCert,
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if clientHello.ServerName != ZName { if clientHello.ServerName != names[0] {
time.Sleep(time.Second * 10) time.Sleep(time.Second * 10)
return nil, nil return nil, nil
} }
@ -431,10 +446,10 @@ func getPort(hs *httptest.Server) (int, error) {
return int(port), nil return int(port), nil
} }
func TestTLSSNI(t *testing.T) { func TestTLSSNI01(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01) chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssniSrv(t, chall) hs := tlssni01Srv(t, chall)
port, err := getPort(hs) port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port") test.AssertNotError(t, err, "failed to get test server port")
@ -443,7 +458,7 @@ func TestTLSSNI(t *testing.T) {
_, prob := va.validateTLSSNI01(ctx, ident, chall) _, prob := va.validateTLSSNI01(ctx, ident, chall)
if prob != nil { if prob != nil {
t.Fatalf("Unexpected failure in validateTLSSNI01: %s", prob) t.Fatalf("Unexpected failure in validate TLS-SNI-01: %s", prob)
} }
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1) test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
if len(log.GetAllMatching(`challenge for localhost received certificate \(1 of 1\): cert=\[`)) != 1 { if len(log.GetAllMatching(`challenge for localhost received certificate \(1 of 1\): cert=\[`)) != 1 {
@ -501,11 +516,88 @@ func TestTLSSNI(t *testing.T) {
log.Clear() log.Clear()
_, err = va.validateTLSSNI01(ctx, ident, chall) _, err = va.validateTLSSNI01(ctx, ident, chall)
test.AssertError(t, err, "TLS SNI validation passed when talking to a HTTP-only server") test.AssertError(t, err, "TLS-SNI-01 validation passed when talking to a HTTP-only server")
test.Assert(t, strings.HasSuffix( test.Assert(t, strings.HasSuffix(
err.Error(), err.Error(),
"Server only speaks HTTP, not TLS", "Server only speaks HTTP, not TLS",
), "validateTLSSNI01 didn't return useful error") ), "validate TLS-SNI-01 didn't return useful error")
}
func TestTLSSNI02(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI02)
hs := tlssni02Srv(t, chall)
port, err := getPort(hs)
test.AssertNotError(t, err, "failed to get test server port")
va, _, log := setup()
va.tlsPort = port
_, prob := va.validateTLSSNI02(ctx, ident, chall)
if prob != nil {
t.Fatalf("Unexpected failure in validate TLS-SNI-02: %s", prob)
}
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
if len(log.GetAllMatching(`challenge for localhost received certificate \(1 of 1\): cert=\[`)) != 1 {
t.Errorf("Didn't get log message with validated certificate. Instead got:\n%s",
strings.Join(log.GetAllMatching(".*"), "\n"))
}
log.Clear()
_, prob = va.validateTLSSNI02(ctx, core.AcmeIdentifier{
Type: core.IdentifierType("ip"),
Value: net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port)),
}, chall)
if prob == nil {
t.Fatalf("IdentifierType IP shouldn't have worked.")
}
test.AssertEquals(t, prob.Type, probs.MalformedProblem)
log.Clear()
_, prob = va.validateTLSSNI02(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "always.invalid"}, chall)
if prob == nil {
t.Fatalf("Domain name was supposed to be invalid.")
}
test.AssertEquals(t, prob.Type, probs.UnknownHostProblem)
// Need to create a new authorized keys object to get an unknown SNI (from the signature value)
chall.Token = core.NewToken()
chall.ProvidedKeyAuthorization = "invalid"
log.Clear()
started := time.Now()
_, prob = va.validateTLSSNI02(ctx, ident, chall)
took := time.Since(started)
if prob == nil {
t.Fatalf("Validation should have failed")
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
// Check that the TLS connection times out after 5 seconds and doesn't block for 10 seconds
test.Assert(t, (took > (time.Second * 5)), "TLS returned before 5 seconds")
test.Assert(t, (took < (time.Second * 10)), "TLS connection didn't timeout after 5 seconds")
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
// Take down validation server and check that validation fails.
hs.Close()
_, err = va.validateTLSSNI02(ctx, ident, chall)
if err == nil {
t.Fatalf("Server's down; expected refusal. Where did we connect?")
}
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
httpOnly := httpSrv(t, "")
defer httpOnly.Close()
port, err = getPort(httpOnly)
test.AssertNotError(t, err, "failed to get test server port")
va.tlsPort = port
log.Clear()
_, err = va.validateTLSSNI02(ctx, ident, chall)
test.AssertError(t, err, "TLS-SNI-02 validation passed when talking to a HTTP-only server")
test.Assert(t, strings.HasSuffix(
err.Error(),
"Server only speaks HTTP, not TLS",
), "validate TLS-SNI-02 didn't return useful error")
} }
func brokenTLSSrv() *httptest.Server { func brokenTLSSrv() *httptest.Server {
@ -664,7 +756,7 @@ func setChallengeToken(ch *core.Challenge, token string) {
func TestValidateTLSSNI01(t *testing.T) { func TestValidateTLSSNI01(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01) chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssniSrv(t, chall) hs := tlssni01Srv(t, chall)
defer hs.Close() defer hs.Close()
port, err := getPort(hs) port, err := getPort(hs)
@ -678,7 +770,7 @@ func TestValidateTLSSNI01(t *testing.T) {
test.Assert(t, prob == nil, "validation failed") test.Assert(t, prob == nil, "validation failed")
} }
func TestValidateTLSSNINotSane(t *testing.T) { func TestValidateTLSSNI01NotSane(t *testing.T) {
va, _, _ := setup() va, _, _ := setup()
chall := createChallenge(core.ChallengeTypeTLSSNI01) chall := createChallenge(core.ChallengeTypeTLSSNI01)
@ -925,7 +1017,7 @@ func TestDNSValidationNoAuthorityOK(t *testing.T) {
func TestCAAFailure(t *testing.T) { func TestCAAFailure(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01) chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssniSrv(t, chall) hs := tlssni01Srv(t, chall)
defer hs.Close() defer hs.Close()
port, err := getPort(hs) port, err := getPort(hs)
@ -1005,7 +1097,7 @@ func TestCheckCAAFallback(t *testing.T) {
prob = va.checkCAA(ctx, core.AcmeIdentifier{Value: "bad-local-resolver.com", Type: "dns"}) prob = va.checkCAA(ctx, core.AcmeIdentifier{Value: "bad-local-resolver.com", Type: "dns"})
test.Assert(t, prob != nil, "returned ProblemDetails was nil") test.Assert(t, prob != nil, "returned ProblemDetails was nil")
test.AssertEquals(t, prob.Type, probs.ConnectionProblem) test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
test.AssertEquals(t, prob.Detail, "server failure at resolver") test.AssertEquals(t, prob.Detail, "DNS problem: query timed out looking up CAA for bad-local-resolver.com")
} }
func TestParseResults(t *testing.T) { func TestParseResults(t *testing.T) {

View File

@ -3,10 +3,10 @@ package wfe
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rsa" "crypto/rsa"
"fmt"
"github.com/letsencrypt/boulder/core"
"gopkg.in/square/go-jose.v1" "gopkg.in/square/go-jose.v1"
berrors "github.com/letsencrypt/boulder/errors"
) )
func algorithmForKey(key *jose.JsonWebKey) (string, error) { func algorithmForKey(key *jose.JsonWebKey) (string, error) {
@ -23,7 +23,7 @@ func algorithmForKey(key *jose.JsonWebKey) (string, error) {
return string(jose.ES512), nil return string(jose.ES512), nil
} }
} }
return "", core.SignatureValidationError("no signature algorithms suitable for given key type") return "", berrors.SignatureValidationError("no signature algorithms suitable for given key type")
} }
const ( const (
@ -44,15 +44,16 @@ func checkAlgorithm(key *jose.JsonWebKey, parsedJws *jose.JsonWebSignature) (str
} }
jwsAlgorithm := parsedJws.Signatures[0].Header.Algorithm jwsAlgorithm := parsedJws.Signatures[0].Header.Algorithm
if jwsAlgorithm != algorithm { if jwsAlgorithm != algorithm {
return invalidJWSAlgorithm, return invalidJWSAlgorithm, berrors.SignatureValidationError(
core.SignatureValidationError(fmt.Sprintf(
"signature type '%s' in JWS header is not supported, expected one of RS256, ES256, ES384 or ES512", "signature type '%s' in JWS header is not supported, expected one of RS256, ES256, ES384 or ES512",
jwsAlgorithm)) jwsAlgorithm,
)
} }
if key.Algorithm != "" && key.Algorithm != algorithm { if key.Algorithm != "" && key.Algorithm != algorithm {
return invalidAlgorithmOnKey, return invalidAlgorithmOnKey, berrors.SignatureValidationError(
core.SignatureValidationError(fmt.Sprintf( "algorithm '%s' on JWK is unacceptable",
"algorithm '%s' on JWK is unacceptable", key.Algorithm)) key.Algorithm,
)
} }
return "", nil return "", nil
} }

80
wfe/probs.go Normal file
View File

@ -0,0 +1,80 @@
package wfe
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, berrors.SignatureValidation:
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, berrors.TooManyRequests:
// Internal server error messages may include sensitive data, so we do
// not include it.
return probs.ServerInternal(msg)
case berrors.RejectedIdentifier:
return probs.RejectedIdentifier(msg)
case berrors.UnsupportedIdentifier:
return probs.UnsupportedIdentifier(msg)
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 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.SignatureValidationError:
return probs.Malformed(fmt.Sprintf("%s :: %s", msg, err))
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)
}
}

55
wfe/probs_test.go Normal file
View File

@ -0,0 +1,55 @@
package wfe
import (
"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) {
testCases := []struct {
err error
statusCode int
problem probs.ProblemType
}{
// boulder/core error types
{core.InternalServerError("foo"), 500, probs.ServerInternalProblem},
{core.NotSupportedError("foo"), 501, probs.ServerInternalProblem},
{core.MalformedRequestError("foo"), 400, probs.MalformedProblem},
{core.UnauthorizedError("foo"), 403, probs.UnauthorizedProblem},
{core.NotFoundError("foo"), 404, probs.MalformedProblem},
{core.SignatureValidationError("foo"), 400, probs.MalformedProblem},
{core.RateLimitedError("foo"), 429, probs.RateLimitedProblem},
{core.LengthRequiredError("foo"), 411, probs.MalformedProblem},
{core.BadNonceError("foo"), 400, probs.BadNonceProblem},
// boulder/errors error types
{berrors.InternalServerError("foo"), 500, probs.ServerInternalProblem},
{berrors.NotSupportedError("foo"), 501, probs.ServerInternalProblem},
{berrors.MalformedError("foo"), 400, probs.MalformedProblem},
{berrors.UnauthorizedError("foo"), 403, probs.UnauthorizedProblem},
{berrors.NotFoundError("foo"), 404, probs.MalformedProblem},
{berrors.SignatureValidationError("foo"), 400, probs.MalformedProblem},
{berrors.RateLimitError("foo"), 429, probs.RateLimitedProblem},
}
for _, c := range testCases {
p := problemDetailsForError(c.err, "k")
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)
}
}
expected := &probs.ProblemDetails{
Type: probs.MalformedProblem,
HTTPStatus: 200,
Detail: "gotcha",
}
p := problemDetailsForError(expected, "k")
test.AssertDeepEquals(t, expected, p)
}

View File

@ -22,6 +22,7 @@ import (
jose "gopkg.in/square/go-jose.v1" jose "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
@ -464,7 +465,9 @@ func (wfe *WebFrontEndImpl) verifyPOST(ctx context.Context, logEvent *requestEve
// Special case: If no registration was found, but regCheck is false, use an // Special case: If no registration was found, but regCheck is false, use an
// empty registration and the submitted key. The caller is expected to do some // empty registration and the submitted key. The caller is expected to do some
// validation on the returned key. // validation on the returned key.
if _, ok := err.(core.NoSuchRegistrationError); ok && !regCheck { // TODO(#2600): Remove core.NoSuchRegistrationError check once boulder/errors
// code is deployed
if _, ok := err.(core.NoSuchRegistrationError); (ok || berrors.Is(err, berrors.NotFound)) && !regCheck {
// When looking up keys from the registrations DB, we can be confident they // When looking up keys from the registrations DB, we can be confident they
// are "good". But when we are verifying against any submitted key, we want // are "good". But when we are verifying against any submitted key, we want
// to check its quality before doing the verify. // to check its quality before doing the verify.
@ -478,7 +481,9 @@ func (wfe *WebFrontEndImpl) verifyPOST(ctx context.Context, logEvent *requestEve
// For all other errors, or if regCheck is true, return error immediately. // For all other errors, or if regCheck is true, return error immediately.
wfe.stats.Inc("Errors.UnableToGetRegistrationByKey", 1) wfe.stats.Inc("Errors.UnableToGetRegistrationByKey", 1)
logEvent.AddError("unable to fetch registration by the given JWK: %s", err) logEvent.AddError("unable to fetch registration by the given JWK: %s", err)
if _, ok := err.(core.NoSuchRegistrationError); ok { // TODO(#2600): Remove core.NoSuchRegistrationError check once boulder/errors
// code is deployed
if _, ok := err.(core.NoSuchRegistrationError); ok || berrors.Is(err, berrors.NotFound) {
return nil, nil, reg, probs.Unauthorized(unknownKey) return nil, nil, reg, probs.Unauthorized(unknownKey)
} }
@ -630,7 +635,7 @@ func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *reque
reg, err := wfe.RA.NewRegistration(ctx, init) reg, err := wfe.RA.NewRegistration(ctx, init)
if err != nil { if err != nil {
logEvent.AddError("unable to create new registration: %s", err) logEvent.AddError("unable to create new registration: %s", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error creating new registration"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new registration"), err)
return return
} }
logEvent.Requester = reg.ID logEvent.Requester = reg.ID
@ -686,7 +691,7 @@ func (wfe *WebFrontEndImpl) NewAuthorization(ctx context.Context, logEvent *requ
authz, err := wfe.RA.NewAuthorization(ctx, init, currReg.ID) authz, err := wfe.RA.NewAuthorization(ctx, init, currReg.ID)
if err != nil { if err != nil {
logEvent.AddError("unable to create new authz: %s", err) logEvent.AddError("unable to create new authz: %s", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error creating new authz"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new authz"), err)
return return
} }
logEvent.Extra["AuthzID"] = authz.ID logEvent.Extra["AuthzID"] = authz.ID
@ -816,7 +821,7 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(ctx context.Context, logEvent *req
err = wfe.RA.RevokeCertificateWithReg(ctx, *parsedCertificate, reason, registration.ID) err = wfe.RA.RevokeCertificateWithReg(ctx, *parsedCertificate, reason, registration.ID)
if err != nil { if err != nil {
logEvent.AddError("failed to revoke certificate: %s", err) logEvent.AddError("failed to revoke certificate: %s", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Failed to revoke certificate"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Failed to revoke certificate"), err)
} else { } else {
wfe.log.Debug(fmt.Sprintf("Revoked %v", serial)) wfe.log.Debug(fmt.Sprintf("Revoked %v", serial))
response.WriteHeader(http.StatusOK) response.WriteHeader(http.StatusOK)
@ -911,7 +916,7 @@ func (wfe *WebFrontEndImpl) NewCertificate(ctx context.Context, logEvent *reques
cert, err := wfe.RA.NewCertificate(ctx, certificateRequest, reg.ID) cert, err := wfe.RA.NewCertificate(ctx, certificateRequest, reg.ID)
if err != nil { if err != nil {
logEvent.AddError("unable to create new cert: %s", err) logEvent.AddError("unable to create new cert: %s", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error creating new cert"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new cert"), err)
return return
} }
@ -1103,7 +1108,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
updatedAuthorization, err := wfe.RA.UpdateAuthorization(ctx, authz, challengeIndex, challengeUpdate) updatedAuthorization, err := wfe.RA.UpdateAuthorization(ctx, authz, challengeIndex, challengeUpdate)
if err != nil { if err != nil {
logEvent.AddError("unable to update challenge: %s", err) logEvent.AddError("unable to update challenge: %s", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Unable to update challenge"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Unable to update challenge"), err)
return return
} }
@ -1205,7 +1210,7 @@ func (wfe *WebFrontEndImpl) Registration(ctx context.Context, logEvent *requestE
updatedReg, err := wfe.RA.UpdateRegistration(ctx, currReg, update) updatedReg, err := wfe.RA.UpdateRegistration(ctx, currReg, update)
if err != nil { if err != nil {
logEvent.AddError("unable to update registration: %s", err) logEvent.AddError("unable to update registration: %s", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Unable to update registration"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Unable to update registration"), err)
return return
} }
@ -1251,7 +1256,7 @@ func (wfe *WebFrontEndImpl) deactivateAuthorization(ctx context.Context, authz *
err = wfe.RA.DeactivateAuthorization(ctx, *authz) err = wfe.RA.DeactivateAuthorization(ctx, *authz)
if err != nil { if err != nil {
logEvent.AddError("unable to deactivate authorization", err) logEvent.AddError("unable to deactivate authorization", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error deactivating authorization"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Error deactivating authorization"), err)
return false return false
} }
// Since the authorization passed to DeactivateAuthorization isn't // Since the authorization passed to DeactivateAuthorization isn't
@ -1501,7 +1506,7 @@ func (wfe *WebFrontEndImpl) KeyRollover(ctx context.Context, logEvent *requestEv
updatedReg, err := wfe.RA.UpdateRegistration(ctx, reg, core.Registration{Key: newKey}) updatedReg, err := wfe.RA.UpdateRegistration(ctx, reg, core.Registration{Key: newKey})
if err != nil { if err != nil {
logEvent.AddError("unable to update registration: %s", err) logEvent.AddError("unable to update registration: %s", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Unable to update registration"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Unable to update registration"), err)
return return
} }
@ -1520,7 +1525,7 @@ func (wfe *WebFrontEndImpl) deactivateRegistration(ctx context.Context, reg core
err := wfe.RA.DeactivateRegistration(ctx, reg) err := wfe.RA.DeactivateRegistration(ctx, reg)
if err != nil { if err != nil {
logEvent.AddError("unable to deactivate registration", err) logEvent.AddError("unable to deactivate registration", err)
wfe.sendError(response, logEvent, core.ProblemDetailsForError(err, "Error deactivating registration"), err) wfe.sendError(response, logEvent, problemDetailsForError(err, "Error deactivating registration"), err)
return return
} }
reg.Status = core.StatusDeactivated reg.Status = core.StatusDeactivated

View File

@ -25,6 +25,7 @@ import (
"gopkg.in/square/go-jose.v1" "gopkg.in/square/go-jose.v1"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
@ -809,7 +810,7 @@ func TestIssueCertificate(t *testing.T) {
}`, wfe.nonceService))) }`, wfe.nonceService)))
assertJSONEquals(t, assertJSONEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:unauthorized","detail":"Error creating new cert :: Authorizations for these names not found or expired: meep.com","status":403}`) `{"type":"urn:acme:error:unauthorized","detail":"Error creating new cert :: authorizations for these names not found or expired: meep.com","status":403}`)
assertCsrLogged(t, mockLog) assertCsrLogged(t, mockLog)
mockLog.Clear() mockLog.Clear()
@ -1209,16 +1210,16 @@ func makeRevokeRequestJSON(reason *revocation.Reason) ([]byte, error) {
return revokeRequestJSON, nil return revokeRequestJSON, nil
} }
// An SA mock that always returns NoSuchRegistrationError. This is necessary // An SA mock that always returns a berrors.NotFound type error. This is necessary
// because the standard mock in our mocks package always returns a given test // because the standard mock in our mocks package always returns a given test
// registration when GetRegistrationByKey is called, and we want to get a // registration when GetRegistrationByKey is called, and we want to get a
// NoSuchRegistrationError for tests that pass regCheck = false to verifyPOST. // berrors.NotFound type error for tests that pass regCheck = false to verifyPOST.
type mockSANoSuchRegistration struct { type mockSANoSuchRegistration struct {
core.StorageGetter core.StorageGetter
} }
func (msa mockSANoSuchRegistration) GetRegistrationByKey(ctx context.Context, jwk *jose.JsonWebKey) (core.Registration, error) { func (msa mockSANoSuchRegistration) GetRegistrationByKey(ctx context.Context, jwk *jose.JsonWebKey) (core.Registration, error) {
return core.Registration{}, core.NoSuchRegistrationError("reg not found") return core.Registration{}, berrors.NotFoundError("reg not found")
} }
// Valid revocation request for existing, non-revoked cert, signed with cert // Valid revocation request for existing, non-revoked cert, signed with cert
@ -1825,7 +1826,7 @@ func TestBadKeyCSR(t *testing.T) {
assertJSONEquals(t, assertJSONEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Invalid key in certificate request :: Key too small: 512","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Invalid key in certificate request :: key too small: 512","status":400}`)
} }
// This uses httptest.NewServer because ServeMux.ServeHTTP won't prevent the // This uses httptest.NewServer because ServeMux.ServeHTTP won't prevent the