Confine contact addresses to the WFE (#8245)

Change the WFE to stop populating the Contact field of the
NewRegistration requests it sends to the RA. Similarly change the WFE to
ignore the Contact field of any update-account requests it receives,
thereby removing all calls to the RA's UpdateRegistrationContact method.

Hoist the RA's contact validation logic into the WFE, so that we can
still return errors to clients which are presenting grossly malformed
contact fields, and have a first layer of protection against trying to
send malformed addresses to email-exporter.

A follow-up change (after a deploy cycle) will remove the deprecated RA
and SA methods.

Part of https://github.com/letsencrypt/boulder/issues/8199
This commit is contained in:
Aaron Gable 2025-06-25 15:51:44 -07:00 committed by GitHub
parent ea23894910
commit e110ec9a03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 265 additions and 244 deletions

View File

@ -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 {
@ -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
@ -346,6 +355,7 @@ func main() {
logger,
c.WFE.Timeout.Duration,
c.WFE.StaleTimeout.Duration,
c.WFE.MaxContactsPerRegistration,
rac,
sac,
eec,

View File

@ -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
@ -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")
@ -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

View File

@ -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",

View File

@ -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()

View File

@ -14,7 +14,6 @@ import (
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/eggsampler/acme/v3"
@ -42,126 +41,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:valid@valid.com", ""},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `empty contact`,
},
{
name: "empty proto",
contacts: []string{"mailto:valid@valid.com", " "},
expectedProbType: "urn:ietf:params:acme:error:unsupportedContact",
expectedProbDetail: `only contact scheme 'mailto:' is supported`,
},
{
name: "empty mailto",
contacts: []string{"mailto:valid@valid.com", "mailto:"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `unable to parse email address`,
},
{
name: "non-ascii mailto",
contacts: []string{"mailto:valid@valid.com", "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:valid@valid.com", "mailto:a@"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: `unable to parse email address`,
},
{
name: "forbidden contact domain",
contacts: []string{"mailto:valid@valid.com", "mailto:a@example.com"},
expectedProbType: "urn:ietf:params:acme:error:invalidContact",
expectedProbDetail: "contact email has forbidden domain \"example.com\"",
},
{
name: "contact domain invalid TLD",
contacts: []string{"mailto:valid@valid.com", "mailto:a@example.cpu"},
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:valid@valid.com", "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:valid@valid.com")
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()

View File

@ -14,6 +14,7 @@ import (
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"time"
@ -144,6 +145,9 @@ type WebFrontEndImpl struct {
// CORS settings
AllowOrigins []string
// How many contacts to allow in a single NewAccount request.
maxContactsPerReg int
// requestTimeout is the per-request overall timeout.
requestTimeout time.Duration
@ -176,6 +180,7 @@ func NewWebFrontEndImpl(
logger blog.Logger,
requestTimeout time.Duration,
staleTimeout time.Duration,
maxContactsPerReg int,
rac rapb.RegistrationAuthorityClient,
sac sapb.StorageAuthorityReadOnlyClient,
eec emailpb.ExporterClient,
@ -215,6 +220,7 @@ func NewWebFrontEndImpl(
stats: initStats(stats),
requestTimeout: requestTimeout,
staleTimeout: staleTimeout,
maxContactsPerReg: maxContactsPerReg,
ra: rac,
sa: sac,
ee: eec,
@ -633,26 +639,56 @@ func link(url, relation string) string {
return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation)
}
// contactsToEmails converts a *[]string of contacts (e.g. mailto:
// person@example.com) to a []string of valid email addresses. Non-email
// contacts or contacts with invalid email addresses are ignored.
func contactsToEmails(contacts *[]string) []string {
if contacts == nil {
return nil
// contactsToEmails converts a slice of ACME contacts (e.g.
// "mailto:person@example.com") to a slice of valid email addresses. If any of
// the contacts contain non-mailto schemes, unparsable addresses, or forbidden
// mail domains, it returns an error so that we can provide feedback to
// misconfigured clients.
func (wfe *WebFrontEndImpl) contactsToEmails(contacts []string) ([]string, error) {
if len(contacts) == 0 {
return nil, nil
}
if wfe.maxContactsPerReg > 0 && len(contacts) > wfe.maxContactsPerReg {
return nil, berrors.MalformedError("too many contacts provided: %d > %d", len(contacts), wfe.maxContactsPerReg)
}
var emails []string
for _, c := range *contacts {
if !strings.HasPrefix(c, "mailto:") {
continue
for _, contact := range contacts {
if contact == "" {
return nil, berrors.InvalidEmailError("empty contact")
}
address := strings.TrimPrefix(c, "mailto:")
err := policy.ValidEmail(address)
parsed, err := url.Parse(contact)
if err != nil {
continue
return nil, berrors.InvalidEmailError("unparsable contact")
}
emails = append(emails, address)
if parsed.Scheme != "mailto" {
return nil, berrors.UnsupportedContactError("only contact scheme 'mailto:' is supported")
}
if parsed.RawQuery != "" || contact[len(contact)-1] == '?' {
return nil, berrors.InvalidEmailError("contact email contains a question mark")
}
if parsed.Fragment != "" || contact[len(contact)-1] == '#' {
return nil, berrors.InvalidEmailError("contact email contains a '#'")
}
if !core.IsASCII(contact) {
return nil, berrors.InvalidEmailError("contact email contains non-ASCII characters")
}
err = policy.ValidEmail(parsed.Opaque)
if err != nil {
return nil, err
}
emails = append(emails, parsed.Opaque)
}
return emails
return emails, nil
}
// checkNewAccountLimits checks whether sufficient limit quota exists for the
@ -703,9 +739,9 @@ func (wfe *WebFrontEndImpl) NewAccount(
}
var accountCreateRequest struct {
Contact *[]string `json:"contact"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
OnlyReturnExisting bool `json:"onlyReturnExisting"`
Contact []string `json:"contact"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
OnlyReturnExisting bool `json:"onlyReturnExisting"`
}
err = json.Unmarshal(body, &accountCreateRequest)
@ -773,29 +809,25 @@ func (wfe *WebFrontEndImpl) NewAccount(
return
}
// Do this extraction now, so that we can reject requests whose contact field
// does not contain valid contacts before we actually create the account.
emails, err := wfe.contactsToEmails(accountCreateRequest.Contact)
if err != nil {
wfe.sendError(response, logEvent, web.ProblemDetailsForError(err, "invalid contact"), nil)
return
}
ip, err := extractRequesterIP(request)
if err != nil {
wfe.sendError(
response,
logEvent,
probs.ServerInternal("couldn't parse the remote (that is, the client's) address"),
fmt.Errorf("Couldn't parse RemoteAddr: %s", request.RemoteAddr),
fmt.Errorf("couldn't parse RemoteAddr: %s", request.RemoteAddr),
)
return
}
var contacts []string
if accountCreateRequest.Contact != nil {
contacts = *accountCreateRequest.Contact
}
// Create corepb.Registration from provided account information
reg := corepb.Registration{
Contact: contacts,
Agreement: wfe.SubscriberAgreementURL,
Key: keyBytes,
}
refundLimits, err := wfe.checkNewAccountLimits(ctx, ip)
if err != nil {
if errors.Is(err, berrors.RateLimit) {
@ -815,7 +847,12 @@ func (wfe *WebFrontEndImpl) NewAccount(
}
}()
// Send the registration to the RA via grpc
// Create corepb.Registration from provided account information
reg := corepb.Registration{
Agreement: wfe.SubscriberAgreementURL,
Key: keyBytes,
}
acctPB, err := wfe.ra.NewRegistration(ctx, &reg)
if err != nil {
if errors.Is(err, berrors.Duplicate) {
@ -870,7 +907,6 @@ func (wfe *WebFrontEndImpl) NewAccount(
}
newRegistrationSuccessful = true
emails := contactsToEmails(accountCreateRequest.Contact)
if wfe.ee != nil && len(emails) > 0 {
_, err := wfe.ee.SendContacts(ctx, &emailpb.SendContactsRequest{
// Note: We are explicitly using the contacts provided by the
@ -1411,11 +1447,10 @@ func (wfe *WebFrontEndImpl) Account(
// valid update the resulting updated account is returned, otherwise a problem
// is returned.
func (wfe *WebFrontEndImpl) updateAccount(ctx context.Context, requestBody []byte, currAcct *core.Registration) (*core.Registration, error) {
// Only the Contact and Status fields of an account may be updated this way.
// Only the Status field of an account may be updated this way.
// For key updates clients should be using the key change endpoint.
var accountUpdateRequest struct {
Contact *[]string `json:"contact"`
Status core.AcmeStatus `json:"status"`
Status core.AcmeStatus `json:"status"`
}
err := json.Unmarshal(requestBody, &accountUpdateRequest)
@ -1423,59 +1458,29 @@ func (wfe *WebFrontEndImpl) updateAccount(ctx context.Context, requestBody []byt
return nil, berrors.MalformedError("parsing account update request: %s", err)
}
// If a user tries to send both a deactivation request and an update to
// their contacts, the deactivation will take place and return before an
// update would be performed. Deactivation deletes the contacts field.
if accountUpdateRequest.Status == core.StatusDeactivated {
switch accountUpdateRequest.Status {
case core.StatusValid, "":
// They probably intended to update their contact address, but we don't do
// that anymore, so simply return their account as-is. We don't error out
// here because it would break too many clients.
return currAcct, nil
case core.StatusDeactivated:
updatedAcct, err := wfe.ra.DeactivateRegistration(
ctx, &rapb.DeactivateRegistrationRequest{RegistrationID: currAcct.ID})
if err != nil {
return nil, fmt.Errorf("deactivating account: %w", err)
}
if updatedAcct.Status == string(core.StatusDeactivated) {
// The request was handled by an updated RA/SA, which returned the updated
// account object.
updatedReg, err := bgrpc.PbToRegistration(updatedAcct)
if err != nil {
return nil, fmt.Errorf("parsing deactivated account: %w", err)
}
return &updatedReg, nil
} else {
// The request was handled by an old RA/SA, which returned nothing.
// Instead, modify the existing account object in place and return it.
// TODO(#5554): Remove this after all RAs and SAs are updated.
currAcct.Status = core.StatusDeactivated
currAcct.Contact = nil
return currAcct, nil
updatedReg, err := bgrpc.PbToRegistration(updatedAcct)
if err != nil {
return nil, fmt.Errorf("parsing deactivated account: %w", err)
}
}
return &updatedReg, nil
if accountUpdateRequest.Status != core.StatusValid && accountUpdateRequest.Status != "" {
default:
return nil, berrors.MalformedError("invalid status %q for account update request, must be %q or %q", accountUpdateRequest.Status, core.StatusValid, core.StatusDeactivated)
}
if accountUpdateRequest.Contact == nil {
// We use a pointer-to-slice for the contacts field so that we can tell the
// difference between the request not including the contact field, and the
// request including an empty contact list. If the field was omitted
// entirely, they don't want us to update it, so there's no work to do here.
return currAcct, nil
}
updatedAcct, err := wfe.ra.UpdateRegistrationContact(ctx, &rapb.UpdateRegistrationContactRequest{
RegistrationID: currAcct.ID, Contacts: *accountUpdateRequest.Contact})
if err != nil {
return nil, fmt.Errorf("updating account: %w", err)
}
// Convert proto to core.Registration for return
updatedReg, err := bgrpc.PbToRegistration(updatedAcct)
if err != nil {
return nil, fmt.Errorf("parsing updated account: %w", err)
}
return &updatedReg, nil
}
// deactivateAuthorization processes the given JWS POST body as a request to

View File

@ -192,19 +192,12 @@ type MockRegistrationAuthority struct {
func (ra *MockRegistrationAuthority) NewRegistration(ctx context.Context, in *corepb.Registration, _ ...grpc.CallOption) (*corepb.Registration, error) {
in.Id = 1
in.Contact = nil
created := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
in.CreatedAt = timestamppb.New(created)
return in, nil
}
func (ra *MockRegistrationAuthority) UpdateRegistrationContact(ctx context.Context, in *rapb.UpdateRegistrationContactRequest, _ ...grpc.CallOption) (*corepb.Registration, error) {
return &corepb.Registration{
Status: string(core.StatusValid),
Contact: in.Contacts,
Key: []byte(test1KeyPublicJSON),
}, nil
}
func (ra *MockRegistrationAuthority) UpdateRegistrationKey(ctx context.Context, in *rapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) {
return &corepb.Registration{
Status: string(core.StatusValid),
@ -436,6 +429,7 @@ func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) {
blog.NewMock(),
10*time.Second,
10*time.Second,
2,
&MockRegistrationAuthority{clk: fc},
mockSA,
nil,
@ -1373,8 +1367,6 @@ func TestNewECDSAAccount(t *testing.T) {
responseBody := responseWriter.Body.String()
err := json.Unmarshal([]byte(responseBody), &acct)
test.AssertNotError(t, err, "Couldn't unmarshal returned account object")
test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account")
test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com")
test.AssertEquals(t, acct.Agreement, "")
test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/1")
@ -1487,8 +1479,6 @@ func TestEmptyAccount(t *testing.T) {
var acct core.Registration
err := json.Unmarshal([]byte(responseBody), &acct)
test.AssertNotError(t, err, "Couldn't unmarshal returned account object")
test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account")
test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com")
test.AssertEquals(t, acct.Agreement, "")
}
@ -1575,8 +1565,6 @@ func TestNewAccount(t *testing.T) {
responseBody := responseWriter.Body.String()
err := json.Unmarshal([]byte(responseBody), &acct)
test.AssertNotError(t, err, "Couldn't unmarshal returned account object")
test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account")
test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com")
// Agreement is an ACMEv1 field and should not be present
test.AssertEquals(t, acct.Agreement, "")
@ -1655,14 +1643,129 @@ func TestNewAccountNoID(t *testing.T) {
"n": "qnARLrT7Xz4gRcKyLdydmCr-ey9OuPImX4X40thk3on26FkMznR3fRjs66eLK7mmPcBZ6uOJseURU6wAaZNmemoYx1dMvqvWWIyiQleHSD7Q8vBrhR6uIoO4jAzJZR-ChzZuSDt7iHN-3xUVspu5XGwXU_MVJZshTwp4TaFx5elHIT_ObnTvTOU3Xhish07AbgZKmWsVbXh5s-CrIicU4OexJPgunWZ_YJJueOKmTvnLlTV4MzKR2oZlBKZ27S0-SfdV_QDx_ydle5oMAyKVtlAV35cyPMIsYNwgUGBCdY_2Uzi5eX0lTc7MPRwz6qR1kip-i59VcGcUQgqHV6Fyqw",
"e": "AQAB"
},
"contact": [
"mailto:person@mail.com"
],
"createdAt": "2021-01-01T00:00:00Z",
"status": ""
}`)
}
func TestContactsToEmails(t *testing.T) {
t.Parallel()
wfe, _, _ := setupWFE(t)
for _, tc := range []struct {
name string
contacts []string
want []string
wantErr string
}{
{
name: "no contacts",
contacts: []string{},
want: []string{},
},
{
name: "happy path",
contacts: []string{"mailto:one@mail.com", "mailto:two@mail.com"},
want: []string{"one@mail.com", "two@mail.com"},
},
{
name: "empty url",
contacts: []string{""},
wantErr: "empty contact",
},
{
name: "too many contacts",
contacts: []string{"mailto:one@mail.com", "mailto:two@mail.com", "mailto:three@mail.com"},
wantErr: "too many contacts",
},
{
name: "unknown scheme",
contacts: []string{"ansible:earth.sol.milkyway.laniakea/letsencrypt"},
wantErr: "contact scheme",
},
{
name: "malformed email",
contacts: []string{"mailto:admin.com"},
wantErr: "unable to parse email address",
},
{
name: "non-ascii email",
contacts: []string{"mailto:señor@email.com"},
wantErr: "contains non-ASCII characters",
},
{
name: "unarseable email",
contacts: []string{"mailto:a@mail.com, b@mail.com"},
wantErr: "unable to parse email address",
},
{
name: "forbidden example domain",
contacts: []string{"mailto:a@example.org"},
wantErr: "forbidden",
},
{
name: "forbidden non-public domain",
contacts: []string{"mailto:admin@localhost"},
wantErr: "needs at least one dot",
},
{
name: "forbidden non-iana domain",
contacts: []string{"mailto:admin@non.iana.suffix"},
wantErr: "does not end with a valid public suffix",
},
{
name: "forbidden ip domain",
contacts: []string{"mailto:admin@1.2.3.4"},
wantErr: "value is an IP address",
},
{
name: "forbidden bracketed ip domain",
contacts: []string{"mailto:admin@[1.2.3.4]"},
wantErr: "contains an invalid character",
},
{
name: "query parameter",
contacts: []string{"mailto:admin@a.com?no-reminder-emails"},
wantErr: "contains a question mark",
},
{
name: "empty query parameter",
contacts: []string{"mailto:admin@a.com?"},
wantErr: "contains a question mark",
},
{
name: "fragment url",
contacts: []string{"mailto:admin@a.com#optional"},
wantErr: "contains a '#'",
},
{
name: "empty fragment url",
contacts: []string{"mailto:admin@a.com#"},
wantErr: "contains a '#'",
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := wfe.contactsToEmails(tc.contacts)
if tc.wantErr != "" {
if err == nil {
t.Fatalf("contactsToEmails(%#v) = nil, but want %q", tc.contacts, tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Errorf("contactsToEmails(%#v) = %q, but want %q", tc.contacts, err.Error(), tc.wantErr)
}
} else {
if err != nil {
t.Fatalf("contactsToEmails(%#v) = %q, but want %#v", tc.contacts, err.Error(), tc.want)
}
if !slices.Equal(got, tc.want) {
t.Errorf("contactsToEmails(%#v) = %#v, but want %#v", tc.contacts, got, tc.want)
}
}
})
}
}
func TestGetAuthorizationHandler(t *testing.T) {
wfe, _, signer := setupWFE(t)
@ -1885,46 +1988,57 @@ func TestUpdateAccount(t *testing.T) {
name string
req string
wantAcct *core.Registration
wantErr string
}{
{
name: "deactivate clears contact",
name: "empty status",
req: `{}`,
wantAcct: &core.Registration{Status: core.StatusValid},
},
{
name: "empty status with contact",
req: `{"contact": ["mailto:admin@example.com"]}`,
wantAcct: &core.Registration{Status: core.StatusValid},
},
{
name: "valid",
req: `{"status": "valid"}`,
wantAcct: &core.Registration{Status: core.StatusValid},
},
{
name: "valid with contact",
req: `{"status": "valid", "contact": ["mailto:admin@example.com"]}`,
wantAcct: &core.Registration{Status: core.StatusValid},
},
{
name: "deactivate",
req: `{"status": "deactivated"}`,
wantAcct: &core.Registration{Status: core.StatusDeactivated},
},
{
name: "deactivate takes priority over contact change",
name: "deactivate with contact",
req: `{"status": "deactivated", "contact": ["mailto:admin@example.com"]}`,
wantAcct: &core.Registration{Status: core.StatusDeactivated},
},
{
name: "change contact",
req: `{"contact": ["mailto:admin@example.com"]}`,
wantAcct: &core.Registration{Status: core.StatusValid, Contact: &[]string{"mailto:admin@example.com"}},
name: "unrecognized status",
req: `{"status": "foo"}`,
wantErr: "invalid status",
},
{
name: "change contact with unchanged status",
req: `{"status": "valid", "contact": ["mailto:admin@example.com"]}`,
wantAcct: &core.Registration{Status: core.StatusValid, Contact: &[]string{"mailto:admin@example.com"}},
},
{
name: "unchanged status leaves contact untouched",
req: `{"status": "valid"}`,
wantAcct: &core.Registration{Status: core.StatusValid, Contact: &[]string{"mailto:webmaster@example.com"}},
// We're happy to ignore fields we don't recognize; they might be useful
// for other CAs.
name: "unrecognized request field",
req: `{"foo": "bar"}`,
wantAcct: &core.Registration{Status: core.StatusValid},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
acct := core.Registration{
Status: core.StatusValid,
Contact: &[]string{"mailto:webmaster@example.com"},
}
gotAcct, gotProb := wfe.updateAccount(context.Background(), []byte(tc.req), &acct)
if gotProb != nil {
t.Fatalf("want success, got problem %s", gotProb)
}
currAcct := core.Registration{Status: core.StatusValid}
gotAcct, gotProb := wfe.updateAccount(context.Background(), []byte(tc.req), &currAcct)
if tc.wantAcct != nil {
if gotAcct.Status != tc.wantAcct.Status {
t.Errorf("want status %s, got %s", tc.wantAcct.Status, gotAcct.Status)
@ -1933,6 +2047,14 @@ func TestUpdateAccount(t *testing.T) {
t.Errorf("want contact %v, got %v", tc.wantAcct.Contact, gotAcct.Contact)
}
}
if tc.wantErr != "" {
if gotProb == nil {
t.Fatalf("want error %q, got nil", tc.wantErr)
}
if !strings.Contains(gotProb.Error(), tc.wantErr) {
t.Errorf("want error %q, got %q", tc.wantErr, gotProb.Error())
}
}
})
}
}
@ -4186,7 +4308,7 @@ func TestNewAccountCreatesContacts(t *testing.T) {
{
name: "One valid email, one invalid email",
contacts: []string{"mailto:person@mail.com", "mailto:lol@%mail.com"},
expected: []string{"person@mail.com"},
expected: []string{},
},
{
name: "Valid email with non-email prefix",