Merge branch 'master' into challenge-head
This commit is contained in:
commit
22ef139419
|
|
@ -1,7 +1,7 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.5
|
||||
- 1.5.1
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
|
|
@ -56,3 +56,4 @@ env:
|
|||
|
||||
script:
|
||||
- bash test.sh
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ type Config struct {
|
|||
|
||||
MaxConcurrentRPCServerRequests int64
|
||||
|
||||
MaxContactsPerRegistration int
|
||||
|
||||
// DebugAddr is the address to run the /debug handlers on.
|
||||
DebugAddr string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -741,6 +747,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"`
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ func TestWillingToIssue(t *testing.T) {
|
|||
`zombocom`,
|
||||
`localhost`,
|
||||
`mail`,
|
||||
|
||||
// disallow capitalized letters for #927
|
||||
`CapitalizedLetters.com`,
|
||||
}
|
||||
|
||||
shouldBeNonPublic := []string{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
|
@ -50,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,
|
||||
|
|
@ -61,6 +63,7 @@ func NewRegistrationAuthorityImpl(clk clock.Clock, logger *blog.AuditLogger, sta
|
|||
authorizationLifetime: DefaultAuthorizationLifetime,
|
||||
rlPolicies: policies,
|
||||
tiMu: new(sync.RWMutex),
|
||||
maxContactsPerReg: maxContactsPerReg,
|
||||
}
|
||||
return ra
|
||||
}
|
||||
|
|
@ -138,17 +141,44 @@ 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() {
|
||||
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(), noRegistrationID) {
|
||||
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)
|
||||
|
||||
err = validateContacts(reg.Contact, ra.DNSResolver, ra.stats)
|
||||
// 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 = ra.validateContacts(reg.Contact)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -165,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
|
||||
}
|
||||
|
|
@ -186,7 +221,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 +231,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 +312,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")
|
||||
|
|
@ -546,7 +583,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
@ -220,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
|
||||
|
|
@ -255,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")
|
||||
}
|
||||
|
||||
|
|
@ -303,8 +311,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 +341,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)
|
||||
|
|
@ -403,6 +413,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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1056,7 +1078,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 +1089,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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// 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", 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()
|
||||
actual := incrementIP(ip, tc.index)
|
||||
expectedIP := net.ParseIP(tc.expected)
|
||||
if !actual.Equal(expectedIP) {
|
||||
t.Errorf("Expected incrementIP(%s, %d) to be %s, instead got %s",
|
||||
tc.ip, tc.index, expectedIP, actual.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
sa/model.go
12
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -229,6 +231,80 @@ func (ssa *SQLStorageAuthority) GetLatestValidAuthorization(registrationID int64
|
|||
return ssa.GetAuthorization(auth.ID)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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()
|
||||
// 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 {
|
||||
maskLength = 128
|
||||
}
|
||||
|
||||
mask := net.CIDRMask(maskLength, 128)
|
||||
begin := ip.Mask(mask)
|
||||
end := incrementIP(begin, maskLength)
|
||||
|
||||
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 +420,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 +523,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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@
|
|||
"ra": {
|
||||
"rateLimitPoliciesFilename": "test/rate-limit-policies.yml",
|
||||
"maxConcurrentRPCServerRequests": 16,
|
||||
"maxContactsPerRegistration": 100,
|
||||
"debugAddr": "localhost:8002"
|
||||
},
|
||||
|
||||
|
|
@ -189,6 +190,7 @@
|
|||
},
|
||||
|
||||
"publisher": {
|
||||
"maxConcurrentRPCServerRequests": 16,
|
||||
"debugAddr": "localhost:8009"
|
||||
},
|
||||
|
||||
|
|
@ -208,8 +210,7 @@
|
|||
"submissionRetries": 1,
|
||||
"submissionBackoff": "1s",
|
||||
"intermediateBundleFilename": "test/test-ca.pem"
|
||||
},
|
||||
"maxConcurrentRPCServerRequests": 16
|
||||
}
|
||||
},
|
||||
|
||||
"certChecker": {
|
||||
|
|
|
|||
|
|
@ -15,3 +15,8 @@ certificatesPerName:
|
|||
nginx.wtf: 10000
|
||||
registrationOverrides:
|
||||
101: 1000
|
||||
registrationsPerIP:
|
||||
window: 168h # 1 week
|
||||
threshold: 3
|
||||
overrides:
|
||||
127.0.0.1: 1000000
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))},
|
||||
},
|
||||
|
|
@ -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{}
|
||||
|
|
@ -845,12 +845,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"),
|
||||
|
|
@ -1353,11 +1354,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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue