Skip to content

Confine contact addresses to the WFE #8245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/boulder-wfe2/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ type Config struct {
// Deprecated: This field no longer has any effect.
PendingAuthorizationLifetimeDays int `validate:"-"`

// MaxContactsPerRegistration limits the number of contact addresses which
// can be provided in a single NewAccount request. Requests containing more
// contacts than this are rejected. Default: 10.
MaxContactsPerRegistration int `validate:"omitempty,min=1"`

AccountCache *CacheConfig

Limiter struct {
Expand Down Expand Up @@ -312,6 +317,10 @@ func main() {
c.WFE.StaleTimeout.Duration = time.Minute * 10
}

if c.WFE.MaxContactsPerRegistration == 0 {
c.WFE.MaxContactsPerRegistration = 10
}

var limiter *ratelimits.Limiter
var txnBuilder *ratelimits.TransactionBuilder
var limiterRedis *bredis.Ring
Expand Down Expand Up @@ -346,6 +355,7 @@ func main() {
logger,
c.WFE.Timeout.Duration,
c.WFE.StaleTimeout.Duration,
c.WFE.MaxContactsPerRegistration,
rac,
sac,
eec,
Expand Down
10 changes: 7 additions & 3 deletions ra/ra.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, reques
}

// Check that contacts conform to our expectations.
// TODO(#8199): Remove this when no contacts are included in any requests.
err = ra.validateContacts(request.Contact)
if err != nil {
return nil, err
Expand Down Expand Up @@ -585,7 +586,7 @@ func (ra *RegistrationAuthorityImpl) validateContacts(contacts []string) error {
}
parsed, err := url.Parse(contact)
if err != nil {
return berrors.InvalidEmailError("invalid contact")
return berrors.InvalidEmailError("unparsable contact")
}
if parsed.Scheme != "mailto" {
return berrors.UnsupportedContactError("only contact scheme 'mailto:' is supported")
Expand Down Expand Up @@ -1399,8 +1400,11 @@ func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, precertDER []b
return scts, nil
}

// UpdateRegistrationContact updates an existing Registration's contact.
// The updated contacts field may be empty.
// UpdateRegistrationContact updates an existing Registration's contact. The
// updated contacts field may be empty.
//
// Deprecated: This method has no callers. See
// https://github.com/letsencrypt/boulder/issues/8199 for removal.
func (ra *RegistrationAuthorityImpl) UpdateRegistrationContact(ctx context.Context, req *rapb.UpdateRegistrationContactRequest) (*corepb.Registration, error) {
if core.IsAnyNilOrZero(req.RegistrationID) {
return nil, errIncompleteGRPCRequest
Expand Down
1 change: 1 addition & 0 deletions test/config-next/wfe2.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"directoryWebsite": "https://github.com/letsencrypt/boulder",
"legacyKeyIDPrefix": "http://boulder.service.consul:4000/reg/",
"goodkey": {},
"maxContactsPerRegistration": 10,
"tls": {
"caCertFile": "test/certs/ipki/minica.pem",
"certFile": "test/certs/ipki/wfe.boulder/cert.pem",
Expand Down
4 changes: 2 additions & 2 deletions test/integration/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
)

// TestNewAccount tests that various new-account requests are handled correctly.
// It does not test malform account contacts, as those are covered by
// TestAccountEmailError in errors_test.go.
// It does not test malformed account contacts, as we no longer care about
// how well-formed the contact string is, since we no longer store them.
func TestNewAccount(t *testing.T) {
t.Parallel()

Expand Down
121 changes: 0 additions & 121 deletions test/integration/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"fmt"
"io"
"net/http"
"strings"
"testing"

"github.com/eggsampler/acme/v3"
Expand Down Expand Up @@ -39,126 +38,6 @@ func TestTooBigOrderError(t *testing.T) {
test.AssertContains(t, prob.Detail, "Order cannot contain more than 100 identifiers")
}

// TestAccountEmailError tests that registering a new account, or updating an
// account, with invalid contact information produces the expected problem
// result to ACME clients.
func TestAccountEmailError(t *testing.T) {
t.Parallel()

// The registrations.contact field is VARCHAR(191). 175 'a' characters plus
// the prefix "mailto:" and the suffix "@a.com" makes exactly 191 bytes of
// encoded JSON. The correct size to hit our maximum DB field length.
var longStringBuf strings.Builder
longStringBuf.WriteString("mailto:")
for range 175 {
longStringBuf.WriteRune('a')
}
longStringBuf.WriteString("@a.com")

createErrorPrefix := "Error creating new account :: "
updateErrorPrefix := "Unable to update account :: invalid contact: "

testCases := []struct {
name string
contacts []string
expectedProbType string
expectedProbDetail string
}{
{
name: "empty contact",
contacts: []string{"mailto:[email protected]", ""},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `empty contact`,
},
{
name: "empty proto",
contacts: []string{"mailto:[email protected]", " "},
expectedProbType: "urn:ietf:params:acme:error:unsupportedContact",
expectedProbDetail: `only contact scheme 'mailto:' is supported`,
},
{
name: "empty mailto",
contacts: []string{"mailto:[email protected]", "mailto:"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `unable to parse email address`,
},
{
name: "non-ascii mailto",
contacts: []string{"mailto:[email protected]", "mailto:cpu@l̴etsencrypt.org"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `contact email contains non-ASCII characters`,
},
{
name: "too many contacts",
contacts: []string{"a", "b", "c", "d"},
expectedProbType: "urn:ietf:params:acme:error:malformed",
expectedProbDetail: `too many contacts provided: 4 > 3`,
},
{
name: "invalid contact",
contacts: []string{"mailto:[email protected]", "mailto:a@"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `unable to parse email address`,
},
{
name: "forbidden contact domain",
contacts: []string{"mailto:[email protected]", "mailto:[email protected]"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: "contact email has forbidden domain \"example.com\"",
},
{
name: "contact domain invalid TLD",
contacts: []string{"mailto:[email protected]", "mailto:[email protected]"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `contact email has invalid domain: Domain name does not end with a valid public suffix (TLD)`,
},
{
name: "contact domain invalid",
contacts: []string{"mailto:[email protected]", "mailto:a@example./.com"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: "contact email has invalid domain: Domain name contains an invalid character",
},
{
name: "too long contact",
contacts: []string{
longStringBuf.String(),
},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `too many/too long contact(s). Please use shorter or fewer email addresses`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// First try registering a new account and ensuring the expected problem occurs
var prob acme.Problem
_, err := makeClient(tc.contacts...)
if err != nil {
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, tc.expectedProbType)
test.AssertEquals(t, prob.Detail, createErrorPrefix+tc.expectedProbDetail)
} else {
t.Errorf("expected %s type problem for %q, got nil",
tc.expectedProbType, strings.Join(tc.contacts, ","))
}

// Next try making a client with a good contact and updating with the test
// case contact info. The same problem should occur.
c, err := makeClient("mailto:[email protected]")
test.AssertNotError(t, err, "failed to create account with valid contact")
_, err = c.UpdateAccount(c.Account, tc.contacts...)
if err != nil {
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, tc.expectedProbType)
test.AssertEquals(t, prob.Detail, updateErrorPrefix+tc.expectedProbDetail)
} else {
t.Errorf("expected %s type problem after updating account to %q, got nil",
tc.expectedProbType, strings.Join(tc.contacts, ","))
}
})
}
}

func TestRejectedIdentifier(t *testing.T) {
t.Parallel()

Expand Down
Loading