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:
parent
ea23894910
commit
e110ec9a03
|
|
@ -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,
|
||||
|
|
|
|||
10
ra/ra.go
10
ra/ra.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
161
wfe2/wfe.go
161
wfe2/wfe.go
|
|
@ -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, ®)
|
||||
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
|
||||
|
|
|
|||
202
wfe2/wfe_test.go
202
wfe2/wfe_test.go
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue