From 43040e7f042fb8f902fd2e078377e2d00d8bdc73 Mon Sep 17 00:00:00 2001 From: Jeff Hodges Date: Wed, 7 Oct 2015 20:20:20 -0700 Subject: [PATCH 1/9] bump go to 1.5.1 in travis We don't want to put multiple versions in our config because it'll launch another set of sub-builds and eat up our quota. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 99d11742b..64fa7da8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.5 + - 1.5.1 addons: hosts: From 487d08ec2e4af3a35a7ebf5c7bd8d5a6ce76256c Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 8 Oct 2015 15:20:22 -0700 Subject: [PATCH 2/9] Add rate limiting by registration IP. --- cmd/cert-checker/main_test.go | 4 +- cmd/expiration-mailer/main_test.go | 13 ++- cmd/rate-limits.go | 4 + core/interfaces.go | 1 + core/objects.go | 6 ++ mocks/mocks.go | 13 ++- ra/registration-authority.go | 24 +++++ ra/registration-authority_test.go | 12 ++- rpc/rpc-wrappers.go | 39 ++++++++ ...0151008234926_AddInitialIPAndCreatedAt.sql | 14 +++ sa/ip_range_test.go | 57 ++++++++++++ sa/model.go | 12 +++ sa/satest/satest.go | 8 +- sa/storage-authority.go | 90 ++++++++++++++++++- sa/storage-authority_test.go | 71 +++++++++++++-- test/rate-limit-policies.yml | 5 ++ wfe/web-front-end.go | 12 +++ wfe/web-front-end_test.go | 11 +-- 18 files changed, 366 insertions(+), 30 deletions(-) create mode 100644 sa/_db/migrations/20151008234926_AddInitialIPAndCreatedAt.sql create mode 100644 sa/ip_range_test.go diff --git a/cmd/cert-checker/main_test.go b/cmd/cert-checker/main_test.go index 57b40e2b4..47c01d3d7 100644 --- a/cmd/cert-checker/main_test.go +++ b/cmd/cert-checker/main_test.go @@ -205,9 +205,7 @@ func TestGetAndProcessCerts(t *testing.T) { BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, } - reg, err := sa.NewRegistration(core.Registration{ - Key: satest.GoodJWK(), - }) + reg := satest.CreateWorkingRegistration(t, sa) test.AssertNotError(t, err, "Couldn't create registration") for i := int64(0); i < 5; i++ { rawCert.SerialNumber = big.NewInt(i) diff --git a/cmd/expiration-mailer/main_test.go b/cmd/expiration-mailer/main_test.go index 5dbc1d2f0..147a3c1f6 100644 --- a/cmd/expiration-mailer/main_test.go +++ b/cmd/expiration-mailer/main_test.go @@ -14,6 +14,7 @@ import ( "encoding/json" "fmt" "math/big" + "net" "testing" "text/template" "time" @@ -171,14 +172,16 @@ func TestFindExpiringCertificates(t *testing.T) { Contact: []*core.AcmeURL{ emailA, }, - Key: keyA, + Key: keyA, + InitialIP: net.ParseIP("2.3.2.3"), } regB := core.Registration{ ID: 2, Contact: []*core.AcmeURL{ emailB, }, - Key: keyB, + Key: keyB, + InitialIP: net.ParseIP("2.3.2.3"), } regA, err = ctx.ssa.NewRegistration(regA) if err != nil { @@ -298,7 +301,8 @@ func TestLifetimeOfACert(t *testing.T) { Contact: []*core.AcmeURL{ emailA, }, - Key: keyA, + Key: keyA, + InitialIP: net.ParseIP("1.2.2.1"), } regA, err = ctx.ssa.NewRegistration(regA) if err != nil { @@ -401,7 +405,8 @@ func TestDontFindRevokedCert(t *testing.T) { Contact: []*core.AcmeURL{ emailA, }, - Key: keyA, + Key: keyA, + InitialIP: net.ParseIP("6.5.5.6"), } regA, err = ctx.ssa.NewRegistration(regA) if err != nil { diff --git a/cmd/rate-limits.go b/cmd/rate-limits.go index 92afdfabd..997b9ffc3 100644 --- a/cmd/rate-limits.go +++ b/cmd/rate-limits.go @@ -17,6 +17,10 @@ type RateLimitConfig struct { // These are counted by "base domain" aka eTLD+1, so any entries in the // overrides section must be an eTLD+1 according to the publicsuffix package. CertificatesPerName RateLimitPolicy `yaml:"certificatesPerName"` + // Number of registrations that can be created per IP. + // Note: Since this is checked before a registration is created, setting a + // RegistrationOverride on it has no effect. + RegistrationsPerIP RateLimitPolicy `yaml:"registrationsPerIP"` } // RateLimitPolicy describes a general limiting policy diff --git a/core/interfaces.go b/core/interfaces.go index 7a8a4533b..da3a29264 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -112,6 +112,7 @@ type StorageGetter interface { AlreadyDeniedCSR([]string) (bool, error) CountCertificatesRange(time.Time, time.Time) (int64, error) CountCertificatesByNames([]string, time.Time, time.Time) (map[string]int, error) + CountRegistrationsByIP(net.IP, time.Time, time.Time) (int, error) GetSCTReceipt(string, string) (SignedCertificateTimestamp, error) } diff --git a/core/objects.go b/core/objects.go index 210fd9eea..cf513e724 100644 --- a/core/objects.go +++ b/core/objects.go @@ -168,6 +168,12 @@ type Registration struct { // Agreement with terms of service Agreement string `json:"agreement,omitempty"` + + // InitialIP is the IP address from which the registration was created + InitialIP net.IP `json:"initialIp"` + + // CreatedAt is the time the registration was created. + CreatedAt time.Time `json:"createdAt"` } // MergeUpdate copies a subset of information from the input Registration diff --git a/mocks/mocks.go b/mocks/mocks.go index 737473644..bc05f8cbf 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -163,7 +163,13 @@ func (sa *StorageAuthority) GetRegistration(id int64) (core.Registration, error) var parsedKey jose.JsonWebKey parsedKey.UnmarshalJSON(keyJSON) - return core.Registration{ID: id, Key: parsedKey, Agreement: agreementURL}, nil + return core.Registration{ + ID: id, + Key: parsedKey, + Agreement: agreementURL, + InitialIP: net.ParseIP("5.6.7.8"), + CreatedAt: time.Date(2003, 9, 27, 0, 0, 0, 0, time.UTC), + }, nil } // GetRegistrationByKey is a mock @@ -325,6 +331,11 @@ func (sa *StorageAuthority) CountCertificatesByNames(_ []string, _, _ time.Time) return } +// CountRegistrationsByIP is a mock +func (sa *StorageAuthority) CountRegistrationsByIP(_ net.IP, _, _ time.Time) (int, error) { + return 0, nil +} + // Publisher is a mock type Publisher struct { // empty diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 5c8f161c9..2e99a79dd 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "errors" "fmt" + "net" "net/mail" "reflect" "sort" @@ -138,16 +139,39 @@ func (ra *RegistrationAuthorityImpl) setIssuanceCount() (int, error) { return ra.totalIssuedCache, nil } +func (ra *RegistrationAuthorityImpl) checkRegistrationLimit(ip net.IP) error { + limit := ra.rlPolicies.RegistrationsPerIP + if limit.Enabled() { + now := ra.clk.Now() + count, err := ra.SA.CountRegistrationsByIP(ip, limit.WindowBegin(now), now) + if err != nil { + return err + } + if count >= limit.GetThreshold(ip.String(), -1) { + return core.RateLimitedError("Too many registrations from this IP") + } + } + return nil +} + // NewRegistration constructs a new Registration from a request. func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration) (reg core.Registration, err error) { if err = core.GoodKey(init.Key.Key); err != nil { return core.Registration{}, core.MalformedRequestError(fmt.Sprintf("Invalid public key: %s", err.Error())) } + if err = ra.checkRegistrationLimit(init.InitialIP); err != nil { + return core.Registration{}, err + } + reg = core.Registration{ Key: init.Key, } reg.MergeUpdate(init) + // This field isn't updatable by the end user, so it isn't copied by + // MergeUpdate. But we need to fill it in for new registrations. + reg.InitialIP = init.InitialIP + err = validateContacts(reg.Contact, ra.DNSResolver, ra.stats) if err != nil { return diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index 77959a3a9..d2f2562c0 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -13,6 +13,7 @@ import ( "encoding/json" "encoding/pem" "fmt" + "net" "net/url" "testing" "time" @@ -212,7 +213,10 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut csrDER, _ := hex.DecodeString(CSRhex) ExampleCSR, _ = x509.ParseCertificateRequest(csrDER) - Registration, _ = ssa.NewRegistration(core.Registration{Key: AccountKeyA}) + Registration, _ = ssa.NewRegistration(core.Registration{ + Key: AccountKeyA, + InitialIP: net.ParseIP("3.2.3.3"), + }) stats, _ := statsd.NewNoopClient() ra := NewRegistrationAuthorityImpl(fc, blog.GetAuditLogger(), stats, cmd.RateLimitConfig{ @@ -303,8 +307,9 @@ func TestNewRegistration(t *testing.T) { defer cleanUp() mailto, _ := core.ParseAcmeURL("mailto:foo@letsencrypt.org") input := core.Registration{ - Contact: []*core.AcmeURL{mailto}, - Key: AccountKeyB, + Contact: []*core.AcmeURL{mailto}, + Key: AccountKeyB, + InitialIP: net.ParseIP("7.6.6.5"), } result, err := ra.NewRegistration(input) @@ -332,6 +337,7 @@ func TestNewRegistrationNoFieldOverwrite(t *testing.T) { Key: AccountKeyC, Contact: []*core.AcmeURL{mailto}, Agreement: "I agreed", + InitialIP: net.ParseIP("5.0.5.0"), } result, err := ra.NewRegistration(input) diff --git a/rpc/rpc-wrappers.go b/rpc/rpc-wrappers.go index 47aeddd52..c6700f9e7 100644 --- a/rpc/rpc-wrappers.go +++ b/rpc/rpc-wrappers.go @@ -10,6 +10,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "time" jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" @@ -62,6 +63,7 @@ const ( MethodAlreadyDeniedCSR = "AlreadyDeniedCSR" // SA MethodCountCertificatesRange = "CountCertificatesRange" // SA MethodCountCertificatesByNames = "CountCertificatesByNames" // SA + MethodCountRegistrationsByIP = "CountRegistrationsByIP" // SA MethodGetSCTReceipt = "GetSCTReceipt" // SA MethodAddSCTReceipt = "AddSCTReceipt" // SA MethodSubmitToCT = "SubmitToCT" // Pub @@ -151,6 +153,12 @@ type countCertificatesByNamesRequest struct { Latest time.Time } +type countRegistrationsByIPRequest struct { + IP net.IP + Earliest time.Time + Latest time.Time +} + // Response structs type caaResponse struct { Present bool @@ -1042,6 +1050,20 @@ func NewStorageAuthorityServer(rpc Server, impl core.StorageAuthority) error { return json.Marshal(counts) }) + rpc.Handle(MethodCountRegistrationsByIP, func(req []byte) (response []byte, err error) { + var cReq countRegistrationsByIPRequest + err = json.Unmarshal(req, &cReq) + if err != nil { + return + } + + count, err := impl.CountRegistrationsByIP(cReq.IP, cReq.Earliest, cReq.Latest) + if err != nil { + return + } + return json.Marshal(count) + }) + rpc.Handle(MethodGetSCTReceipt, func(req []byte) (response []byte, err error) { var gsctReq struct { Serial string @@ -1370,6 +1392,23 @@ func (cac StorageAuthorityClient) CountCertificatesByNames(names []string, earli return } +// CountRegistrationsByIP calls CountRegistrationsByIP on the remote +// StorageAuthority. +func (cac StorageAuthorityClient) CountRegistrationsByIP(ip net.IP, earliest, latest time.Time) (count int, err error) { + var cReq countRegistrationsByIPRequest + cReq.IP, cReq.Earliest, cReq.Latest = ip, earliest, latest + data, err := json.Marshal(cReq) + if err != nil { + return + } + response, err := cac.rpc.DispatchSync(MethodCountRegistrationsByIP, data) + if err != nil { + return + } + err = json.Unmarshal(response, &count) + return +} + // GetSCTReceipt retrieves an SCT according to the serial number of a certificate // and the logID of the log to which it was submitted. func (cac StorageAuthorityClient) GetSCTReceipt(serial string, logID string) (receipt core.SignedCertificateTimestamp, err error) { diff --git a/sa/_db/migrations/20151008234926_AddInitialIPAndCreatedAt.sql b/sa/_db/migrations/20151008234926_AddInitialIPAndCreatedAt.sql new file mode 100644 index 000000000..2b225a38b --- /dev/null +++ b/sa/_db/migrations/20151008234926_AddInitialIPAndCreatedAt.sql @@ -0,0 +1,14 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `registrations` ADD COLUMN ( + `initialIP` BINARY(16) NOT NULL DEFAULT "", + `createdAt` DATETIME NOT NULL +); +CREATE INDEX `initialIP_createdAt` on `registrations` (`initialIP`, `createdAt`); + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back + +DROP INDEX `initialIP_createdAt` on `registrations`; diff --git a/sa/ip_range_test.go b/sa/ip_range_test.go new file mode 100644 index 000000000..acf45ad57 --- /dev/null +++ b/sa/ip_range_test.go @@ -0,0 +1,57 @@ +// Copyright 2014 ISRG. All rights reserved +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package sa + +import ( + "net" + "testing" +) + +func TestIncrementIP(t *testing.T) { + testCases := []struct { + ip string + index int + expected string + }{ + {"0.0.0.0", 15, "0.0.0.1"}, + {"0.0.0.255", 15, "0.0.1.0"}, + {"127.0.0.1", 15, "127.0.0.2"}, + {"1.2.3.4", 14, "1.2.4.4"}, + {"::1", 15, "::2"}, + {"2002:1001:4008::", 15, "2002:1001:4008::1"}, + {"2002:1001:4008::", 5, "2002:1001:4009::"}, + } + for _, tc := range testCases { + ip := net.ParseIP(tc.ip).To16() + incrementIP(&ip, tc.index) + expectedIP := net.ParseIP(tc.expected) + if !ip.Equal(expectedIP) { + t.Errorf("Expected incrementIP(%s, %d) to be %s, instead got %s", + tc.ip, tc.index, expectedIP, ip.String()) + } + } +} + +func TestIPRange(t *testing.T) { + testCases := []struct { + ip string + expectedBegin string + expectedEnd string + }{ + {"28.45.45.28", "28.45.45.28", "28.45.45.29"}, + {"2002:1001:4008::", "2002:1001:4008::", "2002:1001:4009::"}, + } + for _, tc := range testCases { + ip := net.ParseIP(tc.ip) + expectedBegin := net.ParseIP(tc.expectedBegin) + expectedEnd := net.ParseIP(tc.expectedEnd) + actualBegin, actualEnd := ipRange(ip) + if !expectedBegin.Equal(actualBegin) || !expectedEnd.Equal(actualEnd) { + t.Errorf("Expected ipRange(%s) to be (%s, %s), got (%s, %s)", + tc.ip, tc.expectedBegin, tc.expectedEnd, actualBegin, actualEnd) + } + } +} diff --git a/sa/model.go b/sa/model.go index 8819608da..d7651eaa4 100644 --- a/sa/model.go +++ b/sa/model.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "math" + "net" "time" jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" @@ -31,6 +32,10 @@ type regModel struct { KeySHA256 string `db:"jwk_sha256"` Contact []*core.AcmeURL `db:"contact"` Agreement string `db:"agreement"` + // InitialIP is stored as sixteen binary bytes, regardless of whether it + // represents a v4 or v6 IP address. + InitialIP []byte `db:"initialIp"` + CreatedAt time.Time `db:"createdAt"` LockCol int64 } @@ -65,12 +70,17 @@ func registrationToModel(r *core.Registration) (*regModel, error) { if err != nil { return nil, err } + if r.InitialIP == nil { + return nil, fmt.Errorf("initialIP was nil") + } rm := ®Model{ ID: r.ID, Key: key, KeySHA256: sha, Contact: r.Contact, Agreement: r.Agreement, + InitialIP: []byte(r.InitialIP.To16()), + CreatedAt: r.CreatedAt, } return rm, nil } @@ -87,6 +97,8 @@ func modelToRegistration(rm *regModel) (core.Registration, error) { Key: *k, Contact: rm.Contact, Agreement: rm.Agreement, + InitialIP: net.IP(rm.InitialIP), + CreatedAt: rm.CreatedAt, } return r, nil } diff --git a/sa/satest/satest.go b/sa/satest/satest.go index 78c16e559..268185b78 100644 --- a/sa/satest/satest.go +++ b/sa/satest/satest.go @@ -2,7 +2,9 @@ package satest import ( "encoding/json" + "net" "testing" + "time" jose "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" "github.com/letsencrypt/boulder/core" @@ -42,8 +44,10 @@ func CreateWorkingRegistration(t *testing.T, sa core.StorageAuthority) core.Regi } contacts := []*core.AcmeURL{contact} reg, err := sa.NewRegistration(core.Registration{ - Key: GoodJWK(), - Contact: contacts, + Key: GoodJWK(), + Contact: contacts, + InitialIP: net.ParseIP("88.77.66.11"), + CreatedAt: time.Date(2003, 5, 10, 0, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("Unable to create new registration") diff --git a/sa/storage-authority.go b/sa/storage-authority.go index f962944b9..f25cf3c61 100644 --- a/sa/storage-authority.go +++ b/sa/storage-authority.go @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "sort" "strings" "time" @@ -229,6 +230,73 @@ func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationID int64 return ssa.GetAuthorization(auth.ID) } +// incrementIP increments the byte within `ip` at `index`, accounting for overflow. +// Index must be greater than zero and less than len(ip). +func incrementIP(ip *net.IP, index int) { + if (*ip)[index] == 255 { + (*ip)[index-1]++ + (*ip)[index] = 0 + } else { + (*ip)[index]++ + } +} + +// ipRange returns a range of IP addresses suitable for querying MySQL for the +// purpose of rate limiting using a range that is inclusive on the lower end and +// exclusive at the higher end. If ip is an IPv4 address, it returns that address, +// plus the one immediately higher than it. If ip is an IPv6 address, it applies +// a /48 mask to it and returns the lowest IP in the resulting network, and the +// first IP outside of the resulting network. +func ipRange(ip net.IP) (net.IP, net.IP) { + ip = ip.To16() + begin := make(net.IP, len(ip)) + end := make(net.IP, len(ip)) + // For IPv4 addresses, do a match on exact address, so begin = ip and end = + // next higher IP. + if ip.To4() != nil { + copy(begin, ip) + copy(end, ip) + incrementIP(&end, 15) + } else { + // For IPv6, match on a certain subnet range, since one person can commonly + // have an entire /48 to themselves. + maskLength := 48 + mask := net.CIDRMask(maskLength, 128) + begin = ip.Mask(mask) + copy(end, begin) + incrementIP(&end, (maskLength/8)-1) + } + return begin, end +} + +// CountRegistrationsByIP returns the number of registrations created in the +// time range in an IP range. For IPv4 addresses, that range is limited to the +// single IP. For IPv6 addresses, that range is a /48, since it's not uncommon +// for one person to have a /48 to themselves. +func (ssa *SQLStorageAuthority) CountRegistrationsByIP(ip net.IP, earliest time.Time, latest time.Time) (int, error) { + var count int64 + beginIP, endIP := ipRange(ip) + err := ssa.dbMap.SelectOne( + &count, + `SELECT COUNT(1) FROM registrations + WHERE + :beginIP <= initialIP AND + initialIP < :endIP AND + :earliest < createdAt AND + createdAt <= :latest`, + map[string]interface{}{ + "ip": ip.String(), + "earliest": earliest, + "latest": latest, + "beginIP": []byte(beginIP), + "endIP": []byte(endIP), + }) + if err != nil { + return -1, err + } + return int(count), nil +} + // TooManyCertificatesError indicates that the number of certificates returned by // CountCertificates exceeded the hard-coded limit of 10,000 certificates. type TooManyCertificatesError string @@ -344,6 +412,7 @@ func (ssa *SQLStorageAuthority) NewRegistration(reg core.Registration) (core.Reg if err != nil { return reg, err } + rm.CreatedAt = ssa.clk.Now() err = ssa.dbMap.Insert(rm) if err != nil { return reg, err @@ -446,17 +515,32 @@ func (ssa *SQLStorageAuthority) MarkCertificateRevoked(serial string, ocspRespon // UpdateRegistration stores an updated Registration func (ssa *SQLStorageAuthority) UpdateRegistration(reg core.Registration) error { - rm, err := registrationToModel(®) + lookupResult, err := ssa.dbMap.Get(regModel{}, reg.ID) if err != nil { return err } + if lookupResult == nil { + msg := fmt.Sprintf("No registrations with ID %d", reg.ID) + return core.NoSuchRegistrationError(msg) + } + existingRegModel, ok := lookupResult.(*regModel) + if !ok { + // Shouldn't happen + return fmt.Errorf("Incorrect type returned from registration lookup") + } - n, err := ssa.dbMap.Update(rm) + updatedRegModel, err := registrationToModel(®) + if err != nil { + return err + } + updatedRegModel.LockCol = existingRegModel.LockCol + + n, err := ssa.dbMap.Update(updatedRegModel) if err != nil { return err } if n == 0 { - msg := fmt.Sprintf("Requested registration not found %v", reg.ID) + msg := fmt.Sprintf("Requested registration not found %d", reg.ID) return core.NoSuchRegistrationError(msg) } diff --git a/sa/storage-authority_test.go b/sa/storage-authority_test.go index 74ad5bc8e..db912bb56 100644 --- a/sa/storage-authority_test.go +++ b/sa/storage-authority_test.go @@ -15,6 +15,9 @@ import ( "encoding/json" "fmt" "io/ioutil" + "math/big" + "net" + "net/url" "testing" "time" @@ -41,7 +44,7 @@ func initSA(t *testing.T) (*SQLStorageAuthority, clock.FakeClock, func()) { dbMap.TraceOn("SQL: ", &SQLLogger{log}) fc := clock.NewFake() - fc.Add(1 * time.Hour) + fc.Set(time.Date(2015, 3, 4, 5, 0, 0, 0, time.UTC)) sa, err := NewSQLStorageAuthority(dbMap, fc) if err != nil { @@ -60,7 +63,7 @@ var ( ) func TestAddRegistration(t *testing.T) { - sa, _, cleanUp := initSA(t) + sa, clk, cleanUp := initSA(t) defer cleanUp() jwk := satest.GoodJWK() @@ -71,8 +74,9 @@ func TestAddRegistration(t *testing.T) { } contacts := []*core.AcmeURL{contact} reg, err := sa.NewRegistration(core.Registration{ - Key: jwk, - Contact: contacts, + Key: jwk, + Contact: contacts, + InitialIP: net.ParseIP("43.34.43.34"), }) if err != nil { t.Fatalf("Couldn't create new registration: %s", err) @@ -87,15 +91,23 @@ func TestAddRegistration(t *testing.T) { test.AssertNotError(t, err, fmt.Sprintf("Couldn't get registration with ID %v", reg.ID)) expectedReg := core.Registration{ - ID: reg.ID, - Key: jwk, + ID: reg.ID, + Key: jwk, + InitialIP: net.ParseIP("43.34.43.34"), + CreatedAt: clk.Now(), } test.AssertEquals(t, dbReg.ID, expectedReg.ID) test.Assert(t, core.KeyDigestEquals(dbReg.Key, expectedReg.Key), "Stored key != expected") u, _ := core.ParseAcmeURL("test.com") - newReg := core.Registration{ID: reg.ID, Key: jwk, Contact: []*core.AcmeURL{u}, Agreement: "yes"} + newReg := core.Registration{ + ID: reg.ID, + Key: jwk, + Contact: []*core.AcmeURL{u}, + InitialIP: net.ParseIP("72.72.72.72"), + Agreement: "yes", + } err = sa.UpdateRegistration(newReg) test.AssertNotError(t, err, fmt.Sprintf("Couldn't get registration with ID %v", reg.ID)) dbReg, err = sa.GetRegistrationByKey(jwk) @@ -582,3 +594,48 @@ func TestCountCertificates(t *testing.T) { test.AssertNotError(t, err, "Couldn't get certificate count for the last 24hrs") test.AssertEquals(t, count, int64(0)) } + +func TestCountRegistrationsByIP(t *testing.T) { + sa, fc, cleanUp := initSA(t) + defer cleanUp() + + contact := core.AcmeURL(url.URL{ + Scheme: "mailto", + Opaque: "foo@example.com", + }) + + _, err := sa.NewRegistration(core.Registration{ + Key: jose.JsonWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}, + Contact: []*core.AcmeURL{&contact}, + InitialIP: net.ParseIP("43.34.43.34"), + }) + test.AssertNotError(t, err, "Couldn't insert registration") + _, err = sa.NewRegistration(core.Registration{ + Key: jose.JsonWebKey{Key: &rsa.PublicKey{N: big.NewInt(2), E: 1}}, + Contact: []*core.AcmeURL{&contact}, + InitialIP: net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9652"), + }) + test.AssertNotError(t, err, "Couldn't insert registration") + _, err = sa.NewRegistration(core.Registration{ + Key: jose.JsonWebKey{Key: &rsa.PublicKey{N: big.NewInt(3), E: 1}}, + Contact: []*core.AcmeURL{&contact}, + InitialIP: net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9653"), + }) + test.AssertNotError(t, err, "Couldn't insert registration") + + earliest := fc.Now().Add(-time.Hour * 24) + latest := fc.Now() + + count, err := sa.CountRegistrationsByIP(net.ParseIP("1.1.1.1"), earliest, latest) + test.AssertNotError(t, err, "Failed to count registrations") + test.AssertEquals(t, count, 0) + count, err = sa.CountRegistrationsByIP(net.ParseIP("43.34.43.34"), earliest, latest) + test.AssertNotError(t, err, "Failed to count registrations") + test.AssertEquals(t, count, 1) + count, err = sa.CountRegistrationsByIP(net.ParseIP("2001:cdba:1234:5678:9101:1121:3257:9652"), earliest, latest) + test.AssertNotError(t, err, "Failed to count registrations") + test.AssertEquals(t, count, 2) + count, err = sa.CountRegistrationsByIP(net.ParseIP("2001:cdba:1234:0000:0000:0000:0000:0000"), earliest, latest) + test.AssertNotError(t, err, "Failed to count registrations") + test.AssertEquals(t, count, 2) +} diff --git a/test/rate-limit-policies.yml b/test/rate-limit-policies.yml index a13e6a833..846012e26 100644 --- a/test/rate-limit-policies.yml +++ b/test/rate-limit-policies.yml @@ -15,3 +15,8 @@ certificatesPerName: nginx.wtf: 10000 registrationOverrides: 101: 1000 +registrationsPerIP: + window: 168h # 1 week + threshold: 3 + overrides: + 127.0.0.1: 1000000 diff --git a/wfe/web-front-end.go b/wfe/web-front-end.go index a9a1da249..041dec101 100644 --- a/wfe/web-front-end.go +++ b/wfe/web-front-end.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net" "net/http" "regexp" "strconv" @@ -547,6 +548,17 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques return } init.Key = *key + init.InitialIP = net.ParseIP(request.Header.Get("X-Real-IP")) + if init.InitialIP == nil { + host, _, err := net.SplitHostPort(request.RemoteAddr) + if err == nil { + init.InitialIP = net.ParseIP(host) + } else { + logEvent.Error = "Couldn't parse RemoteAddr" + wfe.sendError(response, logEvent.Error, nil, http.StatusInternalServerError) + return + } + } reg, err := wfe.RA.NewRegistration(init) if err != nil { diff --git a/wfe/web-front-end_test.go b/wfe/web-front-end_test.go index 80cb5e6c2..a89d9d4fa 100644 --- a/wfe/web-front-end_test.go +++ b/wfe/web-front-end_test.go @@ -225,7 +225,7 @@ func setupWFE(t *testing.T) WebFrontEndImpl { func makePostRequest(body string) *http.Request { return &http.Request{ Method: "POST", - RemoteAddr: "1.1.1.1", + RemoteAddr: "1.1.1.1:7882", Header: map[string][]string{ "Content-Length": []string{fmt.Sprintf("%d", len(body))}, }, @@ -807,12 +807,13 @@ func TestNewRegistration(t *testing.T) { wfe.NewRegistration(responseWriter, makePostRequest(result.FullSerialize())) - test.AssertEquals(t, responseWriter.Body.String(), `{"id":0,"key":{"kty":"RSA","n":"qnARLrT7Xz4gRcKyLdydmCr-ey9OuPImX4X40thk3on26FkMznR3fRjs66eLK7mmPcBZ6uOJseURU6wAaZNmemoYx1dMvqvWWIyiQleHSD7Q8vBrhR6uIoO4jAzJZR-ChzZuSDt7iHN-3xUVspu5XGwXU_MVJZshTwp4TaFx5elHIT_ObnTvTOU3Xhish07AbgZKmWsVbXh5s-CrIicU4OexJPgunWZ_YJJueOKmTvnLlTV4MzKR2oZlBKZ27S0-SfdV_QDx_ydle5oMAyKVtlAV35cyPMIsYNwgUGBCdY_2Uzi5eX0lTc7MPRwz6qR1kip-i59VcGcUQgqHV6Fyqw","e":"AQAB"},"contact":["tel:123456789"],"agreement":"http://example.invalid/terms"}`) var reg core.Registration err = json.Unmarshal([]byte(responseWriter.Body.String()), ®) test.AssertNotError(t, err, "Couldn't unmarshal returned registration object") test.Assert(t, len(reg.Contact) >= 1, "No contact field in registration") test.AssertEquals(t, reg.Contact[0].String(), "tel:123456789") + test.AssertEquals(t, reg.Agreement, "http://example.invalid/terms") + test.AssertEquals(t, reg.InitialIP.String(), "1.1.1.1") test.AssertEquals( t, responseWriter.Header().Get("Location"), @@ -1315,11 +1316,7 @@ func TestLogCsrPem(t *testing.T) { wfe.logCsr(req, certificateRequest, reg) - matches := mockLog.GetAllMatching("Certificate request") - test.Assert(t, len(matches) == 1, - "Incorrect number of certificate request log entries") - test.AssertEquals(t, matches[0].Priority, syslog.LOG_NOTICE) - test.AssertEquals(t, matches[0].Message, `[AUDIT] Certificate request JSON={"ClientAddr":"10.0.0.1,172.16.0.1,12.34.98.76","CsrBase64":"MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAycX3ca+fViOuRWF38mssORISFxbJvspDfhPGRBZDxJ63NIqQzupB+6dp48xkcX7Z/KDaRJStcpJT2S0u33moNT4FHLklQBETLhExDk66cmlz6Xibp3LGZAwhWuec7wJoEwIgY8oq4rxihIyGq7HVIJoq9DqZGrUgfZMDeEJqbphukQOaXGEop7mD+eeu8+z5EVkB1LiJ6Yej6R8MAhVPHzG5fyOu6YVo6vY6QgwjRLfZHNj5XthxgPIEETZlUbiSoI6J19GYHvLURBTy5Ys54lYAPIGfNwcIBAH4gtH9FrYcDY68R22rp4iuxdvkf03ZWiT0F2W1y7/C9B2jayTzvQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAHd6Do9DIZ2hvdt1GwBXYjsqprZidT/DYOMfYcK17KlvdkFT58XrBH88ulLZ72NXEpiFMeTyzfs3XEyGq/Bbe7TBGVYZabUEh+LOskYwhgcOuThVN7tHnH5rhN+gb7cEdysjTb1QL+vOUwYgV75CB6PE5JVYK+cQsMIVvo0Kz4TpNgjJnWzbcH7h0mtvub+fCv92vBPjvYq8gUDLNrok6rbg05tdOJkXsF2G/W+Q6sf2Fvx0bK5JeH4an7P7cXF9VG9nd4sRt5zd+L3IcyvHVKxNhIJXZVH0AOqh/1YrKI9R0QKQiZCEy0xN1okPlcaIVaFhb7IKAHPxTI3r5f72LXY=","Registration":{"id":789,"key":{"kty":"RSA","n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ","e":"AQAB"},"agreement":"http://example.invalid/terms"}}`) + assertCsrLogged(t, mockLog) } func TestLengthRequired(t *testing.T) { From ed7c31f0ca658adf3f210cb0cbf7d16a95f3c05c Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 8 Oct 2015 16:26:00 -0700 Subject: [PATCH 3/9] Spurious diff to trigger Travis. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 99d11742b..4d9062de1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,3 +56,4 @@ env: script: - bash test.sh + From c0f5fd1bb6042af983d98d2f59c304f3c525b03c Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Thu, 8 Oct 2015 16:21:12 -0700 Subject: [PATCH 4/9] Fix AddSCTReceipt RPC transit --- core/objects.go | 7 +++++++ rpc/rpc-wrappers.go | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/objects.go b/core/objects.go index 210fd9eea..2a64230b4 100644 --- a/core/objects.go +++ b/core/objects.go @@ -741,6 +741,13 @@ type SignedCertificateTimestamp struct { LockCol int64 } +// RPCSignedCertificateTimestamp is a wrapper around SignedCertificateTimestamp +// so that it can be passed through the RPC layer properly. Without this wrapper +// the UnmarshalJSON method below will be used when marshaling/unmarshaling the +// object, which is not what we want as it is not symmetrical (as it is intended +// to unmarshal a rawSignedCertificateTimestamp into a SignedCertificateTimestamp) +type RPCSignedCertificateTimestamp SignedCertificateTimestamp + type rawSignedCertificateTimestamp struct { Version uint8 `json:"sct_version"` LogID string `json:"id"` diff --git a/rpc/rpc-wrappers.go b/rpc/rpc-wrappers.go index 47aeddd52..7a42f5a50 100644 --- a/rpc/rpc-wrappers.go +++ b/rpc/rpc-wrappers.go @@ -1056,7 +1056,7 @@ func NewStorageAuthorityServer(rpc Server, impl core.StorageAuthority) error { } sct, err := impl.GetSCTReceipt(gsctReq.Serial, gsctReq.LogID) - jsonResponse, err := json.Marshal(core.SignedCertificateTimestamp(sct)) + jsonResponse, err := json.Marshal(core.RPCSignedCertificateTimestamp(sct)) if err != nil { // AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3 errorCondition(MethodGetSCTReceipt, err, req) @@ -1067,7 +1067,7 @@ func NewStorageAuthorityServer(rpc Server, impl core.StorageAuthority) error { }) rpc.Handle(MethodAddSCTReceipt, func(req []byte) (response []byte, err error) { - var sct core.SignedCertificateTimestamp + var sct core.RPCSignedCertificateTimestamp err = json.Unmarshal(req, &sct) if err != nil { // AUDIT[ Improper Messages ] 0786b6f2-91ca-4f48-9883-842a19084c64 From 0df44e5d903b7b8cb375cc2dfd849d72df3c183c Mon Sep 17 00:00:00 2001 From: Jeff Hodges Date: Tue, 6 Oct 2015 00:19:10 -0700 Subject: [PATCH 5/9] clean up CSRs with capitalized letters This change lowercases domains before they are stored in the database and makes policy.WillingToIssue reject any domains with uppercase letters. Fixes #927. --- ca/certificate-authority.go | 12 ++-- ca/certificate-authority_test.go | 33 +++++++++- ca/testdata/capitalized_cn_and_san.der.csr | Bin 0 -> 716 bytes ca/testdata/testcsr.go | 69 +++++++++++++++++++++ core/util.go | 7 ++- core/util_test.go | 7 +++ policy/policy-authority.go | 8 +-- policy/policy-authority_test.go | 3 + ra/registration-authority.go | 6 +- ra/registration-authority_test.go | 19 ++++++ 10 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 ca/testdata/capitalized_cn_and_san.der.csr create mode 100644 ca/testdata/testcsr.go diff --git a/ca/certificate-authority.go b/ca/certificate-authority.go index 34f5a23b2..c7706bdc1 100644 --- a/ca/certificate-authority.go +++ b/ca/certificate-authority.go @@ -16,6 +16,7 @@ import ( "fmt" "io/ioutil" "math/big" + "strings" "time" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock" @@ -223,7 +224,8 @@ func (ca *CertificateAuthorityImpl) RevokeCertificate(serial string, reasonCode } // IssueCertificate attempts to convert a CSR into a signed Certificate, while -// enforcing all policies. +// enforcing all policies. Names (domains) in the CertificateRequest will be +// lowercased before storage. func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest, regID int64) (core.Certificate, error) { emptyCert := core.Certificate{} var err error @@ -253,10 +255,10 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest hostNames := make([]string, len(csr.DNSNames)) copy(hostNames, csr.DNSNames) if len(csr.Subject.CommonName) > 0 { - commonName = csr.Subject.CommonName - hostNames = append(hostNames, csr.Subject.CommonName) + commonName = strings.ToLower(csr.Subject.CommonName) + hostNames = append(hostNames, commonName) } else if len(hostNames) > 0 { - commonName = hostNames[0] + commonName = strings.ToLower(hostNames[0]) } else { err = core.MalformedRequestError("Cannot issue a certificate without a hostname.") // AUDIT[ Certificate Requests ] 11917fa4-10ef-4e0d-9105-bacbe7836a3c @@ -265,7 +267,7 @@ func (ca *CertificateAuthorityImpl) IssueCertificate(csr x509.CertificateRequest } // Collapse any duplicate names. Note that this operation may re-order the names - hostNames = core.UniqueNames(hostNames) + hostNames = core.UniqueLowerNames(hostNames) if ca.MaxNames > 0 && len(hostNames) > ca.MaxNames { err = core.MalformedRequestError(fmt.Sprintf("Certificate request has %d > %d names", len(hostNames), ca.MaxNames)) ca.log.WarningErr(err) diff --git a/ca/certificate-authority_test.go b/ca/certificate-authority_test.go index 0d9873b69..c2de71d52 100644 --- a/ca/certificate-authority_test.go +++ b/ca/certificate-authority_test.go @@ -11,6 +11,7 @@ import ( "encoding/asn1" "fmt" "io/ioutil" + "sort" "testing" "time" @@ -64,7 +65,7 @@ var ( DupeNameCSR = mustRead("./testdata/dupe_name.der.csr") // CSR generated by Go: - // * Random pulic key + // * Random public key // * CN = [none] // * DNSNames = not-example.com, www.not-example.com, mail.example.com TooManyNameCSR = mustRead("./testdata/too_many_names.der.csr") @@ -81,7 +82,14 @@ var ( // * DNSNames = not-example.com, www.not-example.com, mail.not-example.com // * Signature algorithm: SHA1WithRSA BadAlgorithmCSR = mustRead("./testdata/bad_algorithm.der.csr") - log = mocks.UseMockLog() + + // CSR generated by Go: + // * Random public key + // * CN = CapiTalizedLetters.com + // * DNSNames = moreCAPs.com, morecaps.com, evenMOREcaps.com, Capitalizedletters.COM + CapitalizedCSR = mustRead("./testdata/capitalized_cn_and_san.der.csr") + + log = mocks.UseMockLog() ) // CFSSL config @@ -421,3 +429,24 @@ func TestRejectBadAlgorithm(t *testing.T) { _, ok := err.(core.MalformedRequestError) test.Assert(t, ok, "Incorrect error type returned") } + +func TestCapitalizedLetters(t *testing.T) { + ctx := setup(t) + defer ctx.cleanUp() + ctx.caConfig.MaxNames = 3 + ca, err := NewCertificateAuthorityImpl(ctx.caConfig, ctx.fc, caCertFile) + ca.Publisher = &mocks.Publisher{} + ca.PA = ctx.pa + ca.SA = ctx.sa + + csr, _ := x509.ParseCertificateRequest(CapitalizedCSR) + cert, err := ca.IssueCertificate(*csr, ctx.reg.ID) + test.AssertNotError(t, err, "Failed to gracefully handle a CSR with capitalized names") + + parsedCert, err := x509.ParseCertificate(cert.DER) + test.AssertNotError(t, err, "Error parsing certificate produced by CA") + test.AssertEquals(t, "capitalizedletters.com", parsedCert.Subject.CommonName) + sort.Strings(parsedCert.DNSNames) + expected := []string{"capitalizedletters.com", "evenmorecaps.com", "morecaps.com"} + test.AssertDeepEquals(t, expected, parsedCert.DNSNames) +} diff --git a/ca/testdata/capitalized_cn_and_san.der.csr b/ca/testdata/capitalized_cn_and_san.der.csr new file mode 100644 index 0000000000000000000000000000000000000000..a638dc09a222e4b0c192dc52d6fdf153e16b4eba GIT binary patch literal 716 zcmXqLVme{a#JGWpk-8bFQA7%v@{A>MR-aq4G-VU!vRwtRw z4+xph?Em!fsr3K15BYY9Y~#H!<(9V1tuIw4<{jz0DiLn~Jb>ep&%cuj-qNSOzo}tp zVBIzGL}spBP@pv4s`WYf9()@DAr~NMkGOt=F z?@oGJDX??r>SV`*ev35SCjKaOE6co-RK0pWSJuKub{S9aM?FcNed*5T-zUFX`U=Dx zQEB@W&#m<&*MRFuuAy#!@e@9?M@#l!ny~BU?c+NSUsRahVQS38%*epFAju#B9!8ve zhQS7b2L8Zsl@(<1FmP+)$;~fHb#@E@hbV-XoLB(i38a>#=K1;uxxz)jF;N1Ei5zH5 zIQ#n|#{xGv78n`!EEk;Le=pjyMDp_Vi#}hK*cs!Z^!j-pE|~wXv@+|*esdG8wI)te zdG1tve6sP4|Na9Jm9`C0PmP@G7CgB3x!K}oHn-wIb{(B{pZTKa?^(95(l_*oBA4o` z?Vk+g+2SRpHCJc;lbcj4YdB%2ub127&AIol2Q&*$6{}g}n4#I|^>62=e_5B;&o@{a zBV3o!nDy^reBP7oo}4SsioX*xnKyO$ft35YlD8&V|N6XG{;a9wxexrt!R*(gExvtp zRoZRYaIE~l-F7``UvEo(aeen@=kh&Q)!wNTx!m3}d+w$cDl8%@i|U^nwQrrWQPtd` ZaN~rmiS3D#x&6}ezwA`iJ$gvZ5CHPdHU$6x literal 0 HcmV?d00001 diff --git a/ca/testdata/testcsr.go b/ca/testdata/testcsr.go new file mode 100644 index 000000000..367c8d485 --- /dev/null +++ b/ca/testdata/testcsr.go @@ -0,0 +1,69 @@ +// Hack up the x509.CertificateRequest in here, run `go run testcsr.go`, and a +// DER-encoded CertificateRequest will be printed to stdout. +package main + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "log" + "os" +) + +// A 2048-bit RSA private key +var pemPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5cpXqfCaUDD+hf93j5jxbrhK4jrJAzfAEjeZj/Lx5Rv/7eEO +uhS2DdCU2is82vR6yJ7EidUYVz/nUAjSTP7JIEsbyvfsfACABbqRyGltHlJnULVH +y/EMjt9xKZf17T8tOLHVUEAJTxsvjKn4TMIQJTNrAqm/lNrUXmCIR41Go+3RBGC6 +YdAKEwcZMCzrjQGF06mC6/6xMmYMSMd6+VQRFIPpuPK/6BBp1Tgju2LleRC5uatj +QcFOoilGkfh1RnZp3GJ7q58KaqHiPmjl31rkY5vS3LP7yfU5TRBcxCSG8l8LKuRt +MArkbTEtj3PkDjbipL/SkLrZ28e5w9Egl4g1MwIDAQABAoIBABZqY5zPPK5f6SQ3 +JHmciMitL5jb9SncMV9VjyRMpa4cyh1xW9dpF81HMI4Ls7cELEoPuspbQDGaqTzU +b3dVT1dYHFDzWF1MSzDD3162cg+IKE3mMSfCzt/NCiPtj+7hv86NAmr+pCnUVBIb +rn4GXD7UwjaTSn4Bzr+aGREpxd9Nr0JdNQwxVHZ75A92vTihCfaXyMCjhW3JEpF9 +N89XehgidoGgtUxxeeb+WsO3nvVBpLv/HDxMTx/IDzvSA5nLlYMcqVzb7IJoeAQu +og0WJKlniYzvIdoQ6/hGydAW5sKd0qWh0JPYs7uLKAWrdAWvrFAp7//fYKVamalU +8pUu/WkCgYEA+tcTQ3qTnVh41O9YeM/7NULpIkuCAlR+PBRky294zho9nGQIPdaW +VNvyqqjLaHaXJVokYHbU4hDk6RbrhoWVd4Po/5g9cUkT1f6nrdZGRkg4XOCzHWvV +Yrqh3eYYX4bdiH5EhB78m0rrbjHfd7SF3cdYNzOUS2kJvCInYC6zPx8CgYEA6oRr +UhZFuoqRsEb28ELM8sHvdIMA/C3aWCu+nUGQ4gHSEb4uvuOD/7tQNuCaBioiXVPM +/4hjk9jHJcjYf5l33ANqIP7JiYAt4rzTWXF3iS6kQOhQhjksSlSnWqw0Uu1DtlpG +rzeG1ZkBuwH7Bx0yj4sGSz5sAvyF44aRsE6AC20CgYEArafWO0ISDb1hMbFdo44B +ELd45Pg3UluiZP+NZFWQ4cbC3pFWL1FvE+KNll5zK6fmLcLBKlM6QCOIBmKKvb+f +YXVeCg0ghFweMmkxNqUAU8nN02bwOa8ctFQWmaOhPgkFN2iLEJjPMsdkRA6c8ad1 +gbtvNBAuWyKlzawrbGgISesCgYBkGEjGLINubx5noqJbQee/5U6S6CdPezKqV2Fw +NT/ldul2cTn6d5krWYOPKKYU437vXokst8XooKm/Us41CAfEfCCcHKNgcLklAXsj +ve5LOwEYQw+7ekORJjiX1tAuZN51wmpQ9t4x5LB8ZQgDrU6bPbdd/jKTw7xRtGoS +Wi8EsQKBgG8iGy3+kVBIjKHxrN5jVs3vj/l/fQL0WRMLCMmVuDBfsKyy3f9n8R1B +/KdwoyQFwsLOyr5vAjiDgpFurXQbVyH4GDFiJGS1gb6MNcinwSTpsbOLLV7zgibX +A2NgiQ+UeWMia16dZVd6gGDlY3lQpeyLdsdDd+YppNfy9vedjbvT +-----END RSA PRIVATE KEY-----` + +func main() { + block, _ := pem.Decode([]byte(pemPrivateKey)) + rsaPriv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + log.Fatalf("Failed to parse private key: %s", err) + } + + req := &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: "CapiTalizedLetters.com", + }, + DNSNames: []string{ + "moreCAPs.com", + "morecaps.com", + "evenMOREcaps.com", + "Capitalizedletters.COM", + }, + } + csr, err := x509.CreateCertificateRequest(rand.Reader, req, rsaPriv) + if err != nil { + log.Fatalf("unable to create CSR: %s", err) + } + _, err = os.Stdout.Write(csr) + if err != nil { + log.Fatalf("unable to write to stdout: %s", err) + } +} diff --git a/core/util.go b/core/util.go index 224d035cf..cc3027283 100644 --- a/core/util.go +++ b/core/util.go @@ -472,11 +472,12 @@ func GetBuildHost() (retID string) { return } -// UniqueNames returns the set of all unique names in the input. -func UniqueNames(names []string) (unique []string) { +// UniqueLowerNames returns the set of all unique names in the input after all +// of them are lowercased. The returned names will be in their lowercased form. +func UniqueLowerNames(names []string) (unique []string) { nameMap := make(map[string]int, len(names)) for _, name := range names { - nameMap[name] = 1 + nameMap[strings.ToLower(name)] = 1 } unique = make([]string, 0, len(nameMap)) diff --git a/core/util_test.go b/core/util_test.go index b3c5b71bb..6d26fa081 100644 --- a/core/util_test.go +++ b/core/util_test.go @@ -11,6 +11,7 @@ import ( "math" "math/big" "net/url" + "sort" "testing" "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose" @@ -113,3 +114,9 @@ func TestAcmeURL(t *testing.T) { a := (*AcmeURL)(u) test.AssertEquals(t, s, a.String()) } + +func TestUniqueLowerNames(t *testing.T) { + u := UniqueLowerNames([]string{"foobar.com", "fooBAR.com", "baz.com", "foobar.com", "bar.com", "bar.com"}) + sort.Strings(u) + test.AssertDeepEquals(t, []string{"bar.com", "baz.com", "foobar.com"}, u) +} diff --git a/policy/policy-authority.go b/policy/policy-authority.go index a5a08c07d..78c29429c 100644 --- a/policy/policy-authority.go +++ b/policy/policy-authority.go @@ -64,7 +64,7 @@ const ( whitelistedPartnerRegID = -1 ) -var dnsLabelRegexp = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$") +var dnsLabelRegexp = regexp.MustCompile("^[a-z0-9][a-z0-9-]{0,62}$") var punycodeRegexp = regexp.MustCompile("^xn--") func isDNSCharacter(ch byte) bool { @@ -112,7 +112,8 @@ func (e SyntaxError) Error() string { return "Syntax error" } func (e NonPublicError) Error() string { return "Name does not end in a public suffix" } // WillingToIssue determines whether the CA is willing to issue for the provided -// identifier. +// identifier. It expects domains in id to be lowercase to prevent mismatched +// cases breaking queries. // // We place several criteria on identifiers we are willing to issue for: // @@ -132,8 +133,6 @@ func (e NonPublicError) Error() string { return "Name does not end in a // XXX: Is there any need for this method to be constant-time? We're // going to refuse to issue anyway, but timing could leak whether // names are on the blacklist. -// -// XXX: We should probably fold everything to lower-case somehow. func (pa PolicyAuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64) error { if id.Type != core.IdentifierDNS { return InvalidIdentifierError{} @@ -146,7 +145,6 @@ func (pa PolicyAuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64 } } - domain = strings.ToLower(domain) if len(domain) > 255 { return SyntaxError{} } diff --git a/policy/policy-authority_test.go b/policy/policy-authority_test.go index c3cd284f7..b74d6796a 100644 --- a/policy/policy-authority_test.go +++ b/policy/policy-authority_test.go @@ -86,6 +86,9 @@ func TestWillingToIssue(t *testing.T) { `zombocom`, `localhost`, `mail`, + + // disallow capitalized letters for #927 + `CapitalizedLetters.com`, } shouldBeNonPublic := []string{ diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 5c8f161c9..455b8b2bc 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -186,7 +186,8 @@ func validateContacts(contacts []*core.AcmeURL, resolver core.DNSResolver, stats return } -// NewAuthorization constuct a new Authz from a request. +// NewAuthorization constuct a new Authz from a request. Values (domains) in +// request.Identifier will be lowercased before storage. func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization, regID int64) (authz core.Authorization, err error) { reg, err := ra.SA.GetRegistration(regID) if err != nil { @@ -195,6 +196,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(request core.Authorization } identifier := request.Identifier + identifier.Value = strings.ToLower(identifier.Value) // Check that the identifier is present and appropriate if err = ra.PA.WillingToIssue(identifier, regID); err != nil { @@ -275,7 +277,7 @@ func (ra *RegistrationAuthorityImpl) MatchesCSR( if len(csr.Subject.CommonName) > 0 { hostNames = append(hostNames, csr.Subject.CommonName) } - hostNames = core.UniqueNames(hostNames) + hostNames = core.UniqueLowerNames(hostNames) if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) { err = core.InternalServerError("Generated certificate public key doesn't match CSR public key") diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index 77959a3a9..7ca53f869 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -403,6 +403,25 @@ func TestNewAuthorization(t *testing.T) { t.Log("DONE TestNewAuthorization") } +func TestNewAuthorizationCapitalLetters(t *testing.T) { + _, sa, ra, _, cleanUp := initAuthorities(t) + defer cleanUp() + + authzReq := core.Authorization{ + Identifier: core.AcmeIdentifier{ + Type: core.IdentifierDNS, + Value: "NOT-example.COM", + }, + } + authz, err := ra.NewAuthorization(authzReq, Registration.ID) + test.AssertNotError(t, err, "NewAuthorization failed") + test.AssertEquals(t, "not-example.com", authz.Identifier.Value) + + dbAuthz, err := sa.GetAuthorization(authz.ID) + test.AssertNotError(t, err, "Could not fetch authorization from database") + assertAuthzEqual(t, authz, dbAuthz) +} + func TestUpdateAuthorization(t *testing.T) { va, sa, ra, _, cleanUp := initAuthorities(t) defer cleanUp() From 516ba44f38bc58dfd7c6108174f444008eaf429c Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Thu, 8 Oct 2015 19:12:22 -0700 Subject: [PATCH 6/9] Actually run the missing SCT receipt loop --- cmd/ocsp-updater/main.go | 1 + test/boulder-config.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/ocsp-updater/main.go b/cmd/ocsp-updater/main.go index bde17f34d..b8be069a9 100644 --- a/cmd/ocsp-updater/main.go +++ b/cmd/ocsp-updater/main.go @@ -435,6 +435,7 @@ func main() { go updater.newCertificatesLoop.loop() go updater.oldOCSPResponsesLoop.loop() + go updater.missingSCTReceiptsLoop.loop() cmd.FailOnError(err, "Failed to create updater") diff --git a/test/boulder-config.json b/test/boulder-config.json index 00ab1e534..6684deeee 100644 --- a/test/boulder-config.json +++ b/test/boulder-config.json @@ -189,6 +189,7 @@ }, "publisher": { + "maxConcurrentRPCServerRequests": 16, "debugAddr": "localhost:8009" }, @@ -208,8 +209,7 @@ "submissionRetries": 1, "submissionBackoff": "1s", "intermediateBundleFilename": "test/test-ca.pem" - }, - "maxConcurrentRPCServerRequests": 16 + } }, "certChecker": { From c80206d4241f395742561bb8f7118f2afeebcca4 Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Thu, 8 Oct 2015 23:14:25 -0700 Subject: [PATCH 7/9] Respond to review feedback. --- ra/registration-authority.go | 6 ++++- sa/ip_range_test.go | 22 ++++++++-------- sa/storage-authority.go | 50 +++++++++++++++++++++--------------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/ra/registration-authority.go b/ra/registration-authority.go index 2e99a79dd..dd4990527 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -139,6 +139,10 @@ func (ra *RegistrationAuthorityImpl) setIssuanceCount() (int, error) { return ra.totalIssuedCache, nil } +// noRegistrationID is used for the regID parameter to GetThreshold when no +// registration-based overrides are necessary. +const noRegistrationID = -1 + func (ra *RegistrationAuthorityImpl) checkRegistrationLimit(ip net.IP) error { limit := ra.rlPolicies.RegistrationsPerIP if limit.Enabled() { @@ -147,7 +151,7 @@ func (ra *RegistrationAuthorityImpl) checkRegistrationLimit(ip net.IP) error { if err != nil { return err } - if count >= limit.GetThreshold(ip.String(), -1) { + if count >= limit.GetThreshold(ip.String(), noRegistrationID) { return core.RateLimitedError("Too many registrations from this IP") } } diff --git a/sa/ip_range_test.go b/sa/ip_range_test.go index acf45ad57..603780119 100644 --- a/sa/ip_range_test.go +++ b/sa/ip_range_test.go @@ -16,21 +16,23 @@ func TestIncrementIP(t *testing.T) { index int expected string }{ - {"0.0.0.0", 15, "0.0.0.1"}, - {"0.0.0.255", 15, "0.0.1.0"}, - {"127.0.0.1", 15, "127.0.0.2"}, - {"1.2.3.4", 14, "1.2.4.4"}, - {"::1", 15, "::2"}, - {"2002:1001:4008::", 15, "2002:1001:4008::1"}, - {"2002:1001:4008::", 5, "2002:1001:4009::"}, + {"0.0.0.0", 128, "0.0.0.1"}, + {"0.0.0.255", 128, "0.0.1.0"}, + {"127.0.0.1", 128, "127.0.0.2"}, + {"1.2.3.4", 120, "1.2.4.4"}, + {"::1", 128, "::2"}, + {"2002:1001:4008::", 128, "2002:1001:4008::1"}, + {"2002:1001:4008::", 48, "2002:1001:4009::"}, + {"2002:1001:ffff::", 48, "2002:1002::"}, + {"ffff:ffff:ffff::", 48, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}, } for _, tc := range testCases { ip := net.ParseIP(tc.ip).To16() - incrementIP(&ip, tc.index) + actual := incrementIP(ip, tc.index) expectedIP := net.ParseIP(tc.expected) - if !ip.Equal(expectedIP) { + if !actual.Equal(expectedIP) { t.Errorf("Expected incrementIP(%s, %d) to be %s, instead got %s", - tc.ip, tc.index, expectedIP, ip.String()) + tc.ip, tc.index, expectedIP, actual.String()) } } } diff --git a/sa/storage-authority.go b/sa/storage-authority.go index f25cf3c61..a3e5f6bd7 100644 --- a/sa/storage-authority.go +++ b/sa/storage-authority.go @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "net" "sort" "strings" @@ -230,15 +231,26 @@ func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationID int64 return ssa.GetAuthorization(auth.ID) } -// incrementIP increments the byte within `ip` at `index`, accounting for overflow. -// Index must be greater than zero and less than len(ip). -func incrementIP(ip *net.IP, index int) { - if (*ip)[index] == 255 { - (*ip)[index-1]++ - (*ip)[index] = 0 - } else { - (*ip)[index]++ +// incrementIP returns a copy of `ip` incremented at a bit index `index`, +// or in other words the first IP of the next highest subnet given a mask of +// length `index`. +// In order to easily account for overflow, we treat ip as a big.Int and add to +// it. If the increment overflows the max size of a net.IP, return the highest +// possible net.IP. +func incrementIP(ip net.IP, index int) net.IP { + bigInt := new(big.Int) + bigInt.SetBytes([]byte(ip)) + incr := new(big.Int).Lsh(big.NewInt(1), 128-uint(index)) + bigInt.Add(bigInt, incr) + // bigInt.Bytes can be shorter than 16 bytes, so stick it into a + // full-sized net.IP. + resultBytes := bigInt.Bytes() + if len(resultBytes) > 16 { + return net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") } + result := make(net.IP, 16) + copy(result[16-len(resultBytes):], resultBytes) + return result } // ipRange returns a range of IP addresses suitable for querying MySQL for the @@ -249,23 +261,19 @@ func incrementIP(ip *net.IP, index int) { // first IP outside of the resulting network. func ipRange(ip net.IP) (net.IP, net.IP) { ip = ip.To16() - begin := make(net.IP, len(ip)) - end := make(net.IP, len(ip)) + // For IPv6, match on a certain subnet range, since one person can commonly + // have an entire /48 to themselves. + maskLength := 48 // For IPv4 addresses, do a match on exact address, so begin = ip and end = // next higher IP. if ip.To4() != nil { - copy(begin, ip) - copy(end, ip) - incrementIP(&end, 15) - } else { - // For IPv6, match on a certain subnet range, since one person can commonly - // have an entire /48 to themselves. - maskLength := 48 - mask := net.CIDRMask(maskLength, 128) - begin = ip.Mask(mask) - copy(end, begin) - incrementIP(&end, (maskLength/8)-1) + maskLength = 128 } + + mask := net.CIDRMask(maskLength, 128) + begin := ip.Mask(mask) + end := incrementIP(begin, maskLength) + return begin, end } From 200539521f5f63cccbe329be70d9fbbbf4deeba9 Mon Sep 17 00:00:00 2001 From: Roland Shoemaker Date: Fri, 9 Oct 2015 11:22:54 -0700 Subject: [PATCH 8/9] Add short CONTRIBUTING.md file with links to the patch guidelines in Wiki & mailing list/IRC --- CONTRIBUTING.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..dfd39385e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing to Boulder + +> **Note:** We are currently in a *General Availability* only merge window, meaning +> we will only be reviewing & merging patches which close a issue tagged with the *General +> Availability* milestone. + +Thanks for helping us build Boulder, if you haven't already had a chance to look +over our patch submission guidelines take a minute to do so now. + +* [Patch requirements](https://github.com/letsencrypt/boulder/wiki/Boulder-Development#patch-requirements) +* [Review requirements](https://github.com/letsencrypt/boulder/wiki/Boulder-Development#review-requirements) +* [Patch guidelines](https://github.com/letsencrypt/boulder/wiki/Boulder-Development#patch-guidelines) + * [Deployability](https://github.com/letsencrypt/boulder/wiki/Boulder-Development#deployability) + * [Good zero values for config fields](https://github.com/letsencrypt/boulder/wiki/Boulder-Development#good-zero-values-for-config-fields) + * [Flag-gated RPCs](https://github.com/letsencrypt/boulder/wiki/Boulder-Development#flag-gated-rpcs) + * [Dependencies](https://github.com/letsencrypt/boulder/wiki/Boulder-Development#dependencies) + +## Problems or questions? + +The best place to ask dev related questions is either the [discussion list](https://groups.google.com/a/letsencrypt.org/forum/#!forum/ca-dev) or [IRC](https://webchat.freenode.net/?channels=#letsencrypt). From 109f7cf75e8db5d7523b8a2ae750a919dce09403 Mon Sep 17 00:00:00 2001 From: Richard Barnes Date: Fri, 9 Oct 2015 16:35:19 -0400 Subject: [PATCH 9/9] Limit the number of contacts per registration --- cmd/boulder-ra/main.go | 3 ++- cmd/shell.go | 2 ++ ra/registration-authority.go | 21 ++++++++++++++------- ra/registration-authority_test.go | 22 +++++++++++++--------- test/boulder-config.json | 1 + wfe/web-front-end_test.go | 2 +- 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/cmd/boulder-ra/main.go b/cmd/boulder-ra/main.go index 832cfac7f..e9facf68d 100644 --- a/cmd/boulder-ra/main.go +++ b/cmd/boulder-ra/main.go @@ -46,7 +46,8 @@ func main() { rateLimitPolicies, err := cmd.LoadRateLimitPolicies(c.RA.RateLimitPoliciesFilename) cmd.FailOnError(err, "Couldn't load rate limit policies file") - rai := ra.NewRegistrationAuthorityImpl(clock.Default(), auditlogger, stats, rateLimitPolicies) + rai := ra.NewRegistrationAuthorityImpl(clock.Default(), auditlogger, stats, + rateLimitPolicies, c.RA.MaxContactsPerRegistration) rai.PA = pa raDNSTimeout, err := time.ParseDuration(c.Common.DNSTimeout) cmd.FailOnError(err, "Couldn't parse RA DNS timeout") diff --git a/cmd/shell.go b/cmd/shell.go index 0469a950d..99d58d9f2 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -99,6 +99,8 @@ type Config struct { MaxConcurrentRPCServerRequests int64 + MaxContactsPerRegistration int + // DebugAddr is the address to run the /debug handlers on. DebugAddr string } diff --git a/ra/registration-authority.go b/ra/registration-authority.go index dd4990527..32ef80201 100644 --- a/ra/registration-authority.go +++ b/ra/registration-authority.go @@ -51,10 +51,11 @@ type RegistrationAuthorityImpl struct { tiMu *sync.RWMutex totalIssuedCache int lastIssuedCount *time.Time + maxContactsPerReg int } // NewRegistrationAuthorityImpl constructs a new RA object. -func NewRegistrationAuthorityImpl(clk clock.Clock, logger *blog.AuditLogger, stats statsd.Statter, policies cmd.RateLimitConfig) RegistrationAuthorityImpl { +func NewRegistrationAuthorityImpl(clk clock.Clock, logger *blog.AuditLogger, stats statsd.Statter, policies cmd.RateLimitConfig, maxContactsPerReg int) RegistrationAuthorityImpl { ra := RegistrationAuthorityImpl{ stats: stats, clk: clk, @@ -62,6 +63,7 @@ func NewRegistrationAuthorityImpl(clk clock.Clock, logger *blog.AuditLogger, sta authorizationLifetime: DefaultAuthorizationLifetime, rlPolicies: policies, tiMu: new(sync.RWMutex), + maxContactsPerReg: maxContactsPerReg, } return ra } @@ -176,7 +178,7 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration) (re // MergeUpdate. But we need to fill it in for new registrations. reg.InitialIP = init.InitialIP - err = validateContacts(reg.Contact, ra.DNSResolver, ra.stats) + err = ra.validateContacts(reg.Contact) if err != nil { return } @@ -193,15 +195,20 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(init core.Registration) (re return } -func validateContacts(contacts []*core.AcmeURL, resolver core.DNSResolver, stats statsd.Statter) (err error) { +func (ra *RegistrationAuthorityImpl) validateContacts(contacts []*core.AcmeURL) (err error) { + if ra.maxContactsPerReg > 0 && len(contacts) > ra.maxContactsPerReg { + return core.MalformedRequestError(fmt.Sprintf("Too many contacts provided: %d > %d", + len(contacts), ra.maxContactsPerReg)) + } + for _, contact := range contacts { switch contact.Scheme { case "tel": continue case "mailto": - rtt, err := validateEmail(contact.Opaque, resolver) - stats.TimingDuration("RA.DNS.RTT.MX", rtt, 1.0) - stats.Inc("RA.DNS.Rate", 1, 1.0) + rtt, err := validateEmail(contact.Opaque, ra.DNSResolver) + ra.stats.TimingDuration("RA.DNS.RTT.MX", rtt, 1.0) + ra.stats.Inc("RA.DNS.Rate", 1, 1.0) if err != nil { return err } @@ -574,7 +581,7 @@ func (ra *RegistrationAuthorityImpl) checkLimits(names []string, regID int64) er func (ra *RegistrationAuthorityImpl) UpdateRegistration(base core.Registration, update core.Registration) (reg core.Registration, err error) { base.MergeUpdate(update) - err = validateContacts(base.Contact, ra.DNSResolver, ra.stats) + err = ra.validateContacts(base.Contact) if err != nil { return } diff --git a/ra/registration-authority_test.go b/ra/registration-authority_test.go index d2f2562c0..70e67ee6b 100644 --- a/ra/registration-authority_test.go +++ b/ra/registration-authority_test.go @@ -224,7 +224,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut Threshold: 100, Window: cmd.ConfigDuration{Duration: 24 * 90 * time.Hour}, }, - }) + }, 1) ra.SA = ssa ra.VA = va ra.CA = &ca @@ -259,30 +259,34 @@ func assertAuthzEqual(t *testing.T, a1, a2 core.Authorization) { } func TestValidateContacts(t *testing.T) { + _, _, ra, _, cleanUp := initAuthorities(t) + defer cleanUp() + tel, _ := core.ParseAcmeURL("tel:") ansible, _ := core.ParseAcmeURL("ansible:earth.sol.milkyway.laniakea/letsencrypt") validEmail, _ := core.ParseAcmeURL("mailto:admin@email.com") invalidEmail, _ := core.ParseAcmeURL("mailto:admin@example.com") malformedEmail, _ := core.ParseAcmeURL("mailto:admin.com") - nStats, _ := statsd.NewNoopClient() - - err := validateContacts([]*core.AcmeURL{}, &mocks.DNSResolver{}, nStats) + err := ra.validateContacts([]*core.AcmeURL{}) test.AssertNotError(t, err, "No Contacts") - err = validateContacts([]*core.AcmeURL{tel}, &mocks.DNSResolver{}, nStats) + err = ra.validateContacts([]*core.AcmeURL{tel, validEmail}) + test.AssertError(t, err, "Too Many Contacts") + + err = ra.validateContacts([]*core.AcmeURL{tel}) test.AssertNotError(t, err, "Simple Telephone") - err = validateContacts([]*core.AcmeURL{validEmail}, &mocks.DNSResolver{}, nStats) + err = ra.validateContacts([]*core.AcmeURL{validEmail}) test.AssertNotError(t, err, "Valid Email") - err = validateContacts([]*core.AcmeURL{invalidEmail}, &mocks.DNSResolver{}, nStats) + err = ra.validateContacts([]*core.AcmeURL{invalidEmail}) test.AssertError(t, err, "Invalid Email") - err = validateContacts([]*core.AcmeURL{malformedEmail}, &mocks.DNSResolver{}, nStats) + err = ra.validateContacts([]*core.AcmeURL{malformedEmail}) test.AssertError(t, err, "Malformed Email") - err = validateContacts([]*core.AcmeURL{ansible}, &mocks.DNSResolver{}, nStats) + err = ra.validateContacts([]*core.AcmeURL{ansible}) test.AssertError(t, err, "Unknown scehme") } diff --git a/test/boulder-config.json b/test/boulder-config.json index 6684deeee..d8272003c 100644 --- a/test/boulder-config.json +++ b/test/boulder-config.json @@ -121,6 +121,7 @@ "ra": { "rateLimitPoliciesFilename": "test/rate-limit-policies.yml", "maxConcurrentRPCServerRequests": 16, + "maxContactsPerRegistration": 100, "debugAddr": "localhost:8002" }, diff --git a/wfe/web-front-end_test.go b/wfe/web-front-end_test.go index a89d9d4fa..f37f1d3bc 100644 --- a/wfe/web-front-end_test.go +++ b/wfe/web-front-end_test.go @@ -547,7 +547,7 @@ func TestIssueCertificate(t *testing.T) { // TODO: Use a mock RA so we can test various conditions of authorized, not // authorized, etc. stats, _ := statsd.NewNoopClient(nil) - ra := ra.NewRegistrationAuthorityImpl(fakeClock, wfe.log, stats, cmd.RateLimitConfig{}) + ra := ra.NewRegistrationAuthorityImpl(fakeClock, wfe.log, stats, cmd.RateLimitConfig{}, 0) ra.SA = &mocks.StorageAuthority{} ra.CA = &MockCA{} ra.PA = &MockPA{}