Merge remote-tracking branch 'le/master' into cpu-wfe2-revocation

This commit is contained in:
Daniel 2017-08-31 16:07:43 -04:00
commit 38b1c2620c
No known key found for this signature in database
GPG Key ID: 08FB2BFC470E75B4
27 changed files with 1175 additions and 754 deletions

View File

@ -38,14 +38,13 @@ env:
- PATH=$HOME/bin:$PATH # protoc gets installed here - PATH=$HOME/bin:$PATH # protoc gets installed here
- GO15VENDOREXPERIMENT=1 - GO15VENDOREXPERIMENT=1
matrix: matrix:
- RUN="vet fmt migrations integration godep-restore errcheck generate dashlint" - RUN="vet fmt migrations integration godep-restore errcheck generate dashlint rpm"
# Config changes that have landed in master but not yet been applied to # Config changes that have landed in master but not yet been applied to
# production can be made in boulder-config-next.json. # production can be made in boulder-config-next.json.
- RUN="integration" BOULDER_CONFIG_DIR="test/config-next" - RUN="integration" BOULDER_CONFIG_DIR="test/config-next"
- RUN="unit" - RUN="unit"
- RUN="unit-next" BOULDER_CONFIG_DIR="test/config-next" - RUN="unit-next" BOULDER_CONFIG_DIR="test/config-next"
- RUN="coverage" - RUN="coverage"
- RUN="rpm"
install: install:
- ./test/travis-before-install.sh - ./test/travis-before-install.sh

View File

@ -23,6 +23,7 @@ Thanks for helping us build Boulder! This page contains requirements and guideli
# Patch Guidelines # Patch Guidelines
* Please include helpful comments. No need to gratuitously comment clear code, but make sure it's clear why things are being done. * Please include helpful comments. No need to gratuitously comment clear code, but make sure it's clear why things are being done.
* Include information in your pull request about what you're trying to accomplish with your patch. * Include information in your pull request about what you're trying to accomplish with your patch.
* Avoid named return values. See [#3017](https://github.com/letsencrypt/boulder/pull/3017) for an example of a subtle problem they can cause.
* Do not include `XXX`s or naked `TODO`s. Use the formats: * Do not include `XXX`s or naked `TODO`s. Use the formats:
``` ```
// TODO(<email-address>): Hoverboard + Time-machine unsupported until upstream patch. // TODO(<email-address>): Hoverboard + Time-machine unsupported until upstream patch.

4
Godeps/Godeps.json generated
View File

@ -148,11 +148,11 @@
}, },
{ {
"ImportPath": "github.com/google/safebrowsing", "ImportPath": "github.com/google/safebrowsing",
"Rev": "a8c029efb52bae66853e05241150ab338e98fbc7" "Rev": "f387afacc9e702b5ed3e90100d3375871e724c08"
}, },
{ {
"ImportPath": "github.com/google/safebrowsing/internal/safebrowsing_proto", "ImportPath": "github.com/google/safebrowsing/internal/safebrowsing_proto",
"Rev": "a8c029efb52bae66853e05241150ab338e98fbc7" "Rev": "f387afacc9e702b5ed3e90100d3375871e724c08"
}, },
{ {
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus", "ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",

View File

@ -23,6 +23,7 @@ import (
"github.com/letsencrypt/boulder/ra" "github.com/letsencrypt/boulder/ra"
rapb "github.com/letsencrypt/boulder/ra/proto" rapb "github.com/letsencrypt/boulder/ra/proto"
sapb "github.com/letsencrypt/boulder/sa/proto" sapb "github.com/letsencrypt/boulder/sa/proto"
vaPB "github.com/letsencrypt/boulder/va/proto"
) )
type config struct { type config struct {
@ -129,6 +130,8 @@ func main() {
cmd.FailOnError(err, "Unable to create VA client") cmd.FailOnError(err, "Unable to create VA client")
vac := bgrpc.NewValidationAuthorityGRPCClient(vaConn) vac := bgrpc.NewValidationAuthorityGRPCClient(vaConn)
caaClient := vaPB.NewCAAClient(vaConn)
caConn, err := bgrpc.ClientSetup(c.RA.CAService, tls, scope) caConn, err := bgrpc.ClientSetup(c.RA.CAService, tls, scope)
cmd.FailOnError(err, "Unable to create CA client") cmd.FailOnError(err, "Unable to create CA client")
// Build a CA client that is only capable of issuing certificates, not // Build a CA client that is only capable of issuing certificates, not
@ -174,6 +177,7 @@ func main() {
authorizationLifetime, authorizationLifetime,
pendingAuthorizationLifetime, pendingAuthorizationLifetime,
pubc, pubc,
caaClient,
c.RA.OrderLifetime.Duration, c.RA.OrderLifetime.Duration,
) )

View File

@ -13,6 +13,7 @@ import (
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc" bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/va" "github.com/letsencrypt/boulder/va"
vaPB "github.com/letsencrypt/boulder/va/proto"
) )
type config struct { type config struct {
@ -147,6 +148,8 @@ func main() {
cmd.FailOnError(err, "Unable to setup VA gRPC server") cmd.FailOnError(err, "Unable to setup VA gRPC server")
err = bgrpc.RegisterValidationAuthorityGRPCServer(grpcSrv, vai) err = bgrpc.RegisterValidationAuthorityGRPCServer(grpcSrv, vai)
cmd.FailOnError(err, "Unable to register VA gRPC server") cmd.FailOnError(err, "Unable to register VA gRPC server")
vaPB.RegisterCAAServer(grpcSrv, vai)
cmd.FailOnError(err, "Unable to register CAA gRPC server")
go func() { go func() {
err = grpcSrv.Serve(l) err = grpcSrv.Serve(l)
cmd.FailOnError(err, "VA gRPC service failed") cmd.FailOnError(err, "VA gRPC service failed")

View File

@ -4,9 +4,9 @@ package features
import "fmt" import "fmt"
const _FeatureFlag_name = "unusedAllowAccountDeactivationAllowKeyRolloverResubmitMissingSCTsOnlyUseAIAIssuerURLAllowTLS02ChallengesGenerateOCSPEarlyReusePendingAuthzCountCertificatesExactRandomDirectoryEntryIPv6FirstDirectoryMetaAllowRenewalFirstRL" const _FeatureFlag_name = "unusedAllowAccountDeactivationAllowKeyRolloverResubmitMissingSCTsOnlyUseAIAIssuerURLAllowTLS02ChallengesGenerateOCSPEarlyReusePendingAuthzCountCertificatesExactRandomDirectoryEntryIPv6FirstDirectoryMetaAllowRenewalFirstRLRecheckCAA"
var _FeatureFlag_index = [...]uint8{0, 6, 30, 46, 69, 84, 104, 121, 138, 160, 180, 189, 202, 221} var _FeatureFlag_index = [...]uint8{0, 6, 30, 46, 69, 84, 104, 121, 138, 160, 180, 189, 202, 221, 231}
func (i FeatureFlag) String() string { func (i FeatureFlag) String() string {
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) { if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {

View File

@ -26,6 +26,7 @@ const (
IPv6First IPv6First
DirectoryMeta DirectoryMeta
AllowRenewalFirstRL AllowRenewalFirstRL
RecheckCAA
) )
// List of features and their default value, protected by fMu // List of features and their default value, protected by fMu
@ -43,6 +44,7 @@ var features = map[FeatureFlag]bool{
IPv6First: false, IPv6First: false,
DirectoryMeta: false, DirectoryMeta: false,
AllowRenewalFirstRL: false, AllowRenewalFirstRL: false,
RecheckCAA: false,
} }
var fMu = new(sync.RWMutex) var fMu = new(sync.RWMutex)

212
ra/ra.go
View File

@ -26,7 +26,7 @@ import (
berrors "github.com/letsencrypt/boulder/errors" berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/goodkey"
"github.com/letsencrypt/boulder/grpc" bgrpc "github.com/letsencrypt/boulder/grpc"
blog "github.com/letsencrypt/boulder/log" blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/probs"
@ -36,6 +36,7 @@ import (
"github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/revocation"
sapb "github.com/letsencrypt/boulder/sa/proto" sapb "github.com/letsencrypt/boulder/sa/proto"
vaPB "github.com/letsencrypt/boulder/va/proto" vaPB "github.com/letsencrypt/boulder/va/proto"
grpc "google.golang.org/grpc"
) )
// Note: the issuanceExpvar must be a global. If it is a member of the RA, or // Note: the issuanceExpvar must be a global. If it is a member of the RA, or
@ -44,6 +45,14 @@ import (
// of exported var name:" error from the expvar package. // of exported var name:" error from the expvar package.
var issuanceExpvar = expvar.NewInt("lastIssuance") var issuanceExpvar = expvar.NewInt("lastIssuance")
type caaChecker interface {
IsCAAValid(
ctx context.Context,
in *vaPB.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vaPB.IsCAAValidResponse, error)
}
// RegistrationAuthorityImpl defines an RA. // RegistrationAuthorityImpl defines an RA.
// //
// NOTE: All of the fields in RegistrationAuthorityImpl need to be // NOTE: All of the fields in RegistrationAuthorityImpl need to be
@ -54,6 +63,7 @@ type RegistrationAuthorityImpl struct {
SA core.StorageAuthority SA core.StorageAuthority
PA core.PolicyAuthority PA core.PolicyAuthority
publisher core.Publisher publisher core.Publisher
caa caaChecker
stats metrics.Scope stats metrics.Scope
DNSClient bdns.DNSClient DNSClient bdns.DNSClient
@ -94,6 +104,7 @@ func NewRegistrationAuthorityImpl(
authorizationLifetime time.Duration, authorizationLifetime time.Duration,
pendingAuthorizationLifetime time.Duration, pendingAuthorizationLifetime time.Duration,
pubc core.Publisher, pubc core.Publisher,
caaClient caaChecker,
orderLifetime time.Duration, orderLifetime time.Duration,
) *RegistrationAuthorityImpl { ) *RegistrationAuthorityImpl {
ra := &RegistrationAuthorityImpl{ ra := &RegistrationAuthorityImpl{
@ -115,6 +126,7 @@ func NewRegistrationAuthorityImpl(
certsForDomainStats: stats.NewScope("RateLimit", "CertificatesForDomain"), certsForDomainStats: stats.NewScope("RateLimit", "CertificatesForDomain"),
totalCertsStats: stats.NewScope("RateLimit", "TotalCertificates"), totalCertsStats: stats.NewScope("RateLimit", "TotalCertificates"),
publisher: pubc, publisher: pubc,
caa: caaClient,
orderLifetime: orderLifetime, orderLifetime: orderLifetime,
} }
return ra return ra
@ -322,15 +334,15 @@ func (ra *RegistrationAuthorityImpl) checkRegistrationLimits(ctx context.Context
} }
// NewRegistration constructs a new Registration from a request. // NewRegistration constructs a new Registration from a request.
func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init core.Registration) (reg core.Registration, err error) { func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init core.Registration) (core.Registration, error) {
if err = ra.keyPolicy.GoodKey(init.Key.Key); err != nil { if err := ra.keyPolicy.GoodKey(init.Key.Key); err != nil {
return core.Registration{}, berrors.MalformedError("invalid public key: %s", err.Error()) return core.Registration{}, berrors.MalformedError("invalid public key: %s", err.Error())
} }
if err = ra.checkRegistrationLimits(ctx, init.InitialIP); err != nil { if err := ra.checkRegistrationLimits(ctx, init.InitialIP); err != nil {
return core.Registration{}, err return core.Registration{}, err
} }
reg = core.Registration{ reg := core.Registration{
Key: init.Key, Key: init.Key,
Status: core.StatusValid, Status: core.StatusValid,
} }
@ -340,13 +352,12 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init c
// MergeUpdate. But we need to fill it in for new registrations. // MergeUpdate. But we need to fill it in for new registrations.
reg.InitialIP = init.InitialIP reg.InitialIP = init.InitialIP
err = ra.validateContacts(ctx, reg.Contact) if err := ra.validateContacts(ctx, reg.Contact); err != nil {
if err != nil { return core.Registration{}, err
return
} }
// Store the authorization object, then return it // Store the authorization object, then return it
reg, err = ra.SA.NewRegistration(ctx, reg) reg, err := ra.SA.NewRegistration(ctx, reg)
if err != nil { if err != nil {
// berrors.InternalServerError since the user-data was validated before being // berrors.InternalServerError since the user-data was validated before being
// passed to the SA. // passed to the SA.
@ -354,7 +365,7 @@ func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init c
} }
ra.stats.Inc("NewRegistrations", 1) ra.stats.Inc("NewRegistrations", 1)
return return reg, nil
} }
func (ra *RegistrationAuthorityImpl) validateContacts(ctx context.Context, contacts *[]string) error { func (ra *RegistrationAuthorityImpl) validateContacts(ctx context.Context, contacts *[]string) error {
@ -426,7 +437,7 @@ func (ra *RegistrationAuthorityImpl) checkInvalidAuthorizationLimit(ctx context.
// The SA.CountInvalidAuthorizations method is not implemented on the wrapper // The SA.CountInvalidAuthorizations method is not implemented on the wrapper
// interface, because we want to move towards using gRPC interfaces more // interface, because we want to move towards using gRPC interfaces more
// directly. So we type-assert the wrapper to a gRPC-specific type. // directly. So we type-assert the wrapper to a gRPC-specific type.
saGRPC, ok := ra.SA.(*grpc.StorageAuthorityClientWrapper) saGRPC, ok := ra.SA.(*bgrpc.StorageAuthorityClientWrapper)
if !limit.Enabled() || !ok { if !limit.Enabled() || !ok {
return nil return nil
} }
@ -460,21 +471,21 @@ func (ra *RegistrationAuthorityImpl) checkInvalidAuthorizationLimit(ctx context.
// NewAuthorization constructs a new Authz from a request. Values (domains) in // NewAuthorization constructs a new Authz from a request. Values (domains) in
// request.Identifier will be lowercased before storage. // request.Identifier will be lowercased before storage.
func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, request core.Authorization, regID int64) (authz core.Authorization, err error) { func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, request core.Authorization, regID int64) (core.Authorization, error) {
identifier := request.Identifier identifier := request.Identifier
identifier.Value = strings.ToLower(identifier.Value) identifier.Value = strings.ToLower(identifier.Value)
// Check that the identifier is present and appropriate // Check that the identifier is present and appropriate
if err = ra.PA.WillingToIssue(identifier); err != nil { if err := ra.PA.WillingToIssue(identifier); err != nil {
return authz, err return core.Authorization{}, err
} }
if err = ra.checkPendingAuthorizationLimit(ctx, regID); err != nil { if err := ra.checkPendingAuthorizationLimit(ctx, regID); err != nil {
return authz, err return core.Authorization{}, err
} }
if err = ra.checkInvalidAuthorizationLimit(ctx, regID, identifier.Value); err != nil { if err := ra.checkInvalidAuthorizationLimit(ctx, regID, identifier.Value); err != nil {
return authz, err return core.Authorization{}, err
} }
if identifier.Type == core.IdentifierDNS { if identifier.Type == core.IdentifierDNS {
@ -482,10 +493,10 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
if err != nil { if err != nil {
outErr := berrors.InternalServerError("unable to determine if domain was safe") outErr := berrors.InternalServerError("unable to determine if domain was safe")
ra.log.Warning(fmt.Sprintf("%s: %s", outErr, err)) ra.log.Warning(fmt.Sprintf("%s: %s", outErr, err))
return authz, outErr return core.Authorization{}, outErr
} }
if !isSafeResp.GetIsSafe() { if !isSafeResp.GetIsSafe() {
return authz, berrors.UnauthorizedError( return core.Authorization{}, berrors.UnauthorizedError(
"%q was considered an unsafe domain by a third-party API", "%q was considered an unsafe domain by a third-party API",
identifier.Value, identifier.Value,
) )
@ -501,7 +512,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
identifier.Value, identifier.Value,
) )
ra.log.Warning(outErr.Error()) ra.log.Warning(outErr.Error())
return authz, outErr return core.Authorization{}, outErr
} }
if existingAuthz, ok := auths[identifier.Value]; ok { if existingAuthz, ok := auths[identifier.Value]; ok {
@ -515,7 +526,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
existingAuthz.ID, existingAuthz.ID,
) )
ra.log.Warning(fmt.Sprintf("%s: %s", outErr.Error(), existingAuthz.ID)) ra.log.Warning(fmt.Sprintf("%s: %s", outErr.Error(), existingAuthz.ID))
return authz, outErr return core.Authorization{}, outErr
} }
// The existing authorization must not expire within the next 24 hours for // The existing authorization must not expire within the next 24 hours for
@ -537,7 +548,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
ValidUntil: &nowishNano, ValidUntil: &nowishNano,
}) })
if err != nil && !berrors.Is(err, berrors.NotFound) { if err != nil && !berrors.Is(err, berrors.NotFound) {
return authz, berrors.InternalServerError( return core.Authorization{}, berrors.InternalServerError(
"unable to get pending authorization for regID: %d, identifier: %s: %s", "unable to get pending authorization for regID: %d, identifier: %s: %s",
regID, regID,
identifier.Value, identifier.Value,
@ -553,18 +564,14 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
expires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime) expires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime)
// Partially-filled object authz, err := ra.SA.NewPendingAuthorization(ctx, core.Authorization{
authz = core.Authorization{
Identifier: identifier, Identifier: identifier,
RegistrationID: regID, RegistrationID: regID,
Status: core.StatusPending, Status: core.StatusPending,
Combinations: combinations, Combinations: combinations,
Challenges: challenges, Challenges: challenges,
Expires: &expires, Expires: &expires,
} })
// Get a pending Auth first so we can get our ID back, then update with challenges
authz, err = ra.SA.NewPendingAuthorization(ctx, authz)
if err != nil { if err != nil {
// berrors.InternalServerError since the user-data was validated before being // berrors.InternalServerError since the user-data was validated before being
// passed to the SA. // passed to the SA.
@ -594,7 +601,7 @@ func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, reque
// * IsCA is false // * IsCA is false
// * ExtKeyUsage only contains ExtKeyUsageServerAuth & ExtKeyUsageClientAuth // * ExtKeyUsage only contains ExtKeyUsageServerAuth & ExtKeyUsageClientAuth
// * Subject only contains CommonName & Names // * Subject only contains CommonName & Names
func (ra *RegistrationAuthorityImpl) MatchesCSR(parsedCertificate *x509.Certificate, csr *x509.CertificateRequest) (err error) { func (ra *RegistrationAuthorityImpl) MatchesCSR(parsedCertificate *x509.Certificate, csr *x509.CertificateRequest) error {
// Check issued certificate matches what was expected from the CSR // Check issued certificate matches what was expected from the CSR
hostNames := make([]string, len(csr.DNSNames)) hostNames := make([]string, len(csr.DNSNames))
copy(hostNames, csr.DNSNames) copy(hostNames, csr.DNSNames)
@ -604,67 +611,66 @@ func (ra *RegistrationAuthorityImpl) MatchesCSR(parsedCertificate *x509.Certific
hostNames = core.UniqueLowerNames(hostNames) hostNames = core.UniqueLowerNames(hostNames)
if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) { if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) {
err = berrors.InternalServerError("generated certificate public key doesn't match CSR public key") return berrors.InternalServerError("generated certificate public key doesn't match CSR public key")
return
} }
if !ra.forceCNFromSAN && len(csr.Subject.CommonName) > 0 && if !ra.forceCNFromSAN && len(csr.Subject.CommonName) > 0 &&
parsedCertificate.Subject.CommonName != strings.ToLower(csr.Subject.CommonName) { parsedCertificate.Subject.CommonName != strings.ToLower(csr.Subject.CommonName) {
err = berrors.InternalServerError("generated certificate CommonName doesn't match CSR CommonName") return berrors.InternalServerError("generated certificate CommonName doesn't match CSR CommonName")
return
} }
// Sort both slices of names before comparison. // Sort both slices of names before comparison.
parsedNames := parsedCertificate.DNSNames parsedNames := parsedCertificate.DNSNames
sort.Strings(parsedNames) sort.Strings(parsedNames)
sort.Strings(hostNames) sort.Strings(hostNames)
if !reflect.DeepEqual(parsedNames, hostNames) { if !reflect.DeepEqual(parsedNames, hostNames) {
err = berrors.InternalServerError("generated certificate DNSNames don't match CSR DNSNames") return berrors.InternalServerError("generated certificate DNSNames don't match CSR DNSNames")
return
} }
if !reflect.DeepEqual(parsedCertificate.IPAddresses, csr.IPAddresses) { if !reflect.DeepEqual(parsedCertificate.IPAddresses, csr.IPAddresses) {
err = berrors.InternalServerError("generated certificate IPAddresses don't match CSR IPAddresses") return berrors.InternalServerError("generated certificate IPAddresses don't match CSR IPAddresses")
return
} }
if !reflect.DeepEqual(parsedCertificate.EmailAddresses, csr.EmailAddresses) { if !reflect.DeepEqual(parsedCertificate.EmailAddresses, csr.EmailAddresses) {
err = berrors.InternalServerError("generated certificate EmailAddresses don't match CSR EmailAddresses") return berrors.InternalServerError("generated certificate EmailAddresses don't match CSR EmailAddresses")
return
} }
if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 || if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 ||
len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 || len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 ||
len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 || len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 ||
len(parsedCertificate.Subject.PostalCode) > 0 { len(parsedCertificate.Subject.PostalCode) > 0 {
err = berrors.InternalServerError("generated certificate Subject contains fields other than CommonName, or SerialNumber") return berrors.InternalServerError("generated certificate Subject contains fields other than CommonName, or SerialNumber")
return
} }
now := ra.clk.Now() now := ra.clk.Now()
if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 { if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 {
err = berrors.InternalServerError("generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore)) return berrors.InternalServerError("generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore))
return
} }
if !parsedCertificate.BasicConstraintsValid { if !parsedCertificate.BasicConstraintsValid {
err = berrors.InternalServerError("generated certificate doesn't have basic constraints set") return berrors.InternalServerError("generated certificate doesn't have basic constraints set")
return
} }
if parsedCertificate.IsCA { if parsedCertificate.IsCA {
err = berrors.InternalServerError("generated certificate can sign other certificates") return berrors.InternalServerError("generated certificate can sign other certificates")
return
} }
if !reflect.DeepEqual(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) { if !reflect.DeepEqual(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
err = berrors.InternalServerError("generated certificate doesn't have correct key usage extensions") return berrors.InternalServerError("generated certificate doesn't have correct key usage extensions")
return
} }
return return nil
} }
// checkAuthorizations checks that each requested name has a valid authorization // checkAuthorizations checks that each requested name has a valid authorization
// that won't expire before the certificate expires. Returns an error otherwise. // that won't expire before the certificate expires. Returns an error otherwise.
func (ra *RegistrationAuthorityImpl) checkAuthorizations(ctx context.Context, names []string, registration *core.Registration) error { func (ra *RegistrationAuthorityImpl) checkAuthorizations(ctx context.Context, names []string, regID int64) error {
now := ra.clk.Now() now := ra.clk.Now()
var badNames []string var badNames, recheckNames []string
for i := range names { for i := range names {
names[i] = strings.ToLower(names[i]) names[i] = strings.ToLower(names[i])
} }
auths, err := ra.SA.GetValidAuthorizations(ctx, registration.ID, names, now) // Per Baseline Requirements, CAA must be checked within 8 hours of issuance.
// CAA is checked when an authorization is validated, so as long as that was
// less than 8 hours ago, we're fine. If it was more than 8 hours ago
// we have to recheck. Since we don't record the validation time for
// authorizations, we instead look at the expiration time and subtract out the
// expected authorization lifetime. Note: If we adjust the authorization
// lifetime in the future we will need to tweak this correspondingly so it
// works correctly during the switchover.
caaRecheckTime := now.Add(ra.authorizationLifetime).Add(-8 * time.Hour)
auths, err := ra.SA.GetValidAuthorizations(ctx, regID, names, now)
if err != nil { if err != nil {
return err return err
} }
@ -676,6 +682,14 @@ func (ra *RegistrationAuthorityImpl) checkAuthorizations(ctx context.Context, na
return berrors.InternalServerError("found an authorization with a nil Expires field: id %s", authz.ID) return berrors.InternalServerError("found an authorization with a nil Expires field: id %s", authz.ID)
} else if authz.Expires.Before(now) { } else if authz.Expires.Before(now) {
badNames = append(badNames, name) badNames = append(badNames, name)
} else if authz.Expires.Before(caaRecheckTime) {
recheckNames = append(recheckNames, name)
}
}
if features.Enabled(features.RecheckCAA) {
if err = ra.recheckCAA(ctx, recheckNames); err != nil {
return err
} }
} }
@ -685,11 +699,56 @@ func (ra *RegistrationAuthorityImpl) checkAuthorizations(ctx context.Context, na
strings.Join(badNames, ", "), strings.Join(badNames, ", "),
) )
} }
return nil
}
func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, names []string) error {
ra.stats.Inc("recheck_caa", 1)
ra.stats.Inc("recheck_caa_names", int64(len(names)))
wg := sync.WaitGroup{}
ch := make(chan *probs.ProblemDetails, len(names))
for _, name := range names {
wg.Add(1)
go func(name string) {
defer wg.Done()
resp, err := ra.caa.IsCAAValid(ctx, &vaPB.IsCAAValidRequest{
Domain: &name,
})
if err != nil {
ra.log.AuditErr(fmt.Sprintf("Rechecking CAA: %s", err))
ch <- probs.ServerInternal("Internal error rechecking CAA for " + name)
} else if resp.Problem != nil {
ch <- &probs.ProblemDetails{
Type: probs.ProblemType(*resp.Problem.ProblemType),
Detail: *resp.Problem.Detail,
}
}
}(name)
}
wg.Wait()
close(ch)
var fails []*probs.ProblemDetails
for err := range ch {
if err != nil {
fails = append(fails, err)
}
}
if len(fails) > 0 {
message := "Rechecking CAA: "
for i, pd := range fails {
if i > 0 {
message = message + ", "
}
message = message + pd.Detail
}
return berrors.ConnectionFailureError(message)
}
return nil return nil
} }
// NewCertificate requests the issuance of a certificate. // NewCertificate requests the issuance of a certificate.
func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req core.CertificateRequest, regID int64) (cert core.Certificate, err error) { func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req core.CertificateRequest, regID int64) (core.Certificate, error) {
emptyCert := core.Certificate{} emptyCert := core.Certificate{}
var logEventResult string var logEventResult string
@ -710,8 +769,7 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req cor
}() }()
if regID <= 0 { if regID <= 0 {
err = berrors.MalformedError("invalid registration ID: %d", regID) return emptyCert, berrors.MalformedError("invalid registration ID: %d", regID)
return emptyCert, err
} }
registration, err := ra.SA.GetRegistration(ctx, regID) registration, err := ra.SA.GetRegistration(ctx, regID)
@ -753,7 +811,7 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req cor
return emptyCert, err return emptyCert, err
} }
err = ra.checkAuthorizations(ctx, names, &registration) err = ra.checkAuthorizations(ctx, names, registration.ID)
if err != nil { if err != nil {
logEvent.Error = err.Error() logEvent.Error = err.Error()
return emptyCert, err return emptyCert, err
@ -767,7 +825,8 @@ func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req cor
Csr: csr.Raw, Csr: csr.Raw,
RegistrationID: &regID, RegistrationID: &regID,
} }
if cert, err = ra.CA.IssueCertificate(ctx, issueReq); err != nil { cert, err := ra.CA.IssueCertificate(ctx, issueReq)
if err != nil {
logEvent.Error = err.Error() logEvent.Error = err.Error()
return emptyCert, err return emptyCert, err
} }
@ -1097,17 +1156,15 @@ func mergeUpdate(r *core.Registration, input core.Registration) bool {
} }
// UpdateAuthorization updates an authorization with new values. // UpdateAuthorization updates an authorization with new values.
func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, base core.Authorization, challengeIndex int, response core.Challenge) (authz core.Authorization, err error) { func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, base core.Authorization, challengeIndex int, response core.Challenge) (core.Authorization, error) {
// Refuse to update expired authorizations // Refuse to update expired authorizations
if base.Expires == nil || base.Expires.Before(ra.clk.Now()) { if base.Expires == nil || base.Expires.Before(ra.clk.Now()) {
err = berrors.MalformedError("expired authorization") return core.Authorization{}, berrors.MalformedError("expired authorization")
return
} }
authz = base authz := base
if challengeIndex >= len(authz.Challenges) { if challengeIndex >= len(authz.Challenges) {
err = berrors.MalformedError("invalid challenge index '%d'", challengeIndex) return core.Authorization{}, berrors.MalformedError("invalid challenge index '%d'", challengeIndex)
return
} }
ch := &authz.Challenges[challengeIndex] ch := &authz.Challenges[challengeIndex]
@ -1129,26 +1186,23 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
// case and return early. // case and return early.
if ra.reuseValidAuthz && authz.Status == core.StatusValid { if ra.reuseValidAuthz && authz.Status == core.StatusValid {
ra.stats.Inc("ReusedValidAuthzChallenge", 1) ra.stats.Inc("ReusedValidAuthzChallenge", 1)
return return authz, nil
} }
// Look up the account key for this authorization // Look up the account key for this authorization
reg, err := ra.SA.GetRegistration(ctx, authz.RegistrationID) reg, err := ra.SA.GetRegistration(ctx, authz.RegistrationID)
if err != nil { if err != nil {
err = berrors.InternalServerError(err.Error()) return core.Authorization{}, berrors.InternalServerError(err.Error())
return
} }
// Recompute the key authorization field provided by the client and // Recompute the key authorization field provided by the client and
// check it against the value provided // check it against the value provided
expectedKeyAuthorization, err := ch.ExpectedKeyAuthorization(reg.Key) expectedKeyAuthorization, err := ch.ExpectedKeyAuthorization(reg.Key)
if err != nil { if err != nil {
err = berrors.InternalServerError("could not compute expected key authorization value") return core.Authorization{}, berrors.InternalServerError("could not compute expected key authorization value")
return
} }
if expectedKeyAuthorization != response.ProvidedKeyAuthorization { if expectedKeyAuthorization != response.ProvidedKeyAuthorization {
err = berrors.MalformedError("provided key authorization was incorrect") return core.Authorization{}, berrors.MalformedError("provided key authorization was incorrect")
return
} }
// Copy information over that the client is allowed to supply // Copy information over that the client is allowed to supply
@ -1156,16 +1210,14 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
// Double check before sending to VA // Double check before sending to VA
if cErr := ch.CheckConsistencyForValidation(); cErr != nil { if cErr := ch.CheckConsistencyForValidation(); cErr != nil {
err = berrors.MalformedError(cErr.Error()) return core.Authorization{}, berrors.MalformedError(cErr.Error())
return
} }
// Store the updated version // Store the updated version
if err = ra.SA.UpdatePendingAuthorization(ctx, authz); err != nil { if err = ra.SA.UpdatePendingAuthorization(ctx, authz); err != nil {
ra.log.Warning(fmt.Sprintf( ra.log.Warning(fmt.Sprintf(
"Error calling ra.SA.UpdatePendingAuthorization: %s\n", err.Error())) "Error calling ra.SA.UpdatePendingAuthorization: %s\n", err.Error()))
err = berrors.InternalServerError("could not update pending authorization") return core.Authorization{}, berrors.InternalServerError("could not update pending authorization")
return
} }
ra.stats.Inc("NewPendingAuthorizations", 1) ra.stats.Inc("NewPendingAuthorizations", 1)
@ -1206,7 +1258,7 @@ func (ra *RegistrationAuthorityImpl) UpdateAuthorization(ctx context.Context, ba
} }
}() }()
ra.stats.Inc("UpdatedPendingAuthorizations", 1) ra.stats.Inc("UpdatedPendingAuthorizations", 1)
return return authz, nil
} }
func revokeEvent(state, serial, cn string, names []string, revocationCode revocation.Reason) string { func revokeEvent(state, serial, cn string, names []string, revocationCode revocation.Reason) string {
@ -1221,9 +1273,9 @@ func revokeEvent(state, serial, cn string, names []string, revocationCode revoca
} }
// RevokeCertificateWithReg terminates trust in the certificate provided. // RevokeCertificateWithReg terminates trust in the certificate provided.
func (ra *RegistrationAuthorityImpl) RevokeCertificateWithReg(ctx context.Context, cert x509.Certificate, revocationCode revocation.Reason, regID int64) (err error) { func (ra *RegistrationAuthorityImpl) RevokeCertificateWithReg(ctx context.Context, cert x509.Certificate, revocationCode revocation.Reason, regID int64) error {
serialString := core.SerialToString(cert.SerialNumber) serialString := core.SerialToString(cert.SerialNumber)
err = ra.SA.MarkCertificateRevoked(ctx, serialString, revocationCode) err := ra.SA.MarkCertificateRevoked(ctx, serialString, revocationCode)
state := "Failure" state := "Failure"
defer func() { defer func() {
@ -1381,7 +1433,7 @@ func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.New
if err != nil { if err != nil {
return nil, err return nil, err
} }
authzPB, err := grpc.AuthzToPB(authz) authzPB, err := bgrpc.AuthzToPB(authz)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -14,14 +14,11 @@ import (
"net/url" "net/url"
"os" "os"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"github.com/weppos/publicsuffix-go/publicsuffix"
"golang.org/x/net/context"
jose "gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/bdns" "github.com/letsencrypt/boulder/bdns"
"github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
@ -40,6 +37,10 @@ import (
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
"github.com/letsencrypt/boulder/test/vars" "github.com/letsencrypt/boulder/test/vars"
vaPB "github.com/letsencrypt/boulder/va/proto" vaPB "github.com/letsencrypt/boulder/va/proto"
"github.com/weppos/publicsuffix-go/publicsuffix"
"golang.org/x/net/context"
"google.golang.org/grpc"
jose "gopkg.in/square/go-jose.v2"
) )
type DummyValidationAuthority struct { type DummyValidationAuthority struct {
@ -259,7 +260,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, *sa.SQLStorageAut
ra := NewRegistrationAuthorityImpl(fc, ra := NewRegistrationAuthorityImpl(fc,
log, log,
stats, stats,
1, testKeyPolicy, 0, true, false, 300*24*time.Hour, 7*24*time.Hour, nil, 0) 1, testKeyPolicy, 0, true, false, 300*24*time.Hour, 7*24*time.Hour, nil, noopCAA{}, 0)
ra.SA = ssa ra.SA = ssa
ra.VA = va ra.VA = va
ra.CA = ca ra.CA = ca
@ -1680,6 +1681,140 @@ func TestDeactivateRegistration(t *testing.T) {
test.AssertEquals(t, dbReg.Status, core.StatusDeactivated) test.AssertEquals(t, dbReg.Status, core.StatusDeactivated)
} }
// noopCAA implements caaChecker, always returning nil
type noopCAA struct{}
func (cr noopCAA) IsCAAValid(
ctx context.Context,
in *vaPB.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vaPB.IsCAAValidResponse, error) {
return &vaPB.IsCAAValidResponse{}, nil
}
// caaRecorder implements caaChecker, always returning nil, but recording the
// names it was called for.
type caaRecorder struct {
sync.Mutex
names map[string]bool
}
func (cr *caaRecorder) IsCAAValid(
ctx context.Context,
in *vaPB.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vaPB.IsCAAValidResponse, error) {
cr.Lock()
defer cr.Unlock()
cr.names[*in.Domain] = true
return &vaPB.IsCAAValidResponse{}, nil
}
// A mock SA that returns special authzs for testing rechecking of CAA (in
// TestRecheckCAADates below)
type mockSAWithRecentAndOlder struct {
recent, older time.Time
mocks.StorageAuthority
}
func (m *mockSAWithRecentAndOlder) GetValidAuthorizations(
ctx context.Context,
registrationID int64,
names []string,
now time.Time) (map[string]*core.Authorization, error) {
return map[string]*core.Authorization{
"recent.com": &core.Authorization{Expires: &m.recent},
"older.com": &core.Authorization{Expires: &m.older},
"older2.com": &core.Authorization{Expires: &m.older},
}, nil
}
// Test that the right set of domain names have their CAA rechecked, based on
// expiration time.
func TestRecheckCAADates(t *testing.T) {
_, _, ra, fc, cleanUp := initAuthorities(t)
defer cleanUp()
_ = features.Set(map[string]bool{"RecheckCAA": true})
defer features.Reset()
recorder := &caaRecorder{names: make(map[string]bool)}
ra.caa = recorder
ra.authorizationLifetime = 15 * time.Hour
ra.SA = &mockSAWithRecentAndOlder{
recent: fc.Now().Add(15 * time.Hour),
older: fc.Now().Add(5 * time.Hour),
}
names := []string{"recent.com", "older.com", "older2.com"}
err := ra.checkAuthorizations(context.Background(), names, 999)
if err != nil {
t.Errorf("expected nil err, got %s", err)
}
if recorder.names["recent.com"] {
t.Errorf("Rechecked CAA unnecessarily for recent.com")
}
if !recorder.names["older.com"] {
t.Errorf("Failed to recheck CAA for older.com %#v", recorder.names)
}
if !recorder.names["older2.com"] {
t.Errorf("Failed to recheck CAA for older.com")
}
}
type caaFailer struct{}
func (cf *caaFailer) IsCAAValid(
ctx context.Context,
in *vaPB.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vaPB.IsCAAValidResponse, error) {
name := *in.Domain
if name == "a.com" {
return nil, fmt.Errorf("Error checking CAA for a.com")
} else if name == "c.com" {
return nil, fmt.Errorf("Error checking CAA for c.com")
}
return &vaPB.IsCAAValidResponse{}, nil
}
func TestRecheckCAAEmpty(t *testing.T) {
_, _, ra, _, cleanUp := initAuthorities(t)
defer cleanUp()
_ = features.Set(map[string]bool{"RecheckCAA": true})
defer features.Reset()
err := ra.recheckCAA(context.Background(), nil)
if err != nil {
t.Errorf("expected nil err, got %s", err)
}
}
func TestRecheckCAASuccess(t *testing.T) {
_, _, ra, _, cleanUp := initAuthorities(t)
defer cleanUp()
_ = features.Set(map[string]bool{"RecheckCAA": true})
defer features.Reset()
names := []string{"a.com", "b.com", "c.com"}
err := ra.recheckCAA(context.Background(), names)
if err != nil {
t.Errorf("expected nil err, got %s", err)
}
}
func TestRecheckCAAFail(t *testing.T) {
_, _, ra, _, cleanUp := initAuthorities(t)
defer cleanUp()
_ = features.Set(map[string]bool{"RecheckCAA": true})
defer features.Reset()
names := []string{"a.com", "b.com", "c.com"}
ra.caa = &caaFailer{}
err := ra.recheckCAA(context.Background(), names)
if err == nil {
t.Errorf("expected err, got nil")
} else if !strings.Contains(err.Error(), "error rechecking CAA for a.com") {
t.Errorf("expected error to contain error for a.com, got %q", err)
} else if !strings.Contains(err.Error(), "error rechecking CAA for c.com") {
t.Errorf("expected error to contain error for c.com, got %q", err)
}
}
func TestNewOrder(t *testing.T) { func TestNewOrder(t *testing.T) {
// Only run under test/config-next config where 20170731115209_AddOrders.sql // Only run under test/config-next config where 20170731115209_AddOrders.sql
// has been applied // has been applied

View File

@ -26,15 +26,15 @@ func (re *RollbackError) Error() string {
return fmt.Sprintf("%s (also, while rolling back: %s)", re.Err, re.RollbackErr) return fmt.Sprintf("%s (also, while rolling back: %s)", re.Err, re.RollbackErr)
} }
// Rollback rolls back the provided transaction (if err is non-nil) and wraps // Rollback rolls back the provided transaction. If the rollback fails for any
// the error, if any, of the rollback into a RollbackError. // reason a `RollbackError` error is returned wrapping the original error. If no
// // rollback error occurs then the original error is returned.
// The err parameter must be non-nil.
//
// err = sa.Rollback(tx, err)
func Rollback(tx *gorp.Transaction, err error) error { func Rollback(tx *gorp.Transaction, err error) error {
if txErr := tx.Rollback(); txErr != nil {
return &RollbackError{ return &RollbackError{
Err: err, Err: err,
RollbackErr: tx.Rollback(), RollbackErr: txErr,
} }
} }
return err
}

36
sa/rollback_test.go Normal file
View File

@ -0,0 +1,36 @@
package sa
import (
"testing"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/test"
)
func TestRollback(t *testing.T) {
sa, _, cleanUp := initSA(t)
defer cleanUp()
tx, _ := sa.dbMap.Begin()
// Commit the transaction so that a subsequent Rollback will always fail.
_ = tx.Commit()
innerErr := berrors.NotFoundError("Gone, gone, gone")
result := Rollback(tx, innerErr)
// Since the tx.Rollback will fail we expect the result to be a wrapped error
test.AssertNotEquals(t, result, innerErr)
if rbErr, ok := result.(*RollbackError); !ok {
t.Fatal("Result was not a RollbackError")
test.AssertEquals(t, rbErr.Err, innerErr)
test.AssertNotNil(t, rbErr.RollbackErr, "RollbackErr was nil")
}
// Create a new transaction and don't commit it this time. The rollback should
// succeed.
tx, _ = sa.dbMap.Begin()
result = Rollback(tx, innerErr)
// We expect that the err is returned unwrapped.
test.AssertEquals(t, result, innerErr)
}

View File

@ -45,6 +45,7 @@
"AllowKeyRollover": true, "AllowKeyRollover": true,
"AllowTLS02Challenges": true, "AllowTLS02Challenges": true,
"CountCertificatesExact": true, "CountCertificatesExact": true,
"RecheckCAA": true,
"ReusePendingAuthz": true "ReusePendingAuthz": true
} }
}, },

View File

@ -149,8 +149,8 @@ func (ts *testSrv) serveTestResolver() {
dnsServer := &dns.Server{ dnsServer := &dns.Server{
Addr: "0.0.0.0:8053", Addr: "0.0.0.0:8053",
Net: "tcp", Net: "tcp",
ReadTimeout: time.Millisecond, ReadTimeout: time.Second,
WriteTimeout: time.Millisecond, WriteTimeout: time.Second,
} }
go func() { go func() {
err := dnsServer.ListenAndServe() err := dnsServer.ListenAndServe()

View File

@ -146,7 +146,7 @@ def test_gsb_lookups():
# The GSB test server tracks hits with a trailing / on the URL # The GSB test server tracks hits with a trailing / on the URL
hits = hits_map.get(hostname + "/", 0) hits = hits_map.get(hostname + "/", 0)
if hits != 1: if hits != 1:
raise("Expected %d Google Safe Browsing lookups for %s, found %d" % (1, url, actual)) raise Exception("Expected %d Google Safe Browsing lookups for %s, found %d" % (1, url, actual))
def test_ocsp(): def test_ocsp():
cert_file_pem = os.path.join(tempdir, "cert.pem") cert_file_pem = os.path.join(tempdir, "cert.pem")
@ -222,7 +222,7 @@ def test_expiration_mailer():
resp = urllib2.urlopen("http://localhost:9381/count?to=%s" % email_addr) resp = urllib2.urlopen("http://localhost:9381/count?to=%s" % email_addr)
mailcount = int(resp.read()) mailcount = int(resp.read())
if mailcount != 2: if mailcount != 2:
raise("\nExpiry mailer failed: expected 2 emails, got %d" % mailcount) raise Exception("\nExpiry mailer failed: expected 2 emails, got %d" % mailcount)
def test_revoke_by_account(): def test_revoke_by_account():
cert_file_pem = os.path.join(tempdir, "revokeme.pem") cert_file_pem = os.path.join(tempdir, "revokeme.pem")

225
va/caa.go Normal file
View File

@ -0,0 +1,225 @@
package va
import (
"fmt"
"strings"
"sync"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/probs"
vapb "github.com/letsencrypt/boulder/va/proto"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
func (va *ValidationAuthorityImpl) IsCAAValid(
ctx context.Context,
req *vapb.IsCAAValidRequest,
) (*vapb.IsCAAValidResponse, error) {
prob := va.checkCAA(ctx, core.AcmeIdentifier{
Type: core.IdentifierDNS,
Value: *req.Domain,
})
if prob != nil {
typ := string(prob.Type)
return &vapb.IsCAAValidResponse{
Problem: &corepb.ProblemDetails{
ProblemType: &typ,
Detail: &prob.Detail,
},
}, nil
}
return &vapb.IsCAAValidResponse{}, nil
}
func (va *ValidationAuthorityImpl) checkCAA(ctx context.Context, identifier core.AcmeIdentifier) *probs.ProblemDetails {
present, valid, err := va.checkCAARecords(ctx, identifier)
if err != nil {
return probs.ConnectionFailure(err.Error())
}
va.log.AuditInfo(fmt.Sprintf(
"Checked CAA records for %s, [Present: %t, Valid for issuance: %t]",
identifier.Value,
present,
valid,
))
if !valid {
return probs.ConnectionFailure(fmt.Sprintf("CAA record for %s prevents issuance", identifier.Value))
}
return nil
}
// CAASet consists of filtered CAA records
type CAASet struct {
Issue []*dns.CAA
Issuewild []*dns.CAA
Iodef []*dns.CAA
Unknown []*dns.CAA
}
// returns true if any CAA records have unknown tag properties and are flagged critical.
func (caaSet CAASet) criticalUnknown() bool {
if len(caaSet.Unknown) > 0 {
for _, caaRecord := range caaSet.Unknown {
// The critical flag is the bit with significance 128. However, many CAA
// record users have misinterpreted the RFC and concluded that the bit
// with significance 1 is the critical bit. This is sufficiently
// widespread that that bit must reasonably be considered an alias for
// the critical bit. The remaining bits are 0/ignore as proscribed by the
// RFC.
if (caaRecord.Flag & (128 | 1)) != 0 {
return true
}
}
}
return false
}
// Filter CAA records by property
func newCAASet(CAAs []*dns.CAA) *CAASet {
var filtered CAASet
for _, caaRecord := range CAAs {
switch caaRecord.Tag {
case "issue":
filtered.Issue = append(filtered.Issue, caaRecord)
case "issuewild":
filtered.Issuewild = append(filtered.Issuewild, caaRecord)
case "iodef":
filtered.Iodef = append(filtered.Iodef, caaRecord)
default:
filtered.Unknown = append(filtered.Unknown, caaRecord)
}
}
return &filtered
}
type caaResult struct {
records []*dns.CAA
err error
}
func parseResults(results []caaResult) (*CAASet, error) {
// Return first result
for _, res := range results {
if res.err != nil {
return nil, res.err
}
if len(res.records) > 0 {
return newCAASet(res.records), nil
}
}
return nil, nil
}
func (va *ValidationAuthorityImpl) parallelCAALookup(ctx context.Context, name string, lookuper func(context.Context, string) ([]*dns.CAA, error)) []caaResult {
labels := strings.Split(name, ".")
results := make([]caaResult, len(labels))
var wg sync.WaitGroup
for i := 0; i < len(labels); i++ {
// Start the concurrent DNS lookup.
wg.Add(1)
go func(name string, r *caaResult) {
r.records, r.err = lookuper(ctx, name)
wg.Done()
}(strings.Join(labels[i:], "."), &results[i])
}
wg.Wait()
return results
}
func (va *ValidationAuthorityImpl) getCAASet(ctx context.Context, hostname string) (*CAASet, error) {
hostname = strings.TrimRight(hostname, ".")
// See RFC 6844 "Certification Authority Processing" for pseudocode.
// Essentially: check CAA records for the FDQN to be issued, and all
// parent domains.
//
// The lookups are performed in parallel in order to avoid timing out
// the RPC call.
//
// We depend on our resolver to snap CNAME and DNAME records.
results := va.parallelCAALookup(ctx, hostname, va.dnsClient.LookupCAA)
return parseResults(results)
}
func (va *ValidationAuthorityImpl) checkCAARecords(ctx context.Context, identifier core.AcmeIdentifier) (present, valid bool, err error) {
hostname := strings.ToLower(identifier.Value)
caaSet, err := va.getCAASet(ctx, hostname)
if err != nil {
return false, false, err
}
present, valid = va.validateCAASet(caaSet)
return present, valid, nil
}
func (va *ValidationAuthorityImpl) validateCAASet(caaSet *CAASet) (present, valid bool) {
if caaSet == nil {
// No CAA records found, can issue
va.stats.Inc("CAA.None", 1)
return false, true
}
// Record stats on directives not currently processed.
if len(caaSet.Iodef) > 0 {
va.stats.Inc("CAA.WithIodef", 1)
}
if caaSet.criticalUnknown() {
// Contains unknown critical directives.
va.stats.Inc("CAA.UnknownCritical", 1)
return true, false
}
if len(caaSet.Unknown) > 0 {
va.stats.Inc("CAA.WithUnknownNoncritical", 1)
}
if len(caaSet.Issue) == 0 {
// Although CAA records exist, none of them pertain to issuance in this case.
// (e.g. there is only an issuewild directive, but we are checking for a
// non-wildcard identifier, or there is only an iodef or non-critical unknown
// directive.)
va.stats.Inc("CAA.NoneRelevant", 1)
return true, true
}
// There are CAA records pertaining to issuance in our case. Note that this
// includes the case of the unsatisfiable CAA record value ";", used to
// prevent issuance by any CA under any circumstance.
//
// Our CAA identity must be found in the chosen checkSet.
for _, caa := range caaSet.Issue {
if extractIssuerDomain(caa) == va.issuerDomain {
va.stats.Inc("CAA.Authorized", 1)
return true, true
}
}
// The list of authorized issuers is non-empty, but we are not in it. Fail.
va.stats.Inc("CAA.Unauthorized", 1)
return true, false
}
// Given a CAA record, assume that the Value is in the issue/issuewild format,
// that is, a domain name with zero or more additional key-value parameters.
// Returns the domain name, which may be "" (unsatisfiable).
func extractIssuerDomain(caa *dns.CAA) string {
v := caa.Value
v = strings.Trim(v, " \t") // Value can start and end with whitespace.
idx := strings.IndexByte(v, ';')
if idx < 0 {
return v // no parameters; domain only
}
// Currently, ignore parameters. Unfortunately, the RFC makes no statement on
// whether any parameters are critical. Treat unknown parameters as
// non-critical.
return strings.Trim(v[0:idx], " \t")
}

119
va/caa_test.go Normal file
View File

@ -0,0 +1,119 @@
package va
import (
"errors"
"testing"
"github.com/miekg/dns"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test"
)
func TestCAATimeout(t *testing.T) {
va, _ := setup(nil, 0)
err := va.checkCAA(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "caa-timeout.com"})
if err.Type != probs.ConnectionProblem {
t.Errorf("Expected timeout error type %s, got %s", probs.ConnectionProblem, err.Type)
}
expected := "DNS problem: query timed out looking up CAA for always.timeout"
if err.Detail != expected {
t.Errorf("checkCAA: got %#v, expected %#v", err.Detail, expected)
}
}
func TestCAAChecking(t *testing.T) {
type CAATest struct {
Domain string
Present bool
Valid bool
}
tests := []CAATest{
// Reserved
{"reserved.com", true, false},
// Critical
{"critical.com", true, false},
{"nx.critical.com", true, false},
// Good (absent)
{"absent.com", false, true},
{"example.co.uk", false, true},
// Good (present)
{"present.com", true, true},
{"present.servfail.com", true, true},
// Good (multiple critical, one matching)
{"multi-crit-present.com", true, true},
// Bad (unknown critical)
{"unknown-critical.com", true, false},
{"unknown-critical2.com", true, false},
// Good (unknown noncritical, no issue/issuewild records)
{"unknown-noncritical.com", true, true},
// Good (issue record with unknown parameters)
{"present-with-parameter.com", true, true},
// Bad (unsatisfiable issue record)
{"unsatisfiable.com", true, false},
}
va, _ := setup(nil, 0)
for _, caaTest := range tests {
present, valid, err := va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain})
if err != nil {
t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
}
if present != caaTest.Present {
t.Errorf("checkCAARecords presence mismatch for %s: got %t expected %t", caaTest.Domain, present, caaTest.Present)
}
if valid != caaTest.Valid {
t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
}
}
present, valid, err := va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
test.AssertError(t, err, "servfail.com")
test.Assert(t, !present, "Present should be false")
test.Assert(t, !valid, "Valid should be false")
_, _, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
if err == nil {
t.Errorf("Should have returned error on CAA lookup, but did not: %s", "servfail.com")
}
present, valid, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.present.com"})
test.AssertError(t, err, "servfail.present.com")
test.Assert(t, !present, "Present should be false")
test.Assert(t, !valid, "Valid should be false")
_, _, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.present.com"})
if err == nil {
t.Errorf("Should have returned error on CAA lookup, but did not: %s", "servfail.present.com")
}
}
func TestCAAFailure(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssni01Srv(t, chall)
defer hs.Close()
va, _ := setup(hs, 0)
_, prob := va.validateChallengeAndCAA(ctx, dnsi("reserved.com"), chall)
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
}
func TestParseResults(t *testing.T) {
r := []caaResult{}
s, err := parseResults(r)
test.Assert(t, s == nil, "set is not nil")
test.Assert(t, err == nil, "error is not nil")
test.AssertNotError(t, err, "no error should be returned")
r = []caaResult{{nil, errors.New("")}, {[]*dns.CAA{{Value: "test"}}, nil}}
s, err = parseResults(r)
test.Assert(t, s == nil, "set is not nil")
test.AssertEquals(t, err.Error(), "")
expected := dns.CAA{Value: "other-test"}
r = []caaResult{{[]*dns.CAA{&expected}, nil}, {[]*dns.CAA{{Value: "test"}}, nil}}
s, err = parseResults(r)
test.AssertEquals(t, len(s.Unknown), 1)
test.Assert(t, s.Unknown[0] == &expected, "Incorrect record returned")
test.AssertNotError(t, err, "no error should be returned")
}

View File

@ -9,6 +9,8 @@ It is generated from these files:
va/proto/va.proto va/proto/va.proto
It has these top-level messages: It has these top-level messages:
IsCAAValidRequest
IsCAAValidResponse
IsSafeDomainRequest IsSafeDomainRequest
IsDomainSafe IsDomainSafe
PerformValidationRequest PerformValidationRequest
@ -38,6 +40,41 @@ var _ = math.Inf
// proto package needs to be updated. // proto package needs to be updated.
const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package
type IsCAAValidRequest struct {
Domain *string `protobuf:"bytes,1,opt,name=domain" json:"domain,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *IsCAAValidRequest) Reset() { *m = IsCAAValidRequest{} }
func (m *IsCAAValidRequest) String() string { return proto1.CompactTextString(m) }
func (*IsCAAValidRequest) ProtoMessage() {}
func (*IsCAAValidRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *IsCAAValidRequest) GetDomain() string {
if m != nil && m.Domain != nil {
return *m.Domain
}
return ""
}
// If CAA is valid for the requested domain, the problem will be empty
type IsCAAValidResponse struct {
Problem *core.ProblemDetails `protobuf:"bytes,1,opt,name=problem" json:"problem,omitempty"`
XXX_unrecognized []byte `json:"-"`
}
func (m *IsCAAValidResponse) Reset() { *m = IsCAAValidResponse{} }
func (m *IsCAAValidResponse) String() string { return proto1.CompactTextString(m) }
func (*IsCAAValidResponse) ProtoMessage() {}
func (*IsCAAValidResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func (m *IsCAAValidResponse) GetProblem() *core.ProblemDetails {
if m != nil {
return m.Problem
}
return nil
}
type IsSafeDomainRequest struct { type IsSafeDomainRequest struct {
Domain *string `protobuf:"bytes,1,opt,name=domain" json:"domain,omitempty"` Domain *string `protobuf:"bytes,1,opt,name=domain" json:"domain,omitempty"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
@ -46,7 +83,7 @@ type IsSafeDomainRequest struct {
func (m *IsSafeDomainRequest) Reset() { *m = IsSafeDomainRequest{} } func (m *IsSafeDomainRequest) Reset() { *m = IsSafeDomainRequest{} }
func (m *IsSafeDomainRequest) String() string { return proto1.CompactTextString(m) } func (m *IsSafeDomainRequest) String() string { return proto1.CompactTextString(m) }
func (*IsSafeDomainRequest) ProtoMessage() {} func (*IsSafeDomainRequest) ProtoMessage() {}
func (*IsSafeDomainRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } func (*IsSafeDomainRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *IsSafeDomainRequest) GetDomain() string { func (m *IsSafeDomainRequest) GetDomain() string {
if m != nil && m.Domain != nil { if m != nil && m.Domain != nil {
@ -63,7 +100,7 @@ type IsDomainSafe struct {
func (m *IsDomainSafe) Reset() { *m = IsDomainSafe{} } func (m *IsDomainSafe) Reset() { *m = IsDomainSafe{} }
func (m *IsDomainSafe) String() string { return proto1.CompactTextString(m) } func (m *IsDomainSafe) String() string { return proto1.CompactTextString(m) }
func (*IsDomainSafe) ProtoMessage() {} func (*IsDomainSafe) ProtoMessage() {}
func (*IsDomainSafe) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } func (*IsDomainSafe) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
func (m *IsDomainSafe) GetIsSafe() bool { func (m *IsDomainSafe) GetIsSafe() bool {
if m != nil && m.IsSafe != nil { if m != nil && m.IsSafe != nil {
@ -82,7 +119,7 @@ type PerformValidationRequest struct {
func (m *PerformValidationRequest) Reset() { *m = PerformValidationRequest{} } func (m *PerformValidationRequest) Reset() { *m = PerformValidationRequest{} }
func (m *PerformValidationRequest) String() string { return proto1.CompactTextString(m) } func (m *PerformValidationRequest) String() string { return proto1.CompactTextString(m) }
func (*PerformValidationRequest) ProtoMessage() {} func (*PerformValidationRequest) ProtoMessage() {}
func (*PerformValidationRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } func (*PerformValidationRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
func (m *PerformValidationRequest) GetDomain() string { func (m *PerformValidationRequest) GetDomain() string {
if m != nil && m.Domain != nil { if m != nil && m.Domain != nil {
@ -114,7 +151,7 @@ type AuthzMeta struct {
func (m *AuthzMeta) Reset() { *m = AuthzMeta{} } func (m *AuthzMeta) Reset() { *m = AuthzMeta{} }
func (m *AuthzMeta) String() string { return proto1.CompactTextString(m) } func (m *AuthzMeta) String() string { return proto1.CompactTextString(m) }
func (*AuthzMeta) ProtoMessage() {} func (*AuthzMeta) ProtoMessage() {}
func (*AuthzMeta) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } func (*AuthzMeta) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
func (m *AuthzMeta) GetId() string { func (m *AuthzMeta) GetId() string {
if m != nil && m.Id != nil { if m != nil && m.Id != nil {
@ -139,7 +176,7 @@ type ValidationResult struct {
func (m *ValidationResult) Reset() { *m = ValidationResult{} } func (m *ValidationResult) Reset() { *m = ValidationResult{} }
func (m *ValidationResult) String() string { return proto1.CompactTextString(m) } func (m *ValidationResult) String() string { return proto1.CompactTextString(m) }
func (*ValidationResult) ProtoMessage() {} func (*ValidationResult) ProtoMessage() {}
func (*ValidationResult) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } func (*ValidationResult) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
func (m *ValidationResult) GetRecords() []*core.ValidationRecord { func (m *ValidationResult) GetRecords() []*core.ValidationRecord {
if m != nil { if m != nil {
@ -156,6 +193,8 @@ func (m *ValidationResult) GetProblems() *core.ProblemDetails {
} }
func init() { func init() {
proto1.RegisterType((*IsCAAValidRequest)(nil), "va.IsCAAValidRequest")
proto1.RegisterType((*IsCAAValidResponse)(nil), "va.IsCAAValidResponse")
proto1.RegisterType((*IsSafeDomainRequest)(nil), "va.IsSafeDomainRequest") proto1.RegisterType((*IsSafeDomainRequest)(nil), "va.IsSafeDomainRequest")
proto1.RegisterType((*IsDomainSafe)(nil), "va.IsDomainSafe") proto1.RegisterType((*IsDomainSafe)(nil), "va.IsDomainSafe")
proto1.RegisterType((*PerformValidationRequest)(nil), "va.PerformValidationRequest") proto1.RegisterType((*PerformValidationRequest)(nil), "va.PerformValidationRequest")
@ -268,28 +307,95 @@ var _VA_serviceDesc = grpc.ServiceDesc{
Metadata: "va/proto/va.proto", Metadata: "va/proto/va.proto",
} }
// Client API for CAA service
type CAAClient interface {
IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error)
}
type cAAClient struct {
cc *grpc.ClientConn
}
func NewCAAClient(cc *grpc.ClientConn) CAAClient {
return &cAAClient{cc}
}
func (c *cAAClient) IsCAAValid(ctx context.Context, in *IsCAAValidRequest, opts ...grpc.CallOption) (*IsCAAValidResponse, error) {
out := new(IsCAAValidResponse)
err := grpc.Invoke(ctx, "/va.CAA/IsCAAValid", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for CAA service
type CAAServer interface {
IsCAAValid(context.Context, *IsCAAValidRequest) (*IsCAAValidResponse, error)
}
func RegisterCAAServer(s *grpc.Server, srv CAAServer) {
s.RegisterService(&_CAA_serviceDesc, srv)
}
func _CAA_IsCAAValid_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(IsCAAValidRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CAAServer).IsCAAValid(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/va.CAA/IsCAAValid",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CAAServer).IsCAAValid(ctx, req.(*IsCAAValidRequest))
}
return interceptor(ctx, in, info, handler)
}
var _CAA_serviceDesc = grpc.ServiceDesc{
ServiceName: "va.CAA",
HandlerType: (*CAAServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "IsCAAValid",
Handler: _CAA_IsCAAValid_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "va/proto/va.proto",
}
func init() { proto1.RegisterFile("va/proto/va.proto", fileDescriptor0) } func init() { proto1.RegisterFile("va/proto/va.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 313 bytes of a gzipped FileDescriptorProto // 368 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0xc1, 0x4f, 0xc2, 0x30, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0xc1, 0x8e, 0xda, 0x30,
0x14, 0xc6, 0xd9, 0x08, 0x02, 0x0f, 0x51, 0xa8, 0xa8, 0x0b, 0x21, 0x86, 0x34, 0x11, 0x39, 0x8d, 0x14, 0x24, 0x89, 0x28, 0xf0, 0x28, 0x2d, 0xbc, 0x02, 0x8d, 0x22, 0x54, 0x21, 0x57, 0x50, 0x4e,
0x84, 0xab, 0x27, 0x94, 0xcb, 0x0e, 0x26, 0x44, 0x13, 0x0e, 0xde, 0x9e, 0xdb, 0x03, 0x96, 0x14, 0x41, 0xca, 0x15, 0xf5, 0x90, 0x92, 0x4b, 0x0e, 0x95, 0x50, 0x2b, 0x71, 0xd8, 0x9b, 0x37, 0x31,
0x8a, 0x6d, 0xd9, 0xc1, 0xbf, 0xc1, 0x3f, 0xda, 0xb4, 0x9b, 0x42, 0x54, 0x6e, 0x6f, 0xdf, 0xef, 0x10, 0x29, 0x60, 0xd6, 0x0e, 0x39, 0xec, 0x37, 0xec, 0x47, 0xaf, 0x6c, 0x67, 0x17, 0xc4, 0xc2,
0xdb, 0xeb, 0xd7, 0xaf, 0xd0, 0xce, 0x70, 0xb4, 0x55, 0xd2, 0xc8, 0x51, 0x86, 0xa1, 0x1b, 0x98, 0xed, 0xe5, 0xcd, 0x64, 0xde, 0x78, 0x34, 0xd0, 0x2b, 0xe9, 0xfc, 0x28, 0x78, 0xc1, 0xe7, 0x25,
0x9f, 0x61, 0xf7, 0x32, 0x96, 0x8a, 0x0a, 0x60, 0xc7, 0x1c, 0xf1, 0x5b, 0xb8, 0x88, 0xf4, 0x0b, 0xf5, 0xf5, 0x80, 0x76, 0x49, 0xbd, 0x41, 0xc2, 0x05, 0xab, 0x00, 0x35, 0x1a, 0x88, 0xfc, 0x84,
0x2e, 0x68, 0x2a, 0xd7, 0x98, 0x6e, 0x9e, 0xe9, 0x7d, 0x47, 0xda, 0xb0, 0x33, 0x38, 0x49, 0x9c, 0x5e, 0x2c, 0x97, 0x61, 0xb8, 0xa6, 0x79, 0x96, 0xfe, 0x63, 0x4f, 0x27, 0x26, 0x0b, 0xfc, 0x02,
0x10, 0x78, 0x7d, 0x6f, 0x58, 0xe7, 0x37, 0x70, 0x1a, 0xe9, 0xdc, 0x62, 0xcd, 0x96, 0xa7, 0xee, 0x9f, 0x52, 0xbe, 0xa7, 0xd9, 0xc1, 0xb5, 0xc6, 0xd6, 0xac, 0x45, 0x16, 0x80, 0x97, 0x24, 0x79,
0x37, 0xc7, 0x6b, 0x5c, 0x40, 0x30, 0x23, 0xb5, 0x90, 0x6a, 0x3d, 0x47, 0x91, 0x26, 0x68, 0x52, 0xe4, 0x07, 0xc9, 0x70, 0x02, 0x8d, 0xa3, 0xe0, 0x8f, 0x39, 0xdb, 0x6b, 0x5a, 0x3b, 0xe8, 0xfb,
0x79, 0x6c, 0x17, 0xe3, 0x50, 0x8f, 0x57, 0x28, 0x04, 0x6d, 0x96, 0x14, 0xf8, 0x7d, 0x6f, 0xd8, 0x5a, 0x78, 0x65, 0x96, 0x11, 0x2b, 0x68, 0x96, 0x4b, 0x32, 0x81, 0x6f, 0xb1, 0xfc, 0x4f, 0x37,
0x18, 0x9f, 0x87, 0x2e, 0xd2, 0xe3, 0xb7, 0xcc, 0x7a, 0x50, 0xc1, 0x9d, 0x59, 0x7d, 0x04, 0x65, 0x2c, 0xd2, 0x92, 0xf7, 0x6e, 0xfc, 0x80, 0xcf, 0xb1, 0x34, 0x14, 0x45, 0x56, 0x78, 0xa6, 0x7f,
0xc7, 0x9b, 0x61, 0x86, 0xe1, 0xc4, 0x0a, 0x4f, 0x64, 0x90, 0x0f, 0xa0, 0xfe, 0xf3, 0xc1, 0x00, 0xd3, 0x78, 0x93, 0xe4, 0xe0, 0xae, 0x98, 0xd8, 0x70, 0xb1, 0xd7, 0x2e, 0x68, 0x91, 0xf1, 0x7b,
0xfc, 0x34, 0x29, 0x56, 0x37, 0xa1, 0xa2, 0x68, 0x19, 0x4d, 0xdd, 0xda, 0x32, 0x8f, 0xa1, 0x75, 0x5a, 0x48, 0xa0, 0x95, 0xec, 0x68, 0x9e, 0xb3, 0xc3, 0x96, 0xb9, 0xb6, 0xf6, 0xf6, 0xd5, 0x78,
0x18, 0x47, 0xef, 0x84, 0x61, 0x77, 0x50, 0x55, 0x14, 0x4b, 0x95, 0xe8, 0xc0, 0xeb, 0x97, 0x87, 0x5b, 0xbe, 0xad, 0x71, 0x04, 0x75, 0x7a, 0x2a, 0x76, 0xcf, 0xae, 0xa3, 0xf1, 0x8e, 0x5f, 0x52,
0x8d, 0xf1, 0x55, 0x7e, 0xf6, 0xa1, 0xd1, 0x62, 0x36, 0x80, 0xda, 0x56, 0xc9, 0x37, 0x41, 0x6b, 0x3f, 0x54, 0x8b, 0xbf, 0xac, 0xa0, 0x64, 0x0a, 0xad, 0xf7, 0x0f, 0x04, 0xb0, 0xb3, 0xb4, 0x92,
0x5d, 0xa4, 0xec, 0xe4, 0xce, 0x59, 0xae, 0x4e, 0xc9, 0x60, 0x2a, 0xf4, 0xf8, 0xd3, 0x03, 0x7f, 0xee, 0x40, 0x5d, 0xb0, 0x6d, 0x1c, 0x69, 0x59, 0x87, 0x24, 0xd0, 0xbd, 0xb4, 0x23, 0x4f, 0x79,
0x3e, 0x61, 0xf7, 0xb6, 0xa1, 0x7d, 0x91, 0xec, 0xda, 0x46, 0xfe, 0xa7, 0xda, 0x6e, 0x2b, 0x07, 0x81, 0xbf, 0xa0, 0x21, 0x58, 0xc2, 0x45, 0x2a, 0x5d, 0x6b, 0xec, 0xcc, 0xda, 0xc1, 0xd0, 0xdc,
0xfb, 0x32, 0x79, 0x89, 0x45, 0xd0, 0xfe, 0x53, 0x1f, 0xeb, 0x59, 0xe3, 0xb1, 0x56, 0xbb, 0x1d, 0xbe, 0x24, 0x2a, 0x18, 0xa7, 0xd0, 0xac, 0x02, 0x94, 0x95, 0xcb, 0x9b, 0x09, 0x06, 0x2f, 0x16,
0x4b, 0x7f, 0xdf, 0x8e, 0x97, 0x1e, 0xaa, 0xaf, 0x15, 0xf7, 0xb2, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xd8, 0xeb, 0x10, 0x17, 0x2a, 0xa1, 0x73, 0x90, 0xf8, 0x5d, 0x59, 0xbe, 0x11, 0xad, 0xd7, 0x35,
0xff, 0x7d, 0xf6, 0x5c, 0x13, 0x08, 0x02, 0x00, 0x00, 0xc0, 0x39, 0x4c, 0x52, 0xc3, 0x18, 0x7a, 0x1f, 0xe2, 0xc3, 0x91, 0x22, 0xde, 0x4b, 0xd5, 0xeb,
0x2b, 0xf4, 0xfa, 0x75, 0xa4, 0x16, 0x44, 0xe0, 0x2c, 0xc3, 0x10, 0x7f, 0x03, 0x9c, 0x4b, 0x81,
0x03, 0x73, 0xf3, 0xaa, 0x49, 0xde, 0xf0, 0x7a, 0x6d, 0xba, 0x43, 0x6a, 0x7f, 0x1a, 0x0f, 0x75,
0xdd, 0xc0, 0xd7, 0x00, 0x00, 0x00, 0xff, 0xff, 0x8a, 0x44, 0xc0, 0x1e, 0xb0, 0x02, 0x00, 0x00,
} }

View File

@ -10,6 +10,19 @@ service VA {
rpc PerformValidation(PerformValidationRequest) returns (ValidationResult) {} rpc PerformValidation(PerformValidationRequest) returns (ValidationResult) {}
} }
service CAA {
rpc IsCAAValid(IsCAAValidRequest) returns (IsCAAValidResponse) {}
}
message IsCAAValidRequest {
optional string domain = 1;
}
// If CAA is valid for the requested domain, the problem will be empty
message IsCAAValidResponse {
optional core.ProblemDetails problem = 1;
}
message IsSafeDomainRequest { message IsSafeDomainRequest {
optional string domain = 1; optional string domain = 1;
} }

192
va/va.go
View File

@ -17,12 +17,10 @@ import (
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -761,23 +759,6 @@ func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, identifier
return nil, probs.Unauthorized("Correct value not found for DNS challenge") return nil, probs.Unauthorized("Correct value not found for DNS challenge")
} }
func (va *ValidationAuthorityImpl) checkCAA(ctx context.Context, identifier core.AcmeIdentifier) *probs.ProblemDetails {
present, valid, err := va.checkCAARecords(ctx, identifier)
if err != nil {
return probs.ConnectionFailure(err.Error())
}
va.log.AuditInfo(fmt.Sprintf(
"Checked CAA records for %s, [Present: %t, Valid for issuance: %t]",
identifier.Value,
present,
valid,
))
if !valid {
return probs.ConnectionFailure(fmt.Sprintf("CAA record for %s prevents issuance", identifier.Value))
}
return nil
}
func (va *ValidationAuthorityImpl) validateChallengeAndCAA(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) { func (va *ValidationAuthorityImpl) validateChallengeAndCAA(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) {
ch := make(chan *probs.ProblemDetails, 1) ch := make(chan *probs.ProblemDetails, 1)
go func() { go func() {
@ -939,176 +920,3 @@ func (va *ValidationAuthorityImpl) PerformValidation(ctx context.Context, domain
return records, prob return records, prob
} }
// CAASet consists of filtered CAA records
type CAASet struct {
Issue []*dns.CAA
Issuewild []*dns.CAA
Iodef []*dns.CAA
Unknown []*dns.CAA
}
// returns true if any CAA records have unknown tag properties and are flagged critical.
func (caaSet CAASet) criticalUnknown() bool {
if len(caaSet.Unknown) > 0 {
for _, caaRecord := range caaSet.Unknown {
// The critical flag is the bit with significance 128. However, many CAA
// record users have misinterpreted the RFC and concluded that the bit
// with significance 1 is the critical bit. This is sufficiently
// widespread that that bit must reasonably be considered an alias for
// the critical bit. The remaining bits are 0/ignore as proscribed by the
// RFC.
if (caaRecord.Flag & (128 | 1)) != 0 {
return true
}
}
}
return false
}
// Filter CAA records by property
func newCAASet(CAAs []*dns.CAA) *CAASet {
var filtered CAASet
for _, caaRecord := range CAAs {
switch caaRecord.Tag {
case "issue":
filtered.Issue = append(filtered.Issue, caaRecord)
case "issuewild":
filtered.Issuewild = append(filtered.Issuewild, caaRecord)
case "iodef":
filtered.Iodef = append(filtered.Iodef, caaRecord)
default:
filtered.Unknown = append(filtered.Unknown, caaRecord)
}
}
return &filtered
}
type caaResult struct {
records []*dns.CAA
err error
}
func parseResults(results []caaResult) (*CAASet, error) {
// Return first result
for _, res := range results {
if res.err != nil {
return nil, res.err
}
if len(res.records) > 0 {
return newCAASet(res.records), nil
}
}
return nil, nil
}
func (va *ValidationAuthorityImpl) parallelCAALookup(ctx context.Context, name string, lookuper func(context.Context, string) ([]*dns.CAA, error)) []caaResult {
labels := strings.Split(name, ".")
results := make([]caaResult, len(labels))
var wg sync.WaitGroup
for i := 0; i < len(labels); i++ {
// Start the concurrent DNS lookup.
wg.Add(1)
go func(name string, r *caaResult) {
r.records, r.err = lookuper(ctx, name)
wg.Done()
}(strings.Join(labels[i:], "."), &results[i])
}
wg.Wait()
return results
}
func (va *ValidationAuthorityImpl) getCAASet(ctx context.Context, hostname string) (*CAASet, error) {
hostname = strings.TrimRight(hostname, ".")
// See RFC 6844 "Certification Authority Processing" for pseudocode.
// Essentially: check CAA records for the FDQN to be issued, and all
// parent domains.
//
// The lookups are performed in parallel in order to avoid timing out
// the RPC call.
//
// We depend on our resolver to snap CNAME and DNAME records.
results := va.parallelCAALookup(ctx, hostname, va.dnsClient.LookupCAA)
return parseResults(results)
}
func (va *ValidationAuthorityImpl) checkCAARecords(ctx context.Context, identifier core.AcmeIdentifier) (present, valid bool, err error) {
hostname := strings.ToLower(identifier.Value)
caaSet, err := va.getCAASet(ctx, hostname)
if err != nil {
return false, false, err
}
present, valid = va.validateCAASet(caaSet)
return present, valid, nil
}
func (va *ValidationAuthorityImpl) validateCAASet(caaSet *CAASet) (present, valid bool) {
if caaSet == nil {
// No CAA records found, can issue
va.stats.Inc("CAA.None", 1)
return false, true
}
// Record stats on directives not currently processed.
if len(caaSet.Iodef) > 0 {
va.stats.Inc("CAA.WithIodef", 1)
}
if caaSet.criticalUnknown() {
// Contains unknown critical directives.
va.stats.Inc("CAA.UnknownCritical", 1)
return true, false
}
if len(caaSet.Unknown) > 0 {
va.stats.Inc("CAA.WithUnknownNoncritical", 1)
}
if len(caaSet.Issue) == 0 {
// Although CAA records exist, none of them pertain to issuance in this case.
// (e.g. there is only an issuewild directive, but we are checking for a
// non-wildcard identifier, or there is only an iodef or non-critical unknown
// directive.)
va.stats.Inc("CAA.NoneRelevant", 1)
return true, true
}
// There are CAA records pertaining to issuance in our case. Note that this
// includes the case of the unsatisfiable CAA record value ";", used to
// prevent issuance by any CA under any circumstance.
//
// Our CAA identity must be found in the chosen checkSet.
for _, caa := range caaSet.Issue {
if extractIssuerDomain(caa) == va.issuerDomain {
va.stats.Inc("CAA.Authorized", 1)
return true, true
}
}
// The list of authorized issuers is non-empty, but we are not in it. Fail.
va.stats.Inc("CAA.Unauthorized", 1)
return true, false
}
// Given a CAA record, assume that the Value is in the issue/issuewild format,
// that is, a domain name with zero or more additional key-value parameters.
// Returns the domain name, which may be "" (unsatisfiable).
func extractIssuerDomain(caa *dns.CAA) string {
v := caa.Value
v = strings.Trim(v, " \t") // Value can start and end with whitespace.
idx := strings.IndexByte(v, ';')
if idx < 0 {
return v // no parameters; domain only
}
// Currently, ignore parameters. Unfortunately, the RFC makes no statement on
// whether any parameters are critical. Treat unknown parameters as
// non-critical.
return strings.Trim(v[0:idx], " \t")
}

View File

@ -9,7 +9,6 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"math/big" "math/big"
mrand "math/rand" mrand "math/rand"
@ -28,7 +27,6 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"github.com/miekg/dns"
"golang.org/x/net/context" "golang.org/x/net/context"
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
@ -68,7 +66,10 @@ var TheKey = rsa.PrivateKey{
var accountKey = &jose.JSONWebKey{Key: TheKey.Public()} var accountKey = &jose.JSONWebKey{Key: TheKey.Public()}
var ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "localhost"} // Return an ACME DNS identifier for the given hostname
func dnsi(hostname string) core.AcmeIdentifier {
return core.AcmeIdentifier{Type: core.IdentifierDNS, Value: hostname}
}
var ctx = context.Background() var ctx = context.Background()
@ -238,7 +239,7 @@ func TestHTTPBadPort(t *testing.T) {
badPort := 40000 + mrand.Intn(25000) badPort := 40000 + mrand.Intn(25000)
va.httpPort = badPort va.httpPort = badPort
_, prob := va.validateHTTP01(ctx, ident, chall) _, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("Server's down; expected refusal. Where did we connect?") t.Fatalf("Server's down; expected refusal. Where did we connect?")
} }
@ -266,7 +267,7 @@ func TestHTTP(t *testing.T) {
log.Clear() log.Clear()
t.Logf("Trying to validate: %+v\n", chall) t.Logf("Trying to validate: %+v\n", chall)
_, prob := va.validateHTTP01(ctx, ident, chall) _, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Errorf("Unexpected failure in HTTP validation: %s", prob) t.Errorf("Unexpected failure in HTTP validation: %s", prob)
} }
@ -274,7 +275,7 @@ func TestHTTP(t *testing.T) {
log.Clear() log.Clear()
setChallengeToken(&chall, path404) setChallengeToken(&chall, path404)
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("Should have found a 404 for the challenge.") t.Fatalf("Should have found a 404 for the challenge.")
} }
@ -285,7 +286,7 @@ func TestHTTP(t *testing.T) {
setChallengeToken(&chall, pathWrongToken) setChallengeToken(&chall, pathWrongToken)
// The "wrong token" will actually be the expectedToken. It's wrong // The "wrong token" will actually be the expectedToken. It's wrong
// because it doesn't match pathWrongToken. // because it doesn't match pathWrongToken.
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("Should have found the wrong token value.") t.Fatalf("Should have found the wrong token value.")
} }
@ -294,7 +295,7 @@ func TestHTTP(t *testing.T) {
log.Clear() log.Clear()
setChallengeToken(&chall, pathMoved) setChallengeToken(&chall, pathMoved)
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Fatalf("Failed to follow 301 redirect") t.Fatalf("Failed to follow 301 redirect")
} }
@ -302,7 +303,7 @@ func TestHTTP(t *testing.T) {
log.Clear() log.Clear()
setChallengeToken(&chall, pathFound) setChallengeToken(&chall, pathFound)
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Fatalf("Failed to follow 302 redirect") t.Fatalf("Failed to follow 302 redirect")
} }
@ -334,7 +335,7 @@ func TestHTTPTimeout(t *testing.T) {
setChallengeToken(&chall, pathWaitLong) setChallengeToken(&chall, pathWaitLong)
started := time.Now() started := time.Now()
_, prob := va.validateHTTP01(ctx, ident, chall) _, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
took := time.Since(started) took := time.Since(started)
// Check that the HTTP connection times out after 5 seconds and doesn't block for 10 seconds // Check that the HTTP connection times out after 5 seconds and doesn't block for 10 seconds
test.Assert(t, (took > (time.Second * 5)), "HTTP timed out before 5 seconds") test.Assert(t, (took > (time.Second * 5)), "HTTP timed out before 5 seconds")
@ -360,7 +361,7 @@ func TestHTTPRedirectLookup(t *testing.T) {
va, log := setup(hs, 0) va, log := setup(hs, 0)
setChallengeToken(&chall, pathMoved) setChallengeToken(&chall, pathMoved)
_, prob := va.validateHTTP01(ctx, ident, chall) _, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Fatalf("Unexpected failure in redirect (%s): %s", pathMoved, prob) t.Fatalf("Unexpected failure in redirect (%s): %s", pathMoved, prob)
} }
@ -369,7 +370,7 @@ func TestHTTPRedirectLookup(t *testing.T) {
log.Clear() log.Clear()
setChallengeToken(&chall, pathFound) setChallengeToken(&chall, pathFound)
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Fatalf("Unexpected failure in redirect (%s): %s", pathFound, prob) t.Fatalf("Unexpected failure in redirect (%s): %s", pathFound, prob)
} }
@ -379,14 +380,14 @@ func TestHTTPRedirectLookup(t *testing.T) {
log.Clear() log.Clear()
setChallengeToken(&chall, pathReLookupInvalid) setChallengeToken(&chall, pathReLookupInvalid)
_, err := va.validateHTTP01(ctx, ident, chall) _, err := va.validateHTTP01(ctx, dnsi("localhost"), chall)
test.AssertError(t, err, chall.Token) test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1) test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`No valid IP addresses found for invalid.invalid`)), 1) test.AssertEquals(t, len(log.GetAllMatching(`No valid IP addresses found for invalid.invalid`)), 1)
log.Clear() log.Clear()
setChallengeToken(&chall, pathReLookup) setChallengeToken(&chall, pathReLookup)
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Fatalf("Unexpected error in redirect (%s): %s", pathReLookup, prob) t.Fatalf("Unexpected error in redirect (%s): %s", pathReLookup, prob)
} }
@ -396,7 +397,7 @@ func TestHTTPRedirectLookup(t *testing.T) {
log.Clear() log.Clear()
setChallengeToken(&chall, pathRedirectPort) setChallengeToken(&chall, pathRedirectPort)
_, err = va.validateHTTP01(ctx, ident, chall) _, err = va.validateHTTP01(ctx, dnsi("localhost"), chall)
test.AssertError(t, err, chall.Token) test.AssertError(t, err, chall.Token)
test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/port-redirect" to ".*other.valid:8080/path"`)), 1) test.AssertEquals(t, len(log.GetAllMatching(`redirect from ".*/port-redirect" to ".*other.valid:8080/path"`)), 1)
test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1) test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost \[using 127.0.0.1\]: \[127.0.0.1\]`)), 1)
@ -407,7 +408,7 @@ func TestHTTPRedirectLookup(t *testing.T) {
// is referencing the redirected to host, instead of the original host. // is referencing the redirected to host, instead of the original host.
log.Clear() log.Clear()
setChallengeToken(&chall, pathRedirectToFailingURL) setChallengeToken(&chall, pathRedirectToFailingURL)
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
test.AssertNotNil(t, prob, "Problem Details should not be nil") test.AssertNotNil(t, prob, "Problem Details should not be nil")
test.AssertEquals(t, prob.Detail, "Fetching http://other.valid/500: Connection refused") test.AssertEquals(t, prob.Detail, "Fetching http://other.valid/500: Connection refused")
} }
@ -420,7 +421,7 @@ func TestHTTPRedirectLoop(t *testing.T) {
defer hs.Close() defer hs.Close()
va, _ := setup(hs, 0) va, _ := setup(hs, 0)
_, prob := va.validateHTTP01(ctx, ident, chall) _, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("Challenge should have failed for %s", chall.Token) t.Fatalf("Challenge should have failed for %s", chall.Token)
} }
@ -436,13 +437,13 @@ func TestHTTPRedirectUserAgent(t *testing.T) {
va.userAgent = rejectUserAgent va.userAgent = rejectUserAgent
setChallengeToken(&chall, pathMoved) setChallengeToken(&chall, pathMoved)
_, prob := va.validateHTTP01(ctx, ident, chall) _, prob := va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathMoved) t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathMoved)
} }
setChallengeToken(&chall, pathFound) setChallengeToken(&chall, pathFound)
_, prob = va.validateHTTP01(ctx, ident, chall) _, prob = va.validateHTTP01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathFound) t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathFound)
} }
@ -471,7 +472,7 @@ func TestTLSSNI01(t *testing.T) {
va, log := setup(hs, 0) va, log := setup(hs, 0)
_, prob := va.validateTLSSNI01(ctx, ident, chall) _, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Fatalf("Unexpected failure in validate TLS-SNI-01: %s", prob) t.Fatalf("Unexpected failure in validate TLS-SNI-01: %s", prob)
} }
@ -505,7 +506,7 @@ func TestTLSSNI01(t *testing.T) {
log.Clear() log.Clear()
started := time.Now() started := time.Now()
_, prob = va.validateTLSSNI01(ctx, ident, chall) _, prob = va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
took := time.Since(started) took := time.Since(started)
if prob == nil { if prob == nil {
t.Fatalf("Validation should've failed") t.Fatalf("Validation should've failed")
@ -518,7 +519,7 @@ func TestTLSSNI01(t *testing.T) {
// Take down validation server and check that validation fails. // Take down validation server and check that validation fails.
hs.Close() hs.Close()
_, err := va.validateTLSSNI01(ctx, ident, chall) _, err := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
if err == nil { if err == nil {
t.Fatalf("Server's down; expected refusal. Where did we connect?") t.Fatalf("Server's down; expected refusal. Where did we connect?")
} }
@ -528,7 +529,7 @@ func TestTLSSNI01(t *testing.T) {
va.tlsPort = getPort(httpOnly) va.tlsPort = getPort(httpOnly)
log.Clear() log.Clear()
_, err = va.validateTLSSNI01(ctx, ident, chall) _, err = va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
test.AssertError(t, err, "TLS-SNI-01 validation passed when talking to a HTTP-only server") test.AssertError(t, err, "TLS-SNI-01 validation passed when talking to a HTTP-only server")
test.Assert(t, strings.HasSuffix( test.Assert(t, strings.HasSuffix(
err.Error(), err.Error(),
@ -543,7 +544,7 @@ func TestTLSSNI02(t *testing.T) {
va, log := setup(hs, 0) va, log := setup(hs, 0)
_, prob := va.validateTLSSNI02(ctx, ident, chall) _, prob := va.validateTLSSNI02(ctx, dnsi("localhost"), chall)
if prob != nil { if prob != nil {
t.Fatalf("Unexpected failure in validate TLS-SNI-02: %s", prob) t.Fatalf("Unexpected failure in validate TLS-SNI-02: %s", prob)
} }
@ -577,7 +578,7 @@ func TestTLSSNI02(t *testing.T) {
log.Clear() log.Clear()
started := time.Now() started := time.Now()
_, prob = va.validateTLSSNI02(ctx, ident, chall) _, prob = va.validateTLSSNI02(ctx, dnsi("localhost"), chall)
took := time.Since(started) took := time.Since(started)
if prob == nil { if prob == nil {
t.Fatalf("Validation should have failed") t.Fatalf("Validation should have failed")
@ -590,7 +591,7 @@ func TestTLSSNI02(t *testing.T) {
// Take down validation server and check that validation fails. // Take down validation server and check that validation fails.
hs.Close() hs.Close()
_, err := va.validateTLSSNI02(ctx, ident, chall) _, err := va.validateTLSSNI02(ctx, dnsi("localhost"), chall)
if err == nil { if err == nil {
t.Fatalf("Server's down; expected refusal. Where did we connect?") t.Fatalf("Server's down; expected refusal. Where did we connect?")
} }
@ -601,7 +602,7 @@ func TestTLSSNI02(t *testing.T) {
va.tlsPort = getPort(httpOnly) va.tlsPort = getPort(httpOnly)
log.Clear() log.Clear()
_, err = va.validateTLSSNI02(ctx, ident, chall) _, err = va.validateTLSSNI02(ctx, dnsi("localhost"), chall)
test.AssertError(t, err, "TLS-SNI-02 validation passed when talking to a HTTP-only server") test.AssertError(t, err, "TLS-SNI-02 validation passed when talking to a HTTP-only server")
test.Assert(t, strings.HasSuffix( test.Assert(t, strings.HasSuffix(
err.Error(), err.Error(),
@ -626,7 +627,7 @@ func TestTLSError(t *testing.T) {
va, _ := setup(hs, 0) va, _ := setup(hs, 0)
_, prob := va.validateTLSSNI01(ctx, ident, chall) _, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("TLS validation should have failed: What cert was used?") t.Fatalf("TLS validation should have failed: What cert was used?")
} }
@ -711,7 +712,7 @@ func TestSNIErrInvalidChain(t *testing.T) {
va, _ := setup(hs, 0) va, _ := setup(hs, 0)
// Validate the SNI challenge with the test server, expecting it to fail // Validate the SNI challenge with the test server, expecting it to fail
_, prob := va.validateTLSSNI01(ctx, ident, chall) _, prob := va.validateTLSSNI01(ctx, dnsi("localhost"), chall)
if prob == nil { if prob == nil {
t.Fatalf("TLS validation should have failed") t.Fatalf("TLS validation should have failed")
} }
@ -733,7 +734,7 @@ func TestValidateHTTP(t *testing.T) {
va, _ := setup(hs, 0) va, _ := setup(hs, 0)
_, prob := va.validateChallenge(ctx, ident, chall) _, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
test.Assert(t, prob == nil, "validation failed") test.Assert(t, prob == nil, "validation failed")
} }
@ -764,7 +765,7 @@ func TestValidateTLSSNI01(t *testing.T) {
va, _ := setup(hs, 0) va, _ := setup(hs, 0)
_, prob := va.validateChallenge(ctx, ident, chall) _, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
test.Assert(t, prob == nil, "validation failed") test.Assert(t, prob == nil, "validation failed")
} }
@ -776,89 +777,11 @@ func TestValidateTLSSNI01NotSane(t *testing.T) {
chall.Token = "not sane" chall.Token = "not sane"
_, prob := va.validateChallenge(ctx, ident, chall) _, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
test.AssertEquals(t, prob.Type, probs.MalformedProblem) test.AssertEquals(t, prob.Type, probs.MalformedProblem)
} }
func TestCAATimeout(t *testing.T) {
va, _ := setup(nil, 0)
err := va.checkCAA(ctx, core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "caa-timeout.com"})
if err.Type != probs.ConnectionProblem {
t.Errorf("Expected timeout error type %s, got %s", probs.ConnectionProblem, err.Type)
}
expected := "DNS problem: query timed out looking up CAA for always.timeout"
if err.Detail != expected {
t.Errorf("checkCAA: got %#v, expected %#v", err.Detail, expected)
}
}
func TestCAAChecking(t *testing.T) {
type CAATest struct {
Domain string
Present bool
Valid bool
}
tests := []CAATest{
// Reserved
{"reserved.com", true, false},
// Critical
{"critical.com", true, false},
{"nx.critical.com", true, false},
// Good (absent)
{"absent.com", false, true},
{"example.co.uk", false, true},
// Good (present)
{"present.com", true, true},
{"present.servfail.com", true, true},
// Good (multiple critical, one matching)
{"multi-crit-present.com", true, true},
// Bad (unknown critical)
{"unknown-critical.com", true, false},
{"unknown-critical2.com", true, false},
// Good (unknown noncritical, no issue/issuewild records)
{"unknown-noncritical.com", true, true},
// Good (issue record with unknown parameters)
{"present-with-parameter.com", true, true},
// Bad (unsatisfiable issue record)
{"unsatisfiable.com", true, false},
}
va, _ := setup(nil, 0)
for _, caaTest := range tests {
present, valid, err := va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: caaTest.Domain})
if err != nil {
t.Errorf("checkCAARecords error for %s: %s", caaTest.Domain, err)
}
if present != caaTest.Present {
t.Errorf("checkCAARecords presence mismatch for %s: got %t expected %t", caaTest.Domain, present, caaTest.Present)
}
if valid != caaTest.Valid {
t.Errorf("checkCAARecords validity mismatch for %s: got %t expected %t", caaTest.Domain, valid, caaTest.Valid)
}
}
present, valid, err := va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
test.AssertError(t, err, "servfail.com")
test.Assert(t, !present, "Present should be false")
test.Assert(t, !valid, "Valid should be false")
_, _, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.com"})
if err == nil {
t.Errorf("Should have returned error on CAA lookup, but did not: %s", "servfail.com")
}
present, valid, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.present.com"})
test.AssertError(t, err, "servfail.present.com")
test.Assert(t, !present, "Present should be false")
test.Assert(t, !valid, "Valid should be false")
_, _, err = va.checkCAARecords(ctx, core.AcmeIdentifier{Type: "dns", Value: "servfail.present.com"})
if err == nil {
t.Errorf("Should have returned error on CAA lookup, but did not: %s", "servfail.present.com")
}
}
func TestPerformValidationInvalid(t *testing.T) { func TestPerformValidationInvalid(t *testing.T) {
va, _ := setup(nil, 0) va, _ := setup(nil, 0)
@ -916,7 +839,7 @@ func TestDNSValidationFailure(t *testing.T) {
chalDNS := createChallenge(core.ChallengeTypeDNS01) chalDNS := createChallenge(core.ChallengeTypeDNS01)
_, prob := va.validateChallenge(ctx, ident, chalDNS) _, prob := va.validateChallenge(ctx, dnsi("localhost"), chalDNS)
test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
} }
@ -952,12 +875,12 @@ func TestDNSValidationNotSane(t *testing.T) {
var authz = core.Authorization{ var authz = core.Authorization{
ID: core.NewToken(), ID: core.NewToken(),
RegistrationID: 1, RegistrationID: 1,
Identifier: ident, Identifier: dnsi("localhost"),
Challenges: []core.Challenge{chal0, chal1, chal2}, Challenges: []core.Challenge{chal0, chal1, chal2},
} }
for i := 0; i < len(authz.Challenges); i++ { for i := 0; i < len(authz.Challenges); i++ {
_, prob := va.validateChallenge(ctx, ident, authz.Challenges[i]) _, prob := va.validateChallenge(ctx, dnsi("localhost"), authz.Challenges[i])
if prob.Type != probs.MalformedProblem { if prob.Type != probs.MalformedProblem {
t.Errorf("Got wrong error type for %d: expected %s, got %s", t.Errorf("Got wrong error type for %d: expected %s, got %s",
i, prob.Type, probs.MalformedProblem) i, prob.Type, probs.MalformedProblem)
@ -973,11 +896,7 @@ func TestDNSValidationServFail(t *testing.T) {
chalDNS := createChallenge(core.ChallengeTypeDNS01) chalDNS := createChallenge(core.ChallengeTypeDNS01)
badIdent := core.AcmeIdentifier{ _, prob := va.validateChallenge(ctx, dnsi("servfail.com"), chalDNS)
Type: core.IdentifierDNS,
Value: "servfail.com",
}
_, prob := va.validateChallenge(ctx, badIdent, chalDNS)
test.AssertEquals(t, prob.Type, probs.ConnectionProblem) test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
} }
@ -993,7 +912,7 @@ func TestDNSValidationNoServer(t *testing.T) {
chalDNS := createChallenge(core.ChallengeTypeDNS01) chalDNS := createChallenge(core.ChallengeTypeDNS01)
_, prob := va.validateChallenge(ctx, ident, chalDNS) _, prob := va.validateChallenge(ctx, dnsi("localhost"), chalDNS)
test.AssertEquals(t, prob.Type, probs.ConnectionProblem) test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
} }
@ -1006,12 +925,7 @@ func TestDNSValidationOK(t *testing.T) {
chalDNS.Token = expectedToken chalDNS.Token = expectedToken
chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
goodIdent := core.AcmeIdentifier{ _, prob := va.validateChallenge(ctx, dnsi("good-dns01.com"), chalDNS)
Type: core.IdentifierDNS,
Value: "good-dns01.com",
}
_, prob := va.validateChallenge(ctx, goodIdent, chalDNS)
test.Assert(t, prob == nil, "Should be valid.") test.Assert(t, prob == nil, "Should be valid.")
} }
@ -1025,38 +939,20 @@ func TestDNSValidationNoAuthorityOK(t *testing.T) {
chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization chalDNS.ProvidedKeyAuthorization = expectedKeyAuthorization
goodIdent := core.AcmeIdentifier{ _, prob := va.validateChallenge(ctx, dnsi("no-authority-dns01.com"), chalDNS)
Type: core.IdentifierDNS,
Value: "no-authority-dns01.com",
}
_, prob := va.validateChallenge(ctx, goodIdent, chalDNS)
test.Assert(t, prob == nil, "Should be valid.") test.Assert(t, prob == nil, "Should be valid.")
} }
func TestCAAFailure(t *testing.T) {
chall := createChallenge(core.ChallengeTypeTLSSNI01)
hs := tlssni01Srv(t, chall)
defer hs.Close()
va, _ := setup(hs, 0)
ident.Value = "reserved.com"
_, prob := va.validateChallengeAndCAA(ctx, ident, chall)
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
}
func TestLimitedReader(t *testing.T) { func TestLimitedReader(t *testing.T) {
chall := core.HTTPChallenge01() chall := core.HTTPChallenge01()
setChallengeToken(&chall, core.NewToken()) setChallengeToken(&chall, core.NewToken())
ident.Value = "localhost"
hs := httpSrv(t, "01234567890123456789012345678901234567890123456789012345678901234567890123456789") hs := httpSrv(t, "01234567890123456789012345678901234567890123456789012345678901234567890123456789")
va, _ := setup(hs, 0) va, _ := setup(hs, 0)
defer hs.Close() defer hs.Close()
_, prob := va.validateChallenge(ctx, ident, chall) _, prob := va.validateChallenge(ctx, dnsi("localhost"), chall)
test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
test.Assert(t, strings.HasPrefix(prob.Detail, "Invalid response from "), test.Assert(t, strings.HasPrefix(prob.Detail, "Invalid response from "),
@ -1089,24 +985,6 @@ func setup(srv *httptest.Server, maxRemoteFailures int) (*ValidationAuthorityImp
return va, logger return va, logger
} }
func TestParseResults(t *testing.T) {
r := []caaResult{}
s, err := parseResults(r)
test.Assert(t, s == nil, "set is not nil")
test.Assert(t, err == nil, "error is not nil")
test.AssertNotError(t, err, "no error should be returned")
r = []caaResult{{nil, errors.New("")}, {[]*dns.CAA{{Value: "test"}}, nil}}
s, err = parseResults(r)
test.Assert(t, s == nil, "set is not nil")
test.AssertEquals(t, err.Error(), "")
expected := dns.CAA{Value: "other-test"}
r = []caaResult{{[]*dns.CAA{&expected}, nil}, {[]*dns.CAA{{Value: "test"}}, nil}}
s, err = parseResults(r)
test.AssertEquals(t, len(s.Unknown), 1)
test.Assert(t, s.Unknown[0] == &expected, "Incorrect record returned")
test.AssertNotError(t, err, "no error should be returned")
}
func TestAvailableAddresses(t *testing.T) { func TestAvailableAddresses(t *testing.T) {
v6a := net.ParseIP("::1") v6a := net.ParseIP("::1")
v6b := net.ParseIP("2001:db8::2:1") // 2001:DB8 is reserved for docs (RFC 3849) v6b := net.ParseIP("2001:db8::2:1") // 2001:DB8 is reserved for docs (RFC 3849)
@ -1250,8 +1128,7 @@ func TestFallbackDialer(t *testing.T) {
// Since the IPv6First feature flag is not enabled we expect that the IPv4 // Since the IPv6First feature flag is not enabled we expect that the IPv4
// address will be used and validation will succeed using the httpSrv we // address will be used and validation will succeed using the httpSrv we
// created earlier. // created earlier.
host := "ipv4.and.ipv6.localhost" ident := dnsi("ipv4.and.ipv6.localhost")
ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: host}
records, prob := va.validateChallenge(ctx, ident, chall) records, prob := va.validateChallenge(ctx, ident, chall)
test.Assert(t, prob == nil, "validation failed for an dual homed host with IPv6First disabled") test.Assert(t, prob == nil, "validation failed for an dual homed host with IPv6First disabled")
// We expect one validation record to be present // We expect one validation record to be present
@ -1303,8 +1180,7 @@ func TestFallbackTLS(t *testing.T) {
// Since the IPv6First feature flag is not enabled we expect that the IPv4 // Since the IPv6First feature flag is not enabled we expect that the IPv4
// address will be used and validation will succeed using the httpSrv we // address will be used and validation will succeed using the httpSrv we
// created earlier. // created earlier.
host := "ipv4.and.ipv6.localhost" ident := dnsi("ipv4.and.ipv6.localhost")
ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: host}
records, prob := va.validateChallenge(ctx, ident, chall) records, prob := va.validateChallenge(ctx, ident, chall)
test.Assert(t, prob == nil, "validation failed for a dual-homed address with an IPv4 server") test.Assert(t, prob == nil, "validation failed for a dual-homed address with an IPv4 server")
// We expect one validation record to be present // We expect one validation record to be present
@ -1342,8 +1218,7 @@ func TestFallbackTLS(t *testing.T) {
// Now try a validation for an IPv6 only host. E.g. one without an IPv4 // Now try a validation for an IPv6 only host. E.g. one without an IPv4
// address. The IPv6 will fail without a server and we expect the overall // address. The IPv6 will fail without a server and we expect the overall
// validation to fail since there is no IPv4 address/listener to fall back to. // validation to fail since there is no IPv4 address/listener to fall back to.
host = "ipv6.localhost" ident = dnsi("ipv6.localhost")
ident = core.AcmeIdentifier{Type: core.IdentifierDNS, Value: host}
va.stats = metrics.NewNoopScope() va.stats = metrics.NewNoopScope()
records, prob = va.validateChallenge(ctx, ident, chall) records, prob = va.validateChallenge(ctx, ident, chall)
@ -1420,8 +1295,7 @@ func TestPerformRemoteValidation(t *testing.T) {
// Both remotes working, should succeed // Both remotes working, should succeed
probCh := make(chan *probs.ProblemDetails, 1) probCh := make(chan *probs.ProblemDetails, 1)
ident := core.AcmeIdentifier{Type: core.IdentifierDNS, Value: "localhost"} localVA.performRemoteValidation(context.Background(), "localhost", chall, core.Authorization{}, probCh)
localVA.performRemoteValidation(context.Background(), ident.Value, chall, core.Authorization{}, probCh)
prob := <-probCh prob := <-probCh
if prob != nil { if prob != nil {
t.Errorf("performRemoteValidation failed: %s", prob) t.Errorf("performRemoteValidation failed: %s", prob)
@ -1431,7 +1305,7 @@ func TestPerformRemoteValidation(t *testing.T) {
ms.mu.Lock() ms.mu.Lock()
delete(ms.allowedUAs, "remote 1") delete(ms.allowedUAs, "remote 1")
ms.mu.Unlock() ms.mu.Unlock()
localVA.performRemoteValidation(context.Background(), ident.Value, chall, core.Authorization{}, probCh) localVA.performRemoteValidation(context.Background(), "localhost", chall, core.Authorization{}, probCh)
prob = <-probCh prob = <-probCh
if prob == nil { if prob == nil {
t.Error("performRemoteValidation didn't fail when one 'remote' validation failed") t.Error("performRemoteValidation didn't fail when one 'remote' validation failed")
@ -1444,7 +1318,7 @@ func TestPerformRemoteValidation(t *testing.T) {
ms.mu.Unlock() ms.mu.Unlock()
// Both local and remotes working, should succeed // Both local and remotes working, should succeed
_, err := localVA.PerformValidation(context.Background(), ident.Value, chall, core.Authorization{}) _, err := localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
if err != nil { if err != nil {
t.Errorf("PerformValidation failed: %s", err) t.Errorf("PerformValidation failed: %s", err)
} }
@ -1453,7 +1327,7 @@ func TestPerformRemoteValidation(t *testing.T) {
ms.mu.Lock() ms.mu.Lock()
delete(ms.allowedUAs, "local") delete(ms.allowedUAs, "local")
ms.mu.Unlock() ms.mu.Unlock()
_, err = localVA.PerformValidation(context.Background(), ident.Value, chall, core.Authorization{}) _, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
if err == nil { if err == nil {
t.Error("PerformValidation didn't fail when local validation failed") t.Error("PerformValidation didn't fail when local validation failed")
} }
@ -1463,7 +1337,7 @@ func TestPerformRemoteValidation(t *testing.T) {
ms.allowedUAs["local"] = struct{}{} ms.allowedUAs["local"] = struct{}{}
delete(ms.allowedUAs, "remote 1") delete(ms.allowedUAs, "remote 1")
ms.mu.Unlock() ms.mu.Unlock()
_, err = localVA.PerformValidation(context.Background(), ident.Value, chall, core.Authorization{}) _, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
if err == nil { if err == nil {
t.Error("PerformValidation didn't fail when one 'remote' validation failed") t.Error("PerformValidation didn't fail when one 'remote' validation failed")
} }
@ -1475,7 +1349,7 @@ func TestPerformRemoteValidation(t *testing.T) {
{remoteVA1, "remote 1"}, {remoteVA1, "remote 1"},
{remoteVA2, "remote 2"}, {remoteVA2, "remote 2"},
} }
_, err = localVA.PerformValidation(context.Background(), ident.Value, chall, core.Authorization{}) _, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
if err != nil { if err != nil {
t.Errorf("PerformValidation failed when one 'remote' validation failed but maxRemoteFailures is 1: %s", err) t.Errorf("PerformValidation failed when one 'remote' validation failed but maxRemoteFailures is 1: %s", err)
} }
@ -1484,7 +1358,7 @@ func TestPerformRemoteValidation(t *testing.T) {
ms.mu.Lock() ms.mu.Lock()
delete(ms.allowedUAs, "remote 2") delete(ms.allowedUAs, "remote 2")
ms.mu.Unlock() ms.mu.Unlock()
_, err = localVA.PerformValidation(context.Background(), ident.Value, chall, core.Authorization{}) _, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
if err == nil { if err == nil {
t.Error("PerformValidation didn't fail when both 'remote' validations failed") t.Error("PerformValidation didn't fail when both 'remote' validations failed")
} }
@ -1495,7 +1369,7 @@ func TestPerformRemoteValidation(t *testing.T) {
ms.mu.Unlock() ms.mu.Unlock()
remoteVA2.userAgent = "slow remote" remoteVA2.userAgent = "slow remote"
s := time.Now() s := time.Now()
_, err = localVA.PerformValidation(context.Background(), ident.Value, chall, core.Authorization{}) _, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
if err != nil { if err != nil {
t.Errorf("PerformValidation failed when one 'remote' validation failed but maxRemoteFailures is 1: %s", err) t.Errorf("PerformValidation failed when one 'remote' validation failed but maxRemoteFailures is 1: %s", err)
} }
@ -1515,7 +1389,7 @@ func TestPerformRemoteValidation(t *testing.T) {
{remoteVA2, "remote 2"}, {remoteVA2, "remote 2"},
} }
s = time.Now() s = time.Now()
_, err = localVA.PerformValidation(context.Background(), ident.Value, chall, core.Authorization{}) _, err = localVA.PerformValidation(context.Background(), "localhost", chall, core.Authorization{})
if err == nil { if err == nil {
t.Error("PerformValidation didn't fail when two validations failed") t.Error("PerformValidation didn't fail when two validations failed")
} }

View File

@ -117,16 +117,16 @@ func (db *database) Init(config *Config, logger *log.Logger) bool {
db.setError(err) db.setError(err)
return false return false
} }
// Validate that the database threat list stored on disk is not too stale.
// Validate that the database threat list stored on disk is at least a if db.isStale(dbf.Time) {
// superset of the specified configuration.
if db.config.now().Sub(dbf.Time) > (db.config.UpdatePeriod + jitter) {
db.log.Printf("database loaded is stale") db.log.Printf("database loaded is stale")
db.ml.Lock() db.ml.Lock()
defer db.ml.Unlock() defer db.ml.Unlock()
db.setStale() db.setStale()
return false return false
} }
// Validate that the database threat list stored on disk is at least a
// superset of the specified configuration.
tfuNew := make(threatsForUpdate) tfuNew := make(threatsForUpdate)
for _, td := range db.config.ThreatLists { for _, td := range db.config.ThreatLists {
if row, ok := dbf.Table[td]; ok { if row, ok := dbf.Table[td]; ok {
@ -142,8 +142,9 @@ func (db *database) Init(config *Config, logger *log.Logger) bool {
return true return true
} }
// Status reports the health of the database. If in a faulted state, the db // Status reports the health of the database. The database is considered faulted
// may repair itself on the next Update. // if there was an error during update or if the last update has gone stale. If
// in a faulted state, the db may repair itself on the next Update.
func (db *database) Status() error { func (db *database) Status() error {
db.ml.RLock() db.ml.RLock()
defer db.ml.RUnlock() defer db.ml.RUnlock()
@ -151,7 +152,7 @@ func (db *database) Status() error {
if db.err != nil { if db.err != nil {
return db.err return db.err
} }
if db.config.now().Sub(db.last) > (db.config.UpdatePeriod + jitter) { if db.isStale(db.last) {
db.setStale() db.setStale()
return db.err return db.err
} }
@ -306,6 +307,16 @@ func (db *database) setError(err error) {
db.ml.Unlock() db.ml.Unlock()
} }
// isStale checks whether the last successful update should be considered stale.
// Staleness is defined as being older than two of the configured update periods
// plus jitter.
func (db *database) isStale(lastUpdate time.Time) bool {
if db.config.now().Sub(lastUpdate) > 2*(db.config.UpdatePeriod+jitter) {
return true
}
return false
}
// setStale sets the error state to a stale message, without clearing // setStale sets the error state to a stale message, without clearing
// the database state. // the database state.
// //

View File

@ -402,19 +402,8 @@ func (sb *SafeBrowser) LookupURLs(urls []string) (threats [][]URLThreat, err err
return threats, err return threats, err
} }
// TODO: There are some optimizations to be made here: hashes := make(map[hashPrefix]string)
// 1.) We could force a database update if it is in error. hash2idx := make(map[hashPrefix]int)
// However, we must ensure that we perform some form of rate-limiting.
// 2.) We should batch all of the partial hashes together such that we
// call api.HashLookup only once.
for i, url := range urls {
hashes, err := generateHashes(url)
if err != nil {
sb.log.Printf("error generating hashes: %v", err)
atomic.AddInt64(&sb.stats.QueriesFail, int64(len(urls)-i))
return threats, err
}
// Construct the follow-up request being made to the server. // Construct the follow-up request being made to the server.
// In the request, we only ask for partial hashes for privacy reasons. // In the request, we only ask for partial hashes for privacy reasons.
@ -428,7 +417,19 @@ func (sb *SafeBrowser) LookupURLs(urls []string) (threats [][]URLThreat, err err
ttm := make(map[pb.ThreatType]bool) ttm := make(map[pb.ThreatType]bool)
ptm := make(map[pb.PlatformType]bool) ptm := make(map[pb.PlatformType]bool)
tetm := make(map[pb.ThreatEntryType]bool) tetm := make(map[pb.ThreatEntryType]bool)
for fullHash, pattern := range hashes {
for i, url := range urls {
urlhashes, err := generateHashes(url)
if err != nil {
sb.log.Printf("error generating urlhashes: %v", err)
atomic.AddInt64(&sb.stats.QueriesFail, int64(len(urls)-i))
return threats, err
}
for fullHash, pattern := range urlhashes {
hashes[fullHash] = pattern
hash2idx[fullHash] = i
// Lookup in database according to threat list. // Lookup in database according to threat list.
partialHash, unsureThreats := sb.db.Lookup(fullHash) partialHash, unsureThreats := sb.db.Lookup(fullHash)
if len(unsureThreats) == 0 { if len(unsureThreats) == 0 {
@ -451,6 +452,7 @@ func (sb *SafeBrowser) LookupURLs(urls []string) (threats [][]URLThreat, err err
}) })
} }
} }
atomic.AddInt64(&sb.stats.QueriesByCache, 1)
case negativeCacheHit: case negativeCacheHit:
// This is cached as a non-threat. // This is cached as a non-threat.
atomic.AddInt64(&sb.stats.QueriesByCache, 1) atomic.AddInt64(&sb.stats.QueriesByCache, 1)
@ -467,6 +469,7 @@ func (sb *SafeBrowser) LookupURLs(urls []string) (threats [][]URLThreat, err err
&pb.ThreatEntry{Hash: []byte(partialHash)}) &pb.ThreatEntry{Hash: []byte(partialHash)})
} }
} }
}
for tt := range ttm { for tt := range ttm {
req.ThreatInfo.ThreatTypes = append(req.ThreatInfo.ThreatTypes, tt) req.ThreatInfo.ThreatTypes = append(req.ThreatInfo.ThreatTypes, tt)
} }
@ -477,17 +480,12 @@ func (sb *SafeBrowser) LookupURLs(urls []string) (threats [][]URLThreat, err err
req.ThreatInfo.ThreatEntryTypes = append(req.ThreatInfo.ThreatEntryTypes, tet) req.ThreatInfo.ThreatEntryTypes = append(req.ThreatInfo.ThreatEntryTypes, tet)
} }
// All results are known, so just continue.
if len(req.ThreatInfo.ThreatEntries) == 0 {
atomic.AddInt64(&sb.stats.QueriesByCache, 1)
continue
}
// Actually query the Safe Browsing API for exact full hash matches. // Actually query the Safe Browsing API for exact full hash matches.
if len(req.ThreatInfo.ThreatEntries) != 0 {
resp, err := sb.api.HashLookup(req) resp, err := sb.api.HashLookup(req)
if err != nil { if err != nil {
sb.log.Printf("HashLookup failure: %v", err) sb.log.Printf("HashLookup failure: %v", err)
atomic.AddInt64(&sb.stats.QueriesFail, int64(len(urls)-i)) atomic.AddInt64(&sb.stats.QueriesFail, 1)
return threats, err return threats, err
} }
@ -500,7 +498,9 @@ func (sb *SafeBrowser) LookupURLs(urls []string) (threats [][]URLThreat, err err
if !fullHash.IsFull() { if !fullHash.IsFull() {
continue continue
} }
if pattern, ok := hashes[fullHash]; ok { pattern, ok := hashes[fullHash]
idx, findidx := hash2idx[fullHash]
if findidx && ok {
td := ThreatDescriptor{ td := ThreatDescriptor{
ThreatType: ThreatType(tm.ThreatType), ThreatType: ThreatType(tm.ThreatType),
PlatformType: PlatformType(tm.PlatformType), PlatformType: PlatformType(tm.PlatformType),
@ -509,7 +509,7 @@ func (sb *SafeBrowser) LookupURLs(urls []string) (threats [][]URLThreat, err err
if !sb.lists[td] { if !sb.lists[td] {
continue continue
} }
threats[i] = append(threats[i], URLThreat{ threats[idx] = append(threats[idx], URLThreat{
Pattern: pattern, Pattern: pattern,
ThreatDescriptor: td, ThreatDescriptor: td,
}) })

View File

@ -21,9 +21,6 @@ import (
"time" "time"
"github.com/jmhodges/clock" "github.com/jmhodges/clock"
"golang.org/x/net/context"
"gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto" corepb "github.com/letsencrypt/boulder/core/proto"
berrors "github.com/letsencrypt/boulder/errors" berrors "github.com/letsencrypt/boulder/errors"
@ -38,6 +35,10 @@ import (
rapb "github.com/letsencrypt/boulder/ra/proto" rapb "github.com/letsencrypt/boulder/ra/proto"
"github.com/letsencrypt/boulder/revocation" "github.com/letsencrypt/boulder/revocation"
"github.com/letsencrypt/boulder/test" "github.com/letsencrypt/boulder/test"
vaPB "github.com/letsencrypt/boulder/va/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"gopkg.in/square/go-jose.v2"
) )
const ( const (
@ -785,6 +786,17 @@ func TestRandomDirectoryKey(t *testing.T) {
} }
} }
// noopCAA implements RA's caaChecker, always returning nil
type noopCAA struct{}
func (cr noopCAA) IsCAAValid(
ctx context.Context,
in *vaPB.IsCAAValidRequest,
opts ...grpc.CallOption,
) (*vaPB.IsCAAValidResponse, error) {
return &vaPB.IsCAAValidResponse{}, nil
}
func TestRelativeDirectory(t *testing.T) { func TestRelativeDirectory(t *testing.T) {
_ = features.Set(map[string]bool{"AllowKeyRollover": true}) _ = features.Set(map[string]bool{"AllowKeyRollover": true})
defer features.Reset() defer features.Reset()
@ -853,6 +865,7 @@ func TestIssueCertificate(t *testing.T) {
// TODO: Use a mock RA so we can test various conditions of authorized, not // TODO: Use a mock RA so we can test various conditions of authorized, not
// authorized, etc. // authorized, etc.
stats := metrics.NewNoopScope() stats := metrics.NewNoopScope()
ra := ra.NewRegistrationAuthorityImpl( ra := ra.NewRegistrationAuthorityImpl(
fc, fc,
wfe.log, wfe.log,
@ -865,6 +878,7 @@ func TestIssueCertificate(t *testing.T) {
300*24*time.Hour, 300*24*time.Hour,
7*24*time.Hour, 7*24*time.Hour,
nil, nil,
noopCAA{},
0, 0,
) )
ra.SA = mocks.NewStorageAuthority(fc) ra.SA = mocks.NewStorageAuthority(fc)

View File

@ -375,7 +375,7 @@ func (wfe *WebFrontEndImpl) lookupJWK(
header := jws.Signatures[0].Header header := jws.Signatures[0].Header
accountURL := header.KeyID accountURL := header.KeyID
prefix := wfe.relativeEndpoint(request, regPath) prefix := wfe.relativeEndpoint(request, acctPath)
accountIDStr := strings.TrimPrefix(accountURL, prefix) accountIDStr := strings.TrimPrefix(accountURL, prefix)
// Convert the account ID string to an int64 for use with the SA's // Convert the account ID string to an int64 for use with the SA's
// GetRegistration RPC // GetRegistration RPC

View File

@ -133,7 +133,7 @@ func signRequestKeyID(
jwk := &jose.JSONWebKey{ jwk := &jose.JSONWebKey{
Key: privateKey, Key: privateKey,
Algorithm: keyAlgForKey(t, privateKey), Algorithm: keyAlgForKey(t, privateKey),
KeyID: fmt.Sprintf("http://localhost/acme/reg/%d", keyID), KeyID: fmt.Sprintf("http://localhost/acme/acct/%d", keyID),
} }
signerKey := jose.SigningKey{ signerKey := jose.SigningKey{
@ -994,7 +994,7 @@ func TestLookupJWK(t *testing.T) {
Request: makePostRequestWithPath("test-path", errorIDJWSBody), Request: makePostRequestWithPath("test-path", errorIDJWSBody),
ExpectedProblem: &probs.ProblemDetails{ ExpectedProblem: &probs.ProblemDetails{
Type: probs.ServerInternalProblem, Type: probs.ServerInternalProblem,
Detail: "Error retreiving account \"http://localhost/acme/reg/100\"", Detail: "Error retreiving account \"http://localhost/acme/acct/100\"",
HTTPStatus: http.StatusInternalServerError, HTTPStatus: http.StatusInternalServerError,
}, },
ErrorStatType: "JWSKeyIDLookupFailed", ErrorStatType: "JWSKeyIDLookupFailed",
@ -1005,7 +1005,7 @@ func TestLookupJWK(t *testing.T) {
Request: makePostRequestWithPath("test-path", missingIDJWSBody), Request: makePostRequestWithPath("test-path", missingIDJWSBody),
ExpectedProblem: &probs.ProblemDetails{ ExpectedProblem: &probs.ProblemDetails{
Type: probs.AccountDoesNotExistProblem, Type: probs.AccountDoesNotExistProblem,
Detail: "Account \"http://localhost/acme/reg/102\" not found", Detail: "Account \"http://localhost/acme/acct/102\" not found",
HTTPStatus: http.StatusBadRequest, HTTPStatus: http.StatusBadRequest,
}, },
ErrorStatType: "JWSKeyIDNotFound", ErrorStatType: "JWSKeyIDNotFound",
@ -1234,7 +1234,7 @@ func TestValidPOSTForAccount(t *testing.T) {
Request: makePostRequestWithPath("test", missingJWSBody), Request: makePostRequestWithPath("test", missingJWSBody),
ExpectedProblem: &probs.ProblemDetails{ ExpectedProblem: &probs.ProblemDetails{
Type: probs.AccountDoesNotExistProblem, Type: probs.AccountDoesNotExistProblem,
Detail: "Account \"http://localhost/acme/reg/102\" not found", Detail: "Account \"http://localhost/acme/acct/102\" not found",
HTTPStatus: http.StatusBadRequest, HTTPStatus: http.StatusBadRequest,
}, },
ErrorStatType: "JWSKeyIDNotFound", ErrorStatType: "JWSKeyIDNotFound",

View File

@ -39,8 +39,8 @@ import (
// measured_http. // measured_http.
const ( const (
directoryPath = "/directory" directoryPath = "/directory"
newRegPath = "/acme/new-reg" newAcctPath = "/acme/new-acct"
regPath = "/acme/reg/" acctPath = "/acme/acct/"
authzPath = "/acme/authz/" authzPath = "/acme/authz/"
challengePath = "/acme/challenge/" challengePath = "/acme/challenge/"
certPath = "/acme/cert/" certPath = "/acme/cert/"
@ -302,8 +302,8 @@ func (wfe *WebFrontEndImpl) relativeDirectory(request *http.Request, directory m
func (wfe *WebFrontEndImpl) Handler() http.Handler { func (wfe *WebFrontEndImpl) Handler() http.Handler {
m := http.NewServeMux() m := http.NewServeMux()
wfe.HandleFunc(m, directoryPath, wfe.Directory, "GET") wfe.HandleFunc(m, directoryPath, wfe.Directory, "GET")
wfe.HandleFunc(m, newRegPath, wfe.NewRegistration, "POST") wfe.HandleFunc(m, newAcctPath, wfe.NewAccount, "POST")
wfe.HandleFunc(m, regPath, wfe.Registration, "POST") wfe.HandleFunc(m, acctPath, wfe.Account, "POST")
wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET", "POST") wfe.HandleFunc(m, authzPath, wfe.Authorization, "GET", "POST")
wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST") wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST")
wfe.HandleFunc(m, certPath, wfe.Certificate, "GET") wfe.HandleFunc(m, certPath, wfe.Certificate, "GET")
@ -372,7 +372,7 @@ func addRequesterHeader(w http.ResponseWriter, requester int64) {
// using the `request.Host` of the HTTP request. // using the `request.Host` of the HTTP request.
func (wfe *WebFrontEndImpl) Directory(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) Directory(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
directoryEndpoints := map[string]interface{}{ directoryEndpoints := map[string]interface{}{
"new-reg": newRegPath, "new-account": newAcctPath,
"revoke-cert": revokeCertPath, "revoke-cert": revokeCertPath,
} }
@ -444,10 +444,14 @@ func link(url, relation string) string {
return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation) return fmt.Sprintf("<%s>;rel=\"%s\"", url, relation)
} }
// NewRegistration is used by clients to submit a new registration/account // NewAccount is used by clients to submit a new account
func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) NewAccount(
ctx context.Context,
logEvent *requestEvent,
response http.ResponseWriter,
request *http.Request) {
// NewRegistration uses `validSelfAuthenticatedPOST` instead of // NewAccount uses `validSelfAuthenticatedPOST` instead of
// `validPOSTforAccount` because there is no account to authenticate against // `validPOSTforAccount` because there is no account to authenticate against
// until after it is created! // until after it is created!
body, key, prob := wfe.validSelfAuthenticatedPOST(request, logEvent) body, key, prob := wfe.validSelfAuthenticatedPOST(request, logEvent)
@ -457,10 +461,11 @@ func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *reque
return return
} }
if existingReg, err := wfe.SA.GetRegistrationByKey(ctx, key); err == nil { if existingAcct, err := wfe.SA.GetRegistrationByKey(ctx, key); err == nil {
response.Header().Set("Location", wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", regPath, existingReg.ID))) response.Header().Set("Location",
// TODO(#595): check for missing registration err wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", acctPath, existingAcct.ID)))
wfe.sendError(response, logEvent, probs.Conflict("Registration key is already in use"), err) // TODO(#595): check for missing account err
wfe.sendError(response, logEvent, probs.Conflict("Account key is already in use"), err)
return return
} }
@ -488,37 +493,36 @@ func (wfe *WebFrontEndImpl) NewRegistration(ctx context.Context, logEvent *reque
} }
} }
reg, err := wfe.RA.NewRegistration(ctx, init) acct, err := wfe.RA.NewRegistration(ctx, init)
if err != nil { if err != nil {
logEvent.AddError("unable to create new registration: %s", err) logEvent.AddError("unable to create new account: %s", err)
wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new registration"), err) wfe.sendError(response, logEvent,
problemDetailsForError(err, "Error creating new account"), err)
return return
} }
logEvent.Requester = reg.ID logEvent.Requester = acct.ID
addRequesterHeader(response, reg.ID) addRequesterHeader(response, acct.ID)
logEvent.Contacts = reg.Contact logEvent.Contacts = acct.Contact
// Use an explicitly typed variable. Otherwise `go vet' incorrectly complains acctURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", acctPath, acct.ID))
// that reg.ID is a string being passed to %d.
regURL := wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", regPath, reg.ID))
response.Header().Add("Location", regURL) response.Header().Add("Location", acctURL)
if len(wfe.SubscriberAgreementURL) > 0 { if len(wfe.SubscriberAgreementURL) > 0 {
response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
} }
err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, reg) err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, acct)
if err != nil { if err != nil {
// ServerInternal because we just created this registration, and it // ServerInternal because we just created this account, and it
// should be OK. // should be OK.
logEvent.AddError("unable to marshal registration: %s", err) logEvent.AddError("unable to marshal account: %s", err)
wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling registration"), err) wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling account"), err)
return return
} }
} }
func (wfe *WebFrontEndImpl) regHoldsAuthorizations(ctx context.Context, regID int64, names []string) (bool, error) { func (wfe *WebFrontEndImpl) acctHoldsAuthorizations(ctx context.Context, acctID int64, names []string) (bool, error) {
authz, err := wfe.SA.GetValidAuthorizations(ctx, regID, names, wfe.clk.Now()) authz, err := wfe.SA.GetValidAuthorizations(ctx, acctID, names, wfe.clk.Now())
if err != nil { if err != nil {
return false, err return false, err
} }
@ -789,7 +793,7 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(
response.WriteHeader(http.StatusOK) response.WriteHeader(http.StatusOK)
} }
func (wfe *WebFrontEndImpl) logCsr(request *http.Request, cr core.CertificateRequest, registration core.Registration) { func (wfe *WebFrontEndImpl) logCsr(request *http.Request, cr core.CertificateRequest, account core.Registration) {
var csrLog = struct { var csrLog = struct {
ClientAddr string ClientAddr string
CSR string CSR string
@ -797,7 +801,7 @@ func (wfe *WebFrontEndImpl) logCsr(request *http.Request, cr core.CertificateReq
}{ }{
ClientAddr: getClientAddr(request), ClientAddr: getClientAddr(request),
CSR: hex.EncodeToString(cr.Bytes), CSR: hex.EncodeToString(cr.Bytes),
Registration: registration, Registration: account,
} }
wfe.log.AuditObject("Certificate request", csrLog) wfe.log.AuditObject("Certificate request", csrLog)
} }
@ -922,7 +926,7 @@ func (wfe *WebFrontEndImpl) postChallenge(
authz core.Authorization, authz core.Authorization,
challengeIndex int, challengeIndex int,
logEvent *requestEvent) { logEvent *requestEvent) {
body, _, currReg, prob := wfe.validPOSTForAccount(request, ctx, logEvent) body, _, currAcct, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
addRequesterHeader(response, logEvent.Requester) addRequesterHeader(response, logEvent.Requester)
if prob != nil { if prob != nil {
// validPOSTForAccount handles its own setting of logEvent.Errors // validPOSTForAccount handles its own setting of logEvent.Errors
@ -930,20 +934,21 @@ func (wfe *WebFrontEndImpl) postChallenge(
return return
} }
// Any version of the agreement is acceptable here. Version match is enforced in // Any version of the agreement is acceptable here. Version match is enforced in
// wfe.Registration when agreeing the first time. Agreement updates happen // wfe.Account when agreeing the first time. Agreement updates happen
// by mailing subscribers and don't require a registration update. // by mailing subscribers and don't require an account update.
if currReg.Agreement == "" { if currAcct.Agreement == "" {
wfe.sendError(response, logEvent, probs.Unauthorized("Registration didn't agree to subscriber agreement before any further actions"), nil) wfe.sendError(response, logEvent,
probs.Unauthorized("Account must agree to subscriber agreement before any further actions"), nil)
return return
} }
// Check that the registration ID matching the key used matches // Check that the account ID matching the key used matches
// the registration ID on the authz object // the account ID on the authz object
if currReg.ID != authz.RegistrationID { if currAcct.ID != authz.RegistrationID {
logEvent.AddError("User registration id: %d != Authorization registration id: %v", currReg.ID, authz.RegistrationID) logEvent.AddError("User account id: %d != Authorization account id: %v", currAcct.ID, authz.RegistrationID)
wfe.sendError(response, wfe.sendError(response,
logEvent, logEvent,
probs.Unauthorized("User registration ID doesn't match registration ID in authorization"), probs.Unauthorized("User account ID doesn't match account ID in authorization"),
nil, nil,
) )
return return
@ -981,13 +986,13 @@ func (wfe *WebFrontEndImpl) postChallenge(
} }
} }
// Registration is used by a client to submit an update to their registration. // Account is used by a client to submit an update to their account.
func (wfe *WebFrontEndImpl) Registration( func (wfe *WebFrontEndImpl) Account(
ctx context.Context, ctx context.Context,
logEvent *requestEvent, logEvent *requestEvent,
response http.ResponseWriter, response http.ResponseWriter,
request *http.Request) { request *http.Request) {
body, _, currReg, prob := wfe.validPOSTForAccount(request, ctx, logEvent) body, _, currAcct, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
addRequesterHeader(response, logEvent.Requester) addRequesterHeader(response, logEvent.Requester)
if prob != nil { if prob != nil {
// validPOSTForAccount handles its own setting of logEvent.Errors // validPOSTForAccount handles its own setting of logEvent.Errors
@ -996,33 +1001,34 @@ func (wfe *WebFrontEndImpl) Registration(
} }
// Requests to this handler should have a path that leads to a known // Requests to this handler should have a path that leads to a known
// registration // account
idStr := request.URL.Path idStr := request.URL.Path
id, err := strconv.ParseInt(idStr, 10, 64) id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil { if err != nil {
logEvent.AddError("registration ID must be an integer, was %#v", idStr) logEvent.AddError("account ID must be an integer, was %#v", idStr)
wfe.sendError(response, logEvent, probs.Malformed("Registration ID must be an integer"), err) wfe.sendError(response, logEvent, probs.Malformed("Account ID must be an integer"), err)
return return
} else if id <= 0 { } else if id <= 0 {
msg := fmt.Sprintf("Registration ID must be a positive non-zero integer, was %d", id) msg := fmt.Sprintf("Account ID must be a positive non-zero integer, was %d", id)
logEvent.AddError(msg) logEvent.AddError(msg)
wfe.sendError(response, logEvent, probs.Malformed(msg), nil) wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
return return
} else if id != currReg.ID { } else if id != currAcct.ID {
logEvent.AddError("Request signing key did not match registration key: %d != %d", id, currReg.ID) logEvent.AddError("Request signing key did not match account key: %d != %d", id, currAcct.ID)
wfe.sendError(response, logEvent, probs.Unauthorized("Request signing key did not match registration key"), nil) wfe.sendError(response, logEvent,
probs.Unauthorized("Request signing key did not match account key"), nil)
return return
} }
var update core.Registration var update core.Registration
err = json.Unmarshal(body, &update) err = json.Unmarshal(body, &update)
if err != nil { if err != nil {
logEvent.AddError("unable to JSON parse registration: %s", err) logEvent.AddError("unable to JSON parse account: %s", err)
wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling registration"), err) wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling account"), err)
return return
} }
// People *will* POST their full registrations to this endpoint, including // People *will* POST their full accounts to this endpoint, including
// the 'valid' status, to avoid always failing out when that happens only // the 'valid' status, to avoid always failing out when that happens only
// attempt to deactivate if the provided status is different from their current // attempt to deactivate if the provided status is different from their current
// status. // status.
@ -1030,42 +1036,44 @@ func (wfe *WebFrontEndImpl) Registration(
// If a user tries to send both a deactivation request and an update to their // If a user tries to send both a deactivation request and an update to their
// contacts or subscriber agreement URL the deactivation will take place and // contacts or subscriber agreement URL the deactivation will take place and
// return before an update would be performed. // return before an update would be performed.
if update.Status != "" && update.Status != currReg.Status { if update.Status != "" && update.Status != currAcct.Status {
if update.Status != core.StatusDeactivated { if update.Status != core.StatusDeactivated {
wfe.sendError(response, logEvent, probs.Malformed("Invalid value provided for status field"), nil) wfe.sendError(response, logEvent, probs.Malformed("Invalid value provided for status field"), nil)
return return
} }
wfe.deactivateRegistration(ctx, *currReg, response, request, logEvent) wfe.deactivateAccount(ctx, *currAcct, response, request, logEvent)
return return
} }
// If a user POSTs their registration object including a previously valid // If a user POSTs their account object including a previously valid
// agreement URL but that URL has since changed we will fail out here // agreement URL but that URL has since changed we will fail out here
// since the update agreement URL doesn't match the current URL. To fix that we // since the update agreement URL doesn't match the current URL. To fix that we
// only fail if the sent URL doesn't match the currently valid agreement URL // only fail if the sent URL doesn't match the currently valid agreement URL
// and it doesn't match the URL currently stored in the registration // and it doesn't match the URL currently stored in the account
// in the database. The RA understands the user isn't actually trying to // in the database. The RA understands the user isn't actually trying to
// update the agreement but since we do an early check here in order to prevent // update the agreement but since we do an early check here in order to prevent
// extraneous requests to the RA we have to add this bypass. // extraneous requests to the RA we have to add this bypass.
if len(update.Agreement) > 0 && update.Agreement != currReg.Agreement && if len(update.Agreement) > 0 && update.Agreement != currAcct.Agreement &&
update.Agreement != wfe.SubscriberAgreementURL { update.Agreement != wfe.SubscriberAgreementURL {
msg := fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", update.Agreement, wfe.SubscriberAgreementURL) msg := fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]",
update.Agreement, wfe.SubscriberAgreementURL)
logEvent.AddError(msg) logEvent.AddError(msg)
wfe.sendError(response, logEvent, probs.Malformed(msg), nil) wfe.sendError(response, logEvent, probs.Malformed(msg), nil)
return return
} }
// Registration objects contain a JWK object which are merged in UpdateRegistration // Account objects contain a JWK object which are merged in UpdateRegistration
// if it is different from the existing registration key. Since this isn't how you // if it is different from the existing account key. Since this isn't how you
// update the key we just copy the existing one into the update object here. This // update the key we just copy the existing one into the update object here. This
// ensures the key isn't changed and that we can cleanly serialize the update as // ensures the key isn't changed and that we can cleanly serialize the update as
// JSON to send via RPC to the RA. // JSON to send via RPC to the RA.
update.Key = currReg.Key update.Key = currAcct.Key
updatedReg, err := wfe.RA.UpdateRegistration(ctx, *currReg, update) updatedAcct, err := wfe.RA.UpdateRegistration(ctx, *currAcct, update)
if err != nil { if err != nil {
logEvent.AddError("unable to update registration: %s", err) logEvent.AddError("unable to update account: %s", err)
wfe.sendError(response, logEvent, problemDetailsForError(err, "Unable to update registration"), err) wfe.sendError(response, logEvent,
problemDetailsForError(err, "Unable to update account"), err)
return return
} }
@ -1073,11 +1081,12 @@ func (wfe *WebFrontEndImpl) Registration(
response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service")) response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
} }
err = wfe.writeJsonResponse(response, logEvent, http.StatusAccepted, updatedReg) err = wfe.writeJsonResponse(response, logEvent, http.StatusAccepted, updatedAcct)
if err != nil { if err != nil {
// ServerInternal because we just generated the reg, it should be OK // ServerInternal because we just generated the account, it should be OK
logEvent.AddError("unable to marshal updated registration: %s", err) logEvent.AddError("unable to marshal updated account: %s", err)
wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal registration"), err) wfe.sendError(response, logEvent,
probs.ServerInternal("Failed to marshal account"), err)
return return
} }
} }
@ -1088,15 +1097,16 @@ func (wfe *WebFrontEndImpl) deactivateAuthorization(
logEvent *requestEvent, logEvent *requestEvent,
response http.ResponseWriter, response http.ResponseWriter,
request *http.Request) bool { request *http.Request) bool {
body, _, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent) body, _, acct, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
addRequesterHeader(response, logEvent.Requester) addRequesterHeader(response, logEvent.Requester)
if prob != nil { if prob != nil {
wfe.sendError(response, logEvent, prob, nil) wfe.sendError(response, logEvent, prob, nil)
return false return false
} }
if reg.ID != authz.RegistrationID { if acct.ID != authz.RegistrationID {
logEvent.AddError("registration ID doesn't match ID for authorization") logEvent.AddError("account ID doesn't match ID for authorization")
wfe.sendError(response, logEvent, probs.Unauthorized("Registration ID doesn't match ID for authorization"), nil) wfe.sendError(response, logEvent,
probs.Unauthorized("Account ID doesn't match ID for authorization"), nil)
return false return false
} }
var req struct { var req struct {
@ -1347,7 +1357,7 @@ func (wfe *WebFrontEndImpl) KeyRollover(
// Check that the new key isn't the same as the old key. This would fail as // Check that the new key isn't the same as the old key. This would fail as
// part of the subsequent `wfe.SA.GetRegistrationByKey` check since the new key // part of the subsequent `wfe.SA.GetRegistrationByKey` check since the new key
// will find the old registration if its equal to the old registration key. We // will find the old account if its equal to the old account key. We
// check new key against old key explicitly to save an RPC round trip and a DB // check new key against old key explicitly to save an RPC round trip and a DB
// query for this easy rejection case // query for this easy rejection case
keysEqual, err := core.PublicKeysEqual(newKey.Key, acct.Key.Key) keysEqual, err := core.PublicKeysEqual(newKey.Key, acct.Key.Key)
@ -1365,8 +1375,10 @@ func (wfe *WebFrontEndImpl) KeyRollover(
// Check that the new key isn't already being used for an existing account // Check that the new key isn't already being used for an existing account
if existingAcct, err := wfe.SA.GetRegistrationByKey(ctx, &newKey); err != nil { if existingAcct, err := wfe.SA.GetRegistrationByKey(ctx, &newKey); err != nil {
response.Header().Set("Location", wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", regPath, existingAcct.ID))) response.Header().Set("Location",
wfe.sendError(response, logEvent, probs.Conflict("New key is already in use for a different account"), err) wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", acctPath, existingAcct.ID)))
wfe.sendError(response, logEvent,
probs.Conflict("New key is already in use for a different account"), err)
return return
} }
@ -1385,20 +1397,27 @@ func (wfe *WebFrontEndImpl) KeyRollover(
} }
} }
func (wfe *WebFrontEndImpl) deactivateRegistration(ctx context.Context, reg core.Registration, response http.ResponseWriter, request *http.Request, logEvent *requestEvent) { func (wfe *WebFrontEndImpl) deactivateAccount(
err := wfe.RA.DeactivateRegistration(ctx, reg) ctx context.Context,
acct core.Registration,
response http.ResponseWriter,
request *http.Request,
logEvent *requestEvent) {
err := wfe.RA.DeactivateRegistration(ctx, acct)
if err != nil { if err != nil {
logEvent.AddError("unable to deactivate registration", err) logEvent.AddError("unable to deactivate account", err)
wfe.sendError(response, logEvent, problemDetailsForError(err, "Error deactivating registration"), err) wfe.sendError(response, logEvent,
problemDetailsForError(err, "Error deactivating account"), err)
return return
} }
reg.Status = core.StatusDeactivated acct.Status = core.StatusDeactivated
err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, reg) err = wfe.writeJsonResponse(response, logEvent, http.StatusOK, acct)
if err != nil { if err != nil {
// ServerInternal because registration is from DB and should be fine // ServerInternal because account is from DB and should be fine
logEvent.AddError("unable to marshal updated registration: %s", err) logEvent.AddError("unable to marshal updated account: %s", err)
wfe.sendError(response, logEvent, probs.ServerInternal("Failed to marshal registration"), err) wfe.sendError(response, logEvent,
probs.ServerInternal("Failed to marshal account"), err)
return return
} }
} }
@ -1426,8 +1445,12 @@ type orderJSON struct {
} }
// NewOrder is used by clients to create a new order object from a CSR // NewOrder is used by clients to create a new order object from a CSR
func (wfe *WebFrontEndImpl) NewOrder(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) { func (wfe *WebFrontEndImpl) NewOrder(
body, _, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent) ctx context.Context,
logEvent *requestEvent,
response http.ResponseWriter,
request *http.Request) {
body, _, acct, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
addRequesterHeader(response, logEvent.Requester) addRequesterHeader(response, logEvent.Requester)
if prob != nil { if prob != nil {
// validPOSTForAccount handles its own setting of logEvent.Errors // validPOSTForAccount handles its own setting of logEvent.Errors
@ -1471,7 +1494,7 @@ func (wfe *WebFrontEndImpl) NewOrder(ctx context.Context, logEvent *requestEvent
} }
order, err := wfe.RA.NewOrder(ctx, &rapb.NewOrderRequest{ order, err := wfe.RA.NewOrder(ctx, &rapb.NewOrderRequest{
RegistrationID: &reg.ID, RegistrationID: &acct.ID,
Csr: rawCSR.CSR, Csr: rawCSR.CSR,
}) })
if err != nil { if err != nil {

View File

@ -207,26 +207,26 @@ type MockRegistrationAuthority struct {
lastRevocationReason revocation.Reason lastRevocationReason revocation.Reason
} }
func (ra *MockRegistrationAuthority) NewRegistration(ctx context.Context, reg core.Registration) (core.Registration, error) { func (ra *MockRegistrationAuthority) NewRegistration(ctx context.Context, acct core.Registration) (core.Registration, error) {
return reg, nil return acct, nil
} }
func (ra *MockRegistrationAuthority) NewAuthorization(ctx context.Context, authz core.Authorization, regID int64) (core.Authorization, error) { func (ra *MockRegistrationAuthority) NewAuthorization(ctx context.Context, authz core.Authorization, acctID int64) (core.Authorization, error) {
authz.RegistrationID = regID authz.RegistrationID = acctID
authz.ID = "bkrPh2u0JUf18-rVBZtOOWWb3GuIiliypL-hBM9Ak1Q" authz.ID = "bkrPh2u0JUf18-rVBZtOOWWb3GuIiliypL-hBM9Ak1Q"
return authz, nil return authz, nil
} }
func (ra *MockRegistrationAuthority) NewCertificate(ctx context.Context, req core.CertificateRequest, regID int64) (core.Certificate, error) { func (ra *MockRegistrationAuthority) NewCertificate(ctx context.Context, req core.CertificateRequest, acctID int64) (core.Certificate, error) {
return core.Certificate{}, nil return core.Certificate{}, nil
} }
func (ra *MockRegistrationAuthority) UpdateRegistration(ctx context.Context, reg core.Registration, updated core.Registration) (core.Registration, error) { func (ra *MockRegistrationAuthority) UpdateRegistration(ctx context.Context, acct core.Registration, updated core.Registration) (core.Registration, error) {
keysMatch, _ := core.PublicKeysEqual(reg.Key.Key, updated.Key.Key) keysMatch, _ := core.PublicKeysEqual(acct.Key.Key, updated.Key.Key)
if !keysMatch { if !keysMatch {
reg.Key = updated.Key acct.Key = updated.Key
} }
return reg, nil return acct, nil
} }
func (ra *MockRegistrationAuthority) UpdateAuthorization(ctx context.Context, authz core.Authorization, foo int, challenge core.Challenge) (core.Authorization, error) { func (ra *MockRegistrationAuthority) UpdateAuthorization(ctx context.Context, authz core.Authorization, foo int, challenge core.Challenge) (core.Authorization, error) {
@ -672,7 +672,7 @@ func TestDirectory(t *testing.T) {
"meta": { "meta": {
"terms-of-service": "http://example.invalid/terms" "terms-of-service": "http://example.invalid/terms"
}, },
"new-reg": "http://localhost:4300/acme/new-reg", "new-account": "http://localhost:4300/acme/new-acct",
"revoke-cert": "http://localhost:4300/acme/revoke-cert", "revoke-cert": "http://localhost:4300/acme/revoke-cert",
"AAAAAAAAAAA": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417" "AAAAAAAAAAA": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417"
}` }`
@ -711,15 +711,15 @@ func TestRelativeDirectory(t *testing.T) {
result string result string
}{ }{
// Test '' (No host header) with no proto header // Test '' (No host header) with no proto header
{"", "", `{"key-change":"http://localhost/acme/key-change","new-reg":"http://localhost/acme/new-reg","revoke-cert":"http://localhost/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`}, {"", "", `{"key-change":"http://localhost/acme/key-change","new-account":"http://localhost/acme/new-acct","revoke-cert":"http://localhost/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`},
// Test localhost:4300 with no proto header // Test localhost:4300 with no proto header
{"localhost:4300", "", `{"key-change":"http://localhost:4300/acme/key-change","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`}, {"localhost:4300", "", `{"key-change":"http://localhost:4300/acme/key-change","new-account":"http://localhost:4300/acme/new-acct","revoke-cert":"http://localhost:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`},
// Test 127.0.0.1:4300 with no proto header // Test 127.0.0.1:4300 with no proto header
{"127.0.0.1:4300", "", `{"key-change":"http://127.0.0.1:4300/acme/key-change","new-reg":"http://127.0.0.1:4300/acme/new-reg","revoke-cert":"http://127.0.0.1:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`}, {"127.0.0.1:4300", "", `{"key-change":"http://127.0.0.1:4300/acme/key-change","new-account":"http://127.0.0.1:4300/acme/new-acct","revoke-cert":"http://127.0.0.1:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`},
// Test localhost:4300 with HTTP proto header // Test localhost:4300 with HTTP proto header
{"localhost:4300", "http", `{"key-change":"http://localhost:4300/acme/key-change","new-reg":"http://localhost:4300/acme/new-reg","revoke-cert":"http://localhost:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`}, {"localhost:4300", "http", `{"key-change":"http://localhost:4300/acme/key-change","new-account":"http://localhost:4300/acme/new-acct","revoke-cert":"http://localhost:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`},
// Test localhost:4300 with HTTPS proto header // Test localhost:4300 with HTTPS proto header
{"localhost:4300", "https", `{"key-change":"https://localhost:4300/acme/key-change","new-reg":"https://localhost:4300/acme/new-reg","revoke-cert":"https://localhost:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`}, {"localhost:4300", "https", `{"key-change":"https://localhost:4300/acme/key-change","new-account":"https://localhost:4300/acme/new-acct","revoke-cert":"https://localhost:4300/acme/revoke-cert","AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417","meta":{"terms-of-service": "http://example.invalid/terms"}}`},
} }
for _, tt := range dirTests { for _, tt := range dirTests {
@ -770,18 +770,13 @@ func TestHTTPMethods(t *testing.T) {
Allowed: getOnly, Allowed: getOnly,
}, },
{ {
Name: "NewReg path should be POST only", Name: "NewAcct path should be POST only",
Path: newRegPath, Path: newAcctPath,
Allowed: postOnly, Allowed: postOnly,
}, },
{ {
Name: "NewReg path should be POST only", Name: "Acct path should be POST only",
Path: newRegPath, Path: acctPath,
Allowed: postOnly,
},
{
Name: "Reg path should be POST only",
Path: regPath,
Allowed: postOnly, Allowed: postOnly,
}, },
{ {
@ -1000,12 +995,12 @@ func TestBadNonce(t *testing.T) {
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
result, err := signer.Sign([]byte(`{"contact":["mailto:person@mail.com"],"agreement":"` + agreementURL + `"}`)) result, err := signer.Sign([]byte(`{"contact":["mailto:person@mail.com"],"agreement":"` + agreementURL + `"}`))
test.AssertNotError(t, err, "Failed to sign body") test.AssertNotError(t, err, "Failed to sign body")
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter, wfe.NewAccount(ctx, newRequestEvent(), responseWriter,
makePostRequestWithPath("nonce", result.FullSerialize())) makePostRequestWithPath("nonce", result.FullSerialize()))
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"JWS has no anti-replay nonce","status":400}`) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"urn:acme:error:badNonce","detail":"JWS has no anti-replay nonce","status":400}`)
} }
func TestNewECDSARegistration(t *testing.T) { func TestNewECDSAAccount(t *testing.T) {
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
// E1 always exists; E2 never exists // E1 always exists; E2 never exists
@ -1013,25 +1008,25 @@ func TestNewECDSARegistration(t *testing.T) {
_, ok := key.(*ecdsa.PrivateKey) _, ok := key.(*ecdsa.PrivateKey)
test.Assert(t, ok, "Couldn't load ECDSA key") test.Assert(t, ok, "Couldn't load ECDSA key")
payload := `{"resource":"new-reg","contact":["mailto:person@mail.com"],"agreement":"` + agreementURL + `"}` payload := `{"contact":["mailto:person@mail.com"],"agreement":"` + agreementURL + `"}`
path := "new-reg" path := newAcctPath
signedURL := fmt.Sprintf("http://localhost/%s", path) signedURL := fmt.Sprintf("http://localhost%s", path)
_, _, body := signRequestEmbed(t, key, signedURL, payload, wfe.nonceService) _, _, body := signRequestEmbed(t, key, signedURL, payload, wfe.nonceService)
request := makePostRequestWithPath(path, body) request := makePostRequestWithPath(path, body)
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter, request) wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request)
var reg core.Registration var acct core.Registration
responseBody := responseWriter.Body.String() responseBody := responseWriter.Body.String()
err := json.Unmarshal([]byte(responseBody), &reg) err := json.Unmarshal([]byte(responseBody), &acct)
test.AssertNotError(t, err, "Couldn't unmarshal returned registration object") test.AssertNotError(t, err, "Couldn't unmarshal returned account object")
test.Assert(t, len(*reg.Contact) >= 1, "No contact field in registration") test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account")
test.AssertEquals(t, (*reg.Contact)[0], "mailto:person@mail.com") test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com")
test.AssertEquals(t, reg.Agreement, "http://example.invalid/terms") test.AssertEquals(t, acct.Agreement, "http://example.invalid/terms")
test.AssertEquals(t, reg.InitialIP.String(), "1.1.1.1") test.AssertEquals(t, acct.InitialIP.String(), "1.1.1.1")
test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/reg/0") test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/0")
key = loadKey(t, []byte(testE1KeyPrivatePEM)) key = loadKey(t, []byte(testE1KeyPrivatePEM))
_, ok = key.(*ecdsa.PrivateKey) _, ok = key.(*ecdsa.PrivateKey)
@ -1043,23 +1038,23 @@ func TestNewECDSARegistration(t *testing.T) {
// Reset the body and status code // Reset the body and status code
responseWriter = httptest.NewRecorder() responseWriter = httptest.NewRecorder()
// POST, Valid JSON, Key already in use // POST, Valid JSON, Key already in use
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter, request) wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request)
responseBody = responseWriter.Body.String() responseBody = responseWriter.Body.String()
test.AssertUnmarshaledEquals(t, responseBody, `{"type":"urn:acme:error:malformed","detail":"Registration key is already in use","status":409}`) test.AssertUnmarshaledEquals(t, responseBody, `{"type":"urn:acme:error:malformed","detail":"Account key is already in use","status":409}`)
test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/reg/3") test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/3")
test.AssertEquals(t, responseWriter.Code, 409) test.AssertEquals(t, responseWriter.Code, 409)
} }
// Test that the WFE handling of the "empty update" POST is correct. The ACME // Test that the WFE handling of the "empty update" POST is correct. The ACME
// spec describes how when clients wish to query the server for information // spec describes how when clients wish to query the server for information
// about a registration an empty registration update should be sent, and // about an account an empty account update should be sent, and
// a populated reg object will be returned. // a populated acct object will be returned.
func TestEmptyRegistration(t *testing.T) { func TestEmptyAccount(t *testing.T) {
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
// Test Key 1 is mocked in the mock StorageAuthority used in setupWFE to // Test Key 1 is mocked in the mock StorageAuthority used in setupWFE to
// return a populated registration for GetRegistrationByKey when test key 1 is // return a populated account for GetRegistrationByKey when test key 1 is
// used. // used.
key := loadKey(t, []byte(test1KeyPrivatePEM)) key := loadKey(t, []byte(test1KeyPrivatePEM))
_, ok := key.(*rsa.PrivateKey) _, ok := key.(*rsa.PrivateKey)
@ -1071,8 +1066,8 @@ func TestEmptyRegistration(t *testing.T) {
_, _, body := signRequestKeyID(t, 1, key, signedURL, payload, wfe.nonceService) _, _, body := signRequestKeyID(t, 1, key, signedURL, payload, wfe.nonceService)
request := makePostRequestWithPath(path, body) request := makePostRequestWithPath(path, body)
// Send a registration update with the trivial body // Send an account update with the trivial body
wfe.Registration( wfe.Account(
ctx, ctx,
newRequestEvent(), newRequestEvent(),
responseWriter, responseWriter,
@ -1082,44 +1077,44 @@ func TestEmptyRegistration(t *testing.T) {
// There should be no error // There should be no error
test.AssertNotContains(t, responseBody, "urn:acme:error") test.AssertNotContains(t, responseBody, "urn:acme:error")
// We should get back a populated Registration // We should get back a populated Account
var reg core.Registration var acct core.Registration
err := json.Unmarshal([]byte(responseBody), &reg) err := json.Unmarshal([]byte(responseBody), &acct)
test.AssertNotError(t, err, "Couldn't unmarshal returned registration object") test.AssertNotError(t, err, "Couldn't unmarshal returned account object")
test.Assert(t, len(*reg.Contact) >= 1, "No contact field in registration") test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account")
test.AssertEquals(t, (*reg.Contact)[0], "mailto:person@mail.com") test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com")
test.AssertEquals(t, reg.Agreement, "http://example.invalid/terms") test.AssertEquals(t, acct.Agreement, "http://example.invalid/terms")
responseWriter.Body.Reset() responseWriter.Body.Reset()
} }
func TestNewRegistration(t *testing.T) { func TestNewAccount(t *testing.T) {
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
mux := wfe.Handler() mux := wfe.Handler()
key := loadKey(t, []byte(test2KeyPrivatePEM)) key := loadKey(t, []byte(test2KeyPrivatePEM))
_, ok := key.(*rsa.PrivateKey) _, ok := key.(*rsa.PrivateKey)
test.Assert(t, ok, "Couldn't load test2 key") test.Assert(t, ok, "Couldn't load test2 key")
path := newRegPath path := newAcctPath
signedURL := fmt.Sprintf("http://localhost%s", newRegPath) signedURL := fmt.Sprintf("http://localhost%s", path)
wrongAgreementReg := `{"contact":["mailto:person@mail.com"],"agreement":"https://letsencrypt.org/im-bad"}` wrongAgreementAcct := `{"contact":["mailto:person@mail.com"],"agreement":"https://letsencrypt.org/im-bad"}`
// A reg with the wrong agreement URL // An acct with the wrong agreement URL
_, _, wrongAgreementBody := signRequestEmbed(t, key, signedURL, wrongAgreementReg, wfe.nonceService) _, _, wrongAgreementBody := signRequestEmbed(t, key, signedURL, wrongAgreementAcct, wfe.nonceService)
// A non-JSON payload // A non-JSON payload
_, _, fooBody := signRequestEmbed(t, key, signedURL, `foo`, wfe.nonceService) _, _, fooBody := signRequestEmbed(t, key, signedURL, `foo`, wfe.nonceService)
type newRegErrorTest struct { type newAcctErrorTest struct {
r *http.Request r *http.Request
respBody string respBody string
} }
regErrTests := []newRegErrorTest{ acctErrTests := []newAcctErrorTest{
// POST, but no body. // POST, but no body.
{ {
&http.Request{ &http.Request{
Method: "POST", Method: "POST",
URL: mustParseURL(newRegPath), URL: mustParseURL(newAcctPath),
Header: map[string][]string{ Header: map[string][]string{
"Content-Length": {"0"}, "Content-Length": {"0"},
}, },
@ -1129,29 +1124,29 @@ func TestNewRegistration(t *testing.T) {
// POST, but body that isn't valid JWS // POST, but body that isn't valid JWS
{ {
makePostRequestWithPath(newRegPath, "hi"), makePostRequestWithPath(newAcctPath, "hi"),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`, `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`,
}, },
// POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON. // POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON.
{ {
makePostRequestWithPath(newRegPath, fooBody), makePostRequestWithPath(newAcctPath, fooBody),
`{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`, `{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`,
}, },
// Same signed body, but payload modified by one byte, breaking signature. // Same signed body, but payload modified by one byte, breaking signature.
// should fail JWS verification. // should fail JWS verification.
{ {
makePostRequestWithPath(newRegPath, makePostRequestWithPath(newAcctPath,
`{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ","signature":"jcTdxSygm_cvD7KbXqsxgnoPApCTSkV4jolToSOd2ciRkg5W7Yl0ZKEEKwOc-dYIbQiwGiDzisyPCicwWsOUA1WSqHylKvZ3nxSMc6KtwJCW2DaOqcf0EEjy5VjiZJUrOt2c-r6b07tbn8sfOJKwlF2lsOeGi4s-rtvvkeQpAU-AWauzl9G4bv2nDUeCviAZjHx_PoUC-f9GmZhYrbDzAvXZ859ktM6RmMeD0OqPN7bhAeju2j9Gl0lnryZMtq2m0J2m1ucenQBL1g4ZkP1JiJvzd2cAz5G7Ftl2YeJJyWhqNd3qq0GVOt1P11s8PTGNaSoM0iR9QfUxT9A6jxARtg"}`), `{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ","signature":"jcTdxSygm_cvD7KbXqsxgnoPApCTSkV4jolToSOd2ciRkg5W7Yl0ZKEEKwOc-dYIbQiwGiDzisyPCicwWsOUA1WSqHylKvZ3nxSMc6KtwJCW2DaOqcf0EEjy5VjiZJUrOt2c-r6b07tbn8sfOJKwlF2lsOeGi4s-rtvvkeQpAU-AWauzl9G4bv2nDUeCviAZjHx_PoUC-f9GmZhYrbDzAvXZ859ktM6RmMeD0OqPN7bhAeju2j9Gl0lnryZMtq2m0J2m1ucenQBL1g4ZkP1JiJvzd2cAz5G7Ftl2YeJJyWhqNd3qq0GVOt1P11s8PTGNaSoM0iR9QfUxT9A6jxARtg"}`),
`{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`, `{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}`,
}, },
{ {
makePostRequestWithPath(newRegPath, wrongAgreementBody), makePostRequestWithPath(newAcctPath, wrongAgreementBody),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`, `{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [` + agreementURL + `]","status":400}`,
}, },
} }
for _, rt := range regErrTests { for _, rt := range acctErrTests {
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
mux.ServeHTTP(responseWriter, rt.r) mux.ServeHTTP(responseWriter, rt.r)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), rt.respBody) test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), rt.respBody)
@ -1163,20 +1158,20 @@ func TestNewRegistration(t *testing.T) {
_, _, body := signRequestEmbed(t, key, signedURL, payload, wfe.nonceService) _, _, body := signRequestEmbed(t, key, signedURL, payload, wfe.nonceService)
request := makePostRequestWithPath(path, body) request := makePostRequestWithPath(path, body)
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter, request) wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request)
var reg core.Registration var acct core.Registration
responseBody := responseWriter.Body.String() responseBody := responseWriter.Body.String()
err := json.Unmarshal([]byte(responseBody), &reg) err := json.Unmarshal([]byte(responseBody), &acct)
test.AssertNotError(t, err, "Couldn't unmarshal returned registration object") test.AssertNotError(t, err, "Couldn't unmarshal returned account object")
test.Assert(t, len(*reg.Contact) >= 1, "No contact field in registration") test.Assert(t, len(*acct.Contact) >= 1, "No contact field in account")
test.AssertEquals(t, (*reg.Contact)[0], "mailto:person@mail.com") test.AssertEquals(t, (*acct.Contact)[0], "mailto:person@mail.com")
test.AssertEquals(t, reg.Agreement, "http://example.invalid/terms") test.AssertEquals(t, acct.Agreement, "http://example.invalid/terms")
test.AssertEquals(t, reg.InitialIP.String(), "1.1.1.1") test.AssertEquals(t, acct.InitialIP.String(), "1.1.1.1")
test.AssertEquals( test.AssertEquals(
t, responseWriter.Header().Get("Location"), t, responseWriter.Header().Get("Location"),
"http://localhost/acme/reg/0") "http://localhost/acme/acct/0")
links := responseWriter.Header()["Link"] links := responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true) test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
@ -1191,13 +1186,13 @@ func TestNewRegistration(t *testing.T) {
_, _, body = signRequestEmbed(t, key, signedURL, payload, wfe.nonceService) _, _, body = signRequestEmbed(t, key, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath(path, body) request = makePostRequestWithPath(path, body)
wfe.NewRegistration(ctx, newRequestEvent(), responseWriter, request) wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Registration key is already in use","status":409}`) `{"type":"urn:acme:error:malformed","detail":"Account key is already in use","status":409}`)
test.AssertEquals( test.AssertEquals(
t, responseWriter.Header().Get("Location"), t, responseWriter.Header().Get("Location"),
"http://localhost/acme/reg/1") "http://localhost/acme/acct/1")
test.AssertEquals(t, responseWriter.Code, 409) test.AssertEquals(t, responseWriter.Code, 409)
} }
@ -1234,7 +1229,7 @@ func contains(s []string, e string) bool {
return false return false
} }
func TestRegistration(t *testing.T) { func TestAccount(t *testing.T) {
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
mux := wfe.Handler() mux := wfe.Handler()
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
@ -1242,7 +1237,7 @@ func TestRegistration(t *testing.T) {
// Test GET proper entry returns 405 // Test GET proper entry returns 405
mux.ServeHTTP(responseWriter, &http.Request{ mux.ServeHTTP(responseWriter, &http.Request{
Method: "GET", Method: "GET",
URL: mustParseURL(regPath), URL: mustParseURL(acctPath),
}) })
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
@ -1250,7 +1245,7 @@ func TestRegistration(t *testing.T) {
responseWriter.Body.Reset() responseWriter.Body.Reset()
// Test POST invalid JSON // Test POST invalid JSON
wfe.Registration(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("2", "invalid")) wfe.Account(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("2", "invalid"))
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`)
@ -1260,68 +1255,68 @@ func TestRegistration(t *testing.T) {
_, ok := key.(*rsa.PrivateKey) _, ok := key.(*rsa.PrivateKey)
test.Assert(t, ok, "Couldn't load RSA key") test.Assert(t, ok, "Couldn't load RSA key")
signedURL := fmt.Sprintf("http://localhost%s%d", regPath, 102) signedURL := fmt.Sprintf("http://localhost%s%d", acctPath, 102)
path := fmt.Sprintf("%s%d", regPath, 102) path := fmt.Sprintf("%s%d", acctPath, 102)
payload := `{"resource":"reg","agreement":"` + agreementURL + `"}` payload := `{"agreement":"` + agreementURL + `"}`
// ID 102 is used by the mock for missing reg // ID 102 is used by the mock for missing acct
_, _, body := signRequestKeyID(t, 102, nil, signedURL, payload, wfe.nonceService) _, _, body := signRequestKeyID(t, 102, nil, signedURL, payload, wfe.nonceService)
request := makePostRequestWithPath(path, body) request := makePostRequestWithPath(path, body)
// Test POST valid JSON but key is not registered // Test POST valid JSON but key is not registered
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:ietf:params:acme:error:accountDoesNotExist","detail":"Account \"http://localhost/acme/reg/102\" not found","status":400}`) `{"type":"urn:ietf:params:acme:error:accountDoesNotExist","detail":"Account \"http://localhost/acme/acct/102\" not found","status":400}`)
responseWriter.Body.Reset() responseWriter.Body.Reset()
key = loadKey(t, []byte(test1KeyPrivatePEM)) key = loadKey(t, []byte(test1KeyPrivatePEM))
_, ok = key.(*rsa.PrivateKey) _, ok = key.(*rsa.PrivateKey)
test.Assert(t, ok, "Couldn't load RSA key") test.Assert(t, ok, "Couldn't load RSA key")
// Test POST valid JSON with registration up in the mock (with incorrect agreement URL) // Test POST valid JSON with account up in the mock (with incorrect agreement URL)
payload = `{"agreement":"https://letsencrypt.org/im-bad"}` payload = `{"agreement":"https://letsencrypt.org/im-bad"}`
path = "1" path = "1"
signedURL = "http://localhost/1" signedURL = "http://localhost/1"
_, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath(path, body) request = makePostRequestWithPath(path, body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]","status":400}`) `{"type":"urn:acme:error:malformed","detail":"Provided agreement URL [https://letsencrypt.org/im-bad] does not match current agreement URL [`+agreementURL+`]","status":400}`)
responseWriter.Body.Reset() responseWriter.Body.Reset()
// Test POST valid JSON with registration up in the mock (with correct agreement URL) // Test POST valid JSON with account up in the mock (with correct agreement URL)
payload = `{"resource":"reg","agreement":"` + agreementURL + `"}` payload = `{"agreement":"` + agreementURL + `"}`
_, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath(path, body) request = makePostRequestWithPath(path, body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error") test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
links := responseWriter.Header()["Link"] links := responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true) test.AssertEquals(t, contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true)
responseWriter.Body.Reset() responseWriter.Body.Reset()
// Test POST valid JSON with garbage in URL but valid registration ID // Test POST valid JSON with garbage in URL but valid account ID
payload = `{"resource":"reg","agreement":"` + agreementURL + `"}` payload = `{"agreement":"` + agreementURL + `"}`
signedURL = "http://localhost/a/bunch/of/garbage/1" signedURL = "http://localhost/a/bunch/of/garbage/1"
_, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath("/a/bunch/of/garbage/1", body) request = makePostRequestWithPath("/a/bunch/of/garbage/1", body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertContains(t, responseWriter.Body.String(), "400") test.AssertContains(t, responseWriter.Body.String(), "400")
test.AssertContains(t, responseWriter.Body.String(), "urn:acme:error:malformed") test.AssertContains(t, responseWriter.Body.String(), "urn:acme:error:malformed")
responseWriter.Body.Reset() responseWriter.Body.Reset()
// Test POST valid JSON with registration up in the mock (with old agreement URL) // Test POST valid JSON with account up in the mock (with old agreement URL)
responseWriter.HeaderMap = http.Header{} responseWriter.HeaderMap = http.Header{}
wfe.SubscriberAgreementURL = "http://example.invalid/new-terms" wfe.SubscriberAgreementURL = "http://example.invalid/new-terms"
payload = `{"resource":"reg","agreement":"` + agreementURL + `"}` payload = `{"agreement":"` + agreementURL + `"}`
signedURL = "http://localhost/1" signedURL = "http://localhost/1"
_, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath(path, body) request = makePostRequestWithPath(path, body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error") test.AssertNotContains(t, responseWriter.Body.String(), "urn:acme:error")
links = responseWriter.Header()["Link"] links = responseWriter.Header()["Link"]
test.AssertEquals(t, contains(links, "<http://example.invalid/new-terms>;rel=\"terms-of-service\""), true) test.AssertEquals(t, contains(links, "<http://example.invalid/new-terms>;rel=\"terms-of-service\""), true)
@ -1517,7 +1512,7 @@ func TestHeaderBoulderRequester(t *testing.T) {
test.Assert(t, ok, "Failed to load test 1 RSA key") test.Assert(t, ok, "Failed to load test 1 RSA key")
payload := `{"agreement":"` + agreementURL + `"}` payload := `{"agreement":"` + agreementURL + `"}`
path := fmt.Sprintf("%s%d", regPath, 1) path := fmt.Sprintf("%s%d", acctPath, 1)
signedURL := fmt.Sprintf("http://localhost%s", path) signedURL := fmt.Sprintf("http://localhost%s", path)
_, _, body := signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body := signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request := makePostRequestWithPath(path, body) request := makePostRequestWithPath(path, body)
@ -1575,7 +1570,7 @@ func TestDeactivateAuthorization(t *testing.T) {
}`) }`)
} }
func TestDeactivateRegistration(t *testing.T) { func TestDeactivateAccount(t *testing.T) {
responseWriter := httptest.NewRecorder() responseWriter := httptest.NewRecorder()
wfe, _ := setupWFE(t) wfe, _ := setupWFE(t)
@ -1586,7 +1581,7 @@ func TestDeactivateRegistration(t *testing.T) {
_, _, body := signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body := signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request := makePostRequestWithPath(path, body) request := makePostRequestWithPath(path, body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{"type": "urn:acme:error:malformed","detail": "Invalid value provided for status field","status": 400}`) `{"type": "urn:acme:error:malformed","detail": "Invalid value provided for status field","status": 400}`)
@ -1596,7 +1591,7 @@ func TestDeactivateRegistration(t *testing.T) {
_, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath(path, body) request = makePostRequestWithPath(path, body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{ `{
@ -1619,7 +1614,7 @@ func TestDeactivateRegistration(t *testing.T) {
payload = `{"status":"deactivated", "contact":[]}` payload = `{"status":"deactivated", "contact":[]}`
_, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService) _, _, body = signRequestKeyID(t, 1, nil, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath(path, body) request = makePostRequestWithPath(path, body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
`{ `{
@ -1649,7 +1644,7 @@ func TestDeactivateRegistration(t *testing.T) {
_, _, body = signRequestKeyID(t, 3, key, signedURL, payload, wfe.nonceService) _, _, body = signRequestKeyID(t, 3, key, signedURL, payload, wfe.nonceService)
request = makePostRequestWithPath(path, body) request = makePostRequestWithPath(path, body)
wfe.Registration(ctx, newRequestEvent(), responseWriter, request) wfe.Account(ctx, newRequestEvent(), responseWriter, request)
test.AssertUnmarshaledEquals(t, test.AssertUnmarshaledEquals(t,
responseWriter.Body.String(), responseWriter.Body.String(),
@ -1774,7 +1769,7 @@ func TestKeyRollover(t *testing.T) {
Payload: `{"newKey":` + string(newJWKJSON) + `}`, Payload: `{"newKey":` + string(newJWKJSON) + `}`,
ExpectedResponse: `{ ExpectedResponse: `{
"type": "urn:acme:error:malformed", "type": "urn:acme:error:malformed",
"detail": "Inner key rollover request specified Account \"\", but outer JWS has Key ID \"http://localhost/acme/reg/1\"", "detail": "Inner key rollover request specified Account \"\", but outer JWS has Key ID \"http://localhost/acme/acct/1\"",
"status": 400 "status": 400
}`, }`,
NewKey: newKey, NewKey: newKey,
@ -1782,7 +1777,7 @@ func TestKeyRollover(t *testing.T) {
}, },
{ {
Name: "Missing new key from inner payload", Name: "Missing new key from inner payload",
Payload: `{"account":"http://localhost/acme/reg/1"}`, Payload: `{"account":"http://localhost/acme/acct/1"}`,
ExpectedResponse: `{ ExpectedResponse: `{
"type": "urn:acme:error:malformed", "type": "urn:acme:error:malformed",
"detail": "Inner JWS does not verify with specified new key", "detail": "Inner JWS does not verify with specified new key",
@ -1792,7 +1787,7 @@ func TestKeyRollover(t *testing.T) {
}, },
{ {
Name: "New key is the same as the old key", Name: "New key is the same as the old key",
Payload: `{"newKey":{"kty":"RSA","n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ","e":"AQAB"},"account":"http://localhost/acme/reg/1"}`, Payload: `{"newKey":{"kty":"RSA","n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ","e":"AQAB"},"account":"http://localhost/acme/acct/1"}`,
ExpectedResponse: `{ ExpectedResponse: `{
"type": "urn:acme:error:malformed", "type": "urn:acme:error:malformed",
"detail": "New key specified by rollover request is the same as the old key", "detail": "New key specified by rollover request is the same as the old key",
@ -1802,7 +1797,7 @@ func TestKeyRollover(t *testing.T) {
}, },
{ {
Name: "Inner JWS signed by the wrong key", Name: "Inner JWS signed by the wrong key",
Payload: `{"newKey":` + string(newJWKJSON) + `,"account":"http://localhost/acme/reg/1"}`, Payload: `{"newKey":` + string(newJWKJSON) + `,"account":"http://localhost/acme/acct/1"}`,
ExpectedResponse: `{ ExpectedResponse: `{
"type": "urn:acme:error:malformed", "type": "urn:acme:error:malformed",
"detail": "Inner JWS does not verify with specified new key", "detail": "Inner JWS does not verify with specified new key",
@ -1812,7 +1807,7 @@ func TestKeyRollover(t *testing.T) {
}, },
{ {
Name: "Valid key rollover request", Name: "Valid key rollover request",
Payload: `{"newKey":` + string(newJWKJSON) + `,"account":"http://localhost/acme/reg/1"}`, Payload: `{"newKey":` + string(newJWKJSON) + `,"account":"http://localhost/acme/acct/1"}`,
ExpectedResponse: `{ ExpectedResponse: `{
"id": 1, "id": 1,
"key": ` + string(newJWKJSON) + `, "key": ` + string(newJWKJSON) + `,