Repository: letsencrypt/boulder

Issue:
Remove OCSP MustStaple allowlist
We have configured the OCSP MustStaple allowlist to be empty in all deployment environments. We can now remove the concept of the allowlist, and unconditionally reject all finalize CSRs which request that extension.

Hints from issue comments:

Solution patch:
diff --git a/.golangci.yml b/.golangci.yml
index e99a71e83..a14e9f6df 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,6 +1,8 @@
 linters:
   disable-all: true
   enable:
+    - asciicheck
+    - bidichk
     - errcheck
     - gofmt
     - gosec
@@ -9,6 +11,7 @@ linters:
     - ineffassign
     - misspell
     - nolintlint
+    - spancheck
     - sqlclosecheck
     - typecheck
     - unconvert
diff --git a/bdns/dns.go b/bdns/dns.go
index 1ad08f440..ec502fe64 100644
--- a/bdns/dns.go
+++ b/bdns/dns.go
@@ -391,7 +391,6 @@ func (dnsClient *impl) exchangeOne(ctx context.Context, hostname string, qtype u
 			return
 		}
 	}
-
 }
 
 // isTLD returns a simplified view of whether something is a TLD: does it have
diff --git a/cmd/boulder-ca/main.go b/cmd/boulder-ca/main.go
index e579a2ba6..156b4fe90 100644
--- a/cmd/boulder-ca/main.go
+++ b/cmd/boulder-ca/main.go
@@ -164,8 +164,9 @@ func main() {
 	metrics := ca.NewCAMetrics(scope)
 
 	cmd.FailOnError(c.PA.CheckChallenges(), "Invalid PA configuration")
+	cmd.FailOnError(c.PA.CheckIdentifiers(), "Invalid PA configuration")
 
-	pa, err := policy.New(c.PA.Challenges, logger)
+	pa, err := policy.New(c.PA.Identifiers, c.PA.Challenges, logger)
 	cmd.FailOnError(err, "Couldn't create PA")
 
 	if c.CA.HostnamePolicyFile == "" {
diff --git a/cmd/boulder-ra/main.go b/cmd/boulder-ra/main.go
index 2025de998..9aa809e42 100644
--- a/cmd/boulder-ra/main.go
+++ b/cmd/boulder-ra/main.go
@@ -6,7 +6,6 @@ import (
 	"os"
 
 	akamaipb "github.com/letsencrypt/boulder/akamai/proto"
-	"github.com/letsencrypt/boulder/allowlist"
 	capb "github.com/letsencrypt/boulder/ca/proto"
 	"github.com/letsencrypt/boulder/cmd"
 	"github.com/letsencrypt/boulder/config"
@@ -95,11 +94,13 @@ type Config struct {
 		// default.
 		DefaultProfileName string `validate:"required"`
 
-		// MustStapleAllowList specifies the path to a YAML file containing a
+		// MustStapleAllowList specified the path to a YAML file containing a
 		// list of account IDs permitted to request certificates with the OCSP
-		// Must-Staple extension. If no path is specified, the extension is
-		// permitted for all accounts. If the file exists but is empty, the
-		// extension is disabled for all accounts.
+		// Must-Staple extension.
+		//
+		// Deprecated: This field no longer has any effect, all Must-Staple requests
+		// are rejected.
+		// TODO(#8177): Remove this field.
 		MustStapleAllowList string `validate:"omitempty"`
 
 		// GoodKey is an embedded config stanza for the goodkey library.
@@ -166,8 +167,9 @@ func main() {
 
 	// Validate PA config and set defaults if needed
 	cmd.FailOnError(c.PA.CheckChallenges(), "Invalid PA configuration")
+	cmd.FailOnError(c.PA.CheckIdentifiers(), "Invalid PA configuration")
 
-	pa, err := policy.New(c.PA.Challenges, logger)
+	pa, err := policy.New(c.PA.Identifiers, c.PA.Challenges, logger)
 	cmd.FailOnError(err, "Couldn't create PA")
 
 	if c.RA.HostnamePolicyFile == "" {
@@ -253,14 +255,6 @@ func main() {
 	validationProfiles, err := ra.NewValidationProfiles(c.RA.DefaultProfileName, c.RA.ValidationProfiles)
 	cmd.FailOnError(err, "Failed to load validation profiles")
 
-	var mustStapleAllowList *allowlist.List[int64]
-	if c.RA.MustStapleAllowList != "" {
-		data, err := os.ReadFile(c.RA.MustStapleAllowList)
-		cmd.FailOnError(err, "Failed to read allow list for Must-Staple extension")
-		mustStapleAllowList, err = allowlist.NewFromYAML[int64](data)
-		cmd.FailOnError(err, "Failed to parse allow list for Must-Staple extension")
-	}
-
 	if features.Get().AsyncFinalize && c.RA.FinalizeTimeout.Duration == 0 {
 		cmd.Fail("finalizeTimeout must be supplied when AsyncFinalize feature is enabled")
 	}
@@ -293,7 +287,6 @@ func main() {
 		txnBuilder,
 		c.RA.MaxNames,
 		validationProfiles,
-		mustStapleAllowList,
 		pubc,
 		c.RA.FinalizeTimeout.Duration,
 		ctp,
diff --git a/cmd/cert-checker/main.go b/cmd/cert-checker/main.go
index 0d78c3f7a..974308ab9 100644
--- a/cmd/cert-checker/main.go
+++ b/cmd/cert-checker/main.go
@@ -562,6 +562,7 @@ func main() {
 
 	// Validate PA config and set defaults if needed.
 	cmd.FailOnError(config.PA.CheckChallenges(), "Invalid PA configuration")
+	cmd.FailOnError(config.PA.CheckIdentifiers(), "Invalid PA configuration")
 
 	kp, err := sagoodkey.NewPolicy(&config.CertChecker.GoodKey, nil)
 	cmd.FailOnError(err, "Unable to create key policy")
@@ -575,7 +576,7 @@ func main() {
 	})
 	prometheus.DefaultRegisterer.MustRegister(checkerLatency)
 
-	pa, err := policy.New(config.PA.Challenges, logger)
+	pa, err := policy.New(config.PA.Identifiers, config.PA.Challenges, logger)
 	cmd.FailOnError(err, "Failed to create PA")
 
 	err = pa.LoadHostnamePolicyFile(config.CertChecker.HostnamePolicyFile)
diff --git a/cmd/config.go b/cmd/config.go
index 3072f206c..f8b6b847f 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/letsencrypt/boulder/config"
 	"github.com/letsencrypt/boulder/core"
+	"github.com/letsencrypt/boulder/identifier"
 )
 
 // PasswordConfig contains a path to a file containing a password.
@@ -99,8 +100,9 @@ type SMTPConfig struct {
 // database, what policies it should enforce, and what challenges
 // it should offer.
 type PAConfig struct {
-	DBConfig   `validate:"-"`
-	Challenges map[core.AcmeChallenge]bool `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01,endkeys"`
+	DBConfig    `validate:"-"`
+	Challenges  map[core.AcmeChallenge]bool        `validate:"omitempty,dive,keys,oneof=http-01 dns-01 tls-alpn-01,endkeys"`
+	Identifiers map[identifier.IdentifierType]bool `validate:"omitempty,dive,keys,oneof=dns ip,endkeys"`
 }
 
 // CheckChallenges checks whether the list of challenges in the PA config
@@ -117,6 +119,17 @@ func (pc PAConfig) CheckChallenges() error {
 	return nil
 }
 
+// CheckIdentifiers checks whether the list of identifiers in the PA config
+// actually contains valid identifier type names
+func (pc PAConfig) CheckIdentifiers() error {
+	for i := range pc.Identifiers {
+		if !i.IsValid() {
+			return fmt.Errorf("invalid identifier type in PA config: %s", i)
+		}
+	}
+	return nil
+}
+
 // HostnamePolicyConfig specifies a file from which to load a policy regarding
 // what hostnames to issue for.
 type HostnamePolicyConfig struct {
diff --git a/cmd/reversed-hostname-checker/main.go b/cmd/reversed-hostname-checker/main.go
index 1e60f618a..9d7a33f37 100644
--- a/cmd/reversed-hostname-checker/main.go
+++ b/cmd/reversed-hostname-checker/main.go
@@ -40,7 +40,7 @@ func main() {
 	scanner := bufio.NewScanner(input)
 	logger := cmd.NewLogger(cmd.SyslogConfig{StdoutLevel: 7})
 	logger.Info(cmd.VersionString())
-	pa, err := policy.New(nil, logger)
+	pa, err := policy.New(nil, nil, logger)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/identifier/identifier.go b/identifier/identifier.go
index 6c9a86f18..e6ed65e1c 100644
--- a/identifier/identifier.go
+++ b/identifier/identifier.go
@@ -30,6 +30,16 @@ const (
 	TypeIP = IdentifierType("ip")
 )
 
+// IsValid tests whether the identifier type is known
+func (i IdentifierType) IsValid() bool {
+	switch i {
+	case TypeDNS, TypeIP:
+		return true
+	default:
+		return false
+	}
+}
+
 // ACMEIdentifier is a struct encoding an identifier that can be validated. The
 // protocol allows for different types of identifier to be supported (DNS
 // names, IP addresses, etc.), but currently we only support RFC 8555 DNS type
diff --git a/mocks/sa.go b/mocks/sa.go
index dd50ebe66..a982a8047 100644
--- a/mocks/sa.go
+++ b/mocks/sa.go
@@ -220,7 +220,6 @@ func (sa *StorageAuthorityReadOnly) GetCertificateStatus(_ context.Context, req
 
 func (sa *StorageAuthorityReadOnly) SetCertificateStatusReady(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*emptypb.Empty, error) {
 	return nil, status.Error(codes.Unimplemented, "unimplemented mock")
-
 }
 
 // GetRevocationStatus is a mock
diff --git a/policy/pa.go b/policy/pa.go
index cf9e619a0..661a6b6bc 100644
--- a/policy/pa.go
+++ b/policy/pa.go
@@ -16,6 +16,7 @@ import (
 	"golang.org/x/net/idna"
 	"golang.org/x/text/unicode/norm"
 
+	"github.com/letsencrypt/boulder/bdns"
 	"github.com/letsencrypt/boulder/core"
 	berrors "github.com/letsencrypt/boulder/errors"
 	"github.com/letsencrypt/boulder/iana"
@@ -33,14 +34,24 @@ type AuthorityImpl struct {
 	wildcardExactBlocklist map[string]bool
 	blocklistMu            sync.RWMutex
 
-	enabledChallenges map[core.AcmeChallenge]bool
+	enabledChallenges  map[core.AcmeChallenge]bool
+	enabledIdentifiers map[identifier.IdentifierType]bool
 }
 
 // New constructs a Policy Authority.
-func New(challengeTypes map[core.AcmeChallenge]bool, log blog.Logger) (*AuthorityImpl, error) {
+func New(identifierTypes map[identifier.IdentifierType]bool, challengeTypes map[core.AcmeChallenge]bool, log blog.Logger) (*AuthorityImpl, error) {
+	// If identifierTypes are not configured (i.e. nil), default to allowing DNS
+	// identifiers. This default is temporary, to improve deployability.
+	//
+	// TODO(#8184): Remove this default.
+	if identifierTypes == nil {
+		identifierTypes = map[identifier.IdentifierType]bool{identifier.TypeDNS: true}
+	}
+
 	return &AuthorityImpl{
-		log:               log,
-		enabledChallenges: challengeTypes,
+		log:                log,
+		enabledChallenges:  challengeTypes,
+		enabledIdentifiers: identifierTypes,
 	}, nil
 }
 
@@ -167,9 +178,11 @@ var (
 	errPolicyForbidden      = berrors.RejectedIdentifierError("The ACME server refuses to issue a certificate for this domain name, because it is forbidden by policy")
 	errInvalidDNSCharacter  = berrors.MalformedError("Domain name contains an invalid character")
 	errNameTooLong          = berrors.MalformedError("Domain name is longer than 253 bytes")
-	errIPAddress            = berrors.MalformedError("The ACME server can not issue a certificate for an IP address")
+	errIPAddressInDNS       = berrors.MalformedError("Identifier type is DNS but value is an IP address")
+	errIPInvalid            = berrors.MalformedError("IP address is invalid")
+	errIPSpecialPurpose     = berrors.MalformedError("IP address is in a special-purpose address block")
 	errTooManyLabels        = berrors.MalformedError("Domain name has more than 10 labels (parts)")
-	errEmptyName            = berrors.MalformedError("Domain name is empty")
+	errEmptyIdentifier      = berrors.MalformedError("Identifier value (name) is empty")
 	errNameEndsInDot        = berrors.MalformedError("Domain name ends in a dot")
 	errTooFewLabels         = berrors.MalformedError("Domain name needs at least one dot")
 	errLabelTooShort        = berrors.MalformedError("Domain name can not have two dots in a row")
@@ -180,7 +193,7 @@ var (
 	errMalformedWildcard    = berrors.MalformedError("Domain name contains an invalid wildcard. A wildcard is only permitted before the first dot in a domain name")
 	errICANNTLDWildcard     = berrors.MalformedError("Domain name is a wildcard for an ICANN TLD")
 	errWildcardNotSupported = berrors.MalformedError("Wildcard domain names are not supported")
-	errUnsupportedIdent     = berrors.MalformedError("invalid non-DNS type identifier")
+	errUnsupportedIdent     = berrors.MalformedError("Invalid identifier type")
 )
 
 // validNonWildcardDomain checks that a domain isn't:
@@ -198,7 +211,7 @@ var (
 // It does NOT ensure that the domain is absent from any PA blocked lists.
 func validNonWildcardDomain(domain string) error {
 	if domain == "" {
-		return errEmptyName
+		return errEmptyIdentifier
 	}
 
 	if strings.HasPrefix(domain, "*.") {
@@ -216,7 +229,7 @@ func validNonWildcardDomain(domain string) error {
 	}
 
 	if ip := net.ParseIP(domain); ip != nil {
-		return errIPAddress
+		return errIPAddressInDNS
 	}
 
 	if strings.HasSuffix(domain, ".") {
@@ -319,6 +332,34 @@ func ValidDomain(domain string) error {
 	return validNonWildcardDomain(baseDomain)
 }
 
+// validIP checks that an IP address:
+//   - isn't empty
+//   - is an IPv4 or IPv6 address
+//   - isn't in an IANA special-purpose address registry
+//
+// It does NOT ensure that the IP address is absent from any PA blocked lists.
+func validIP(ip string) error {
+	if ip == "" {
+		return errEmptyIdentifier
+	}
+
+	// Check the output of net.IP.String(), to ensure the input complied with
+	// RFC 8738, Sec. 3. ("The identifier value MUST contain the textual form of
+	// the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC 5952,
+	// Sec. 4 for IPv6.") ParseIP() will accept a non-compliant but otherwise
+	// valid string; String() will output a compliant string.
+	parsedIP := net.ParseIP(ip)
+	if parsedIP == nil || parsedIP.String() != ip {
+		return errIPInvalid
+	}
+
+	if bdns.IsReservedIP(parsedIP) {
+		return errIPSpecialPurpose
+	}
+
+	return nil
+}
+
 // forbiddenMailDomains is a map of domain names we do not allow after the
 // @ symbol in contact mailto addresses. These are frequently used when
 // copy-pasting example configurations and would not result in expiration
@@ -390,42 +431,50 @@ func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error
 
 	var subErrors []berrors.SubBoulderError
 	for _, ident := range idents {
-		if ident.Type != identifier.TypeDNS {
-			subErrors = append(subErrors, subError(ident, errUnsupportedIdent))
+		if !pa.IdentifierTypeEnabled(ident.Type) {
+			subErrors = append(subErrors, subError(ident, berrors.RejectedIdentifierError("The ACME server has disabled this identifier type")))
 			continue
 		}
 
-		if strings.Count(ident.Value, "*") > 0 {
-			// The base domain is the wildcard request with the `*.` prefix removed
-			baseDomain := strings.TrimPrefix(ident.Value, "*.")
+		// Only DNS identifiers are subject to wildcard and blocklist checks.
+		// Unsupported identifier types will have been caught by
+		// WellFormedIdentifiers().
+		//
+		// TODO(#7311): We may want to implement IP address blocklists too.
+		if ident.Type == identifier.TypeDNS {
+			if strings.Count(ident.Value, "*") > 0 {
+				// The base domain is the wildcard request with the `*.` prefix removed
+				baseDomain := strings.TrimPrefix(ident.Value, "*.")
+
+				// The base domain can't be in the wildcard exact blocklist
+				err = pa.checkWildcardHostList(baseDomain)
+				if err != nil {
+					subErrors = append(subErrors, subError(ident, err))
+					continue
+				}
+			}
 
-			// The base domain can't be in the wildcard exact blocklist
-			err = pa.checkWildcardHostList(baseDomain)
+			// For both wildcard and non-wildcard domains, check whether any parent domain
+			// name is on the regular blocklist.
+			err := pa.checkHostLists(ident.Value)
 			if err != nil {
 				subErrors = append(subErrors, subError(ident, err))
 				continue
 			}
 		}
-
-		// For both wildcard and non-wildcard domains, check whether any parent domain
-		// name is on the regular blocklist.
-		err := pa.checkHostLists(ident.Value)
-		if err != nil {
-			subErrors = append(subErrors, subError(ident, err))
-			continue
-		}
 	}
 	return combineSubErrors(subErrors)
 }
 
-// WellFormedIdentifiers returns an error if any of the provided domains do not
-// meet these criteria:
+// WellFormedIdentifiers returns an error if any of the provided identifiers do
+// not meet these criteria:
 //
+// For DNS identifiers:
 //   - MUST contains only lowercase characters, numbers, hyphens, and dots
 //   - MUST NOT have more than maxLabels labels
 //   - MUST follow the DNS hostname syntax rules in RFC 1035 and RFC 2181
 //
-// In particular, it:
+// In particular, DNS identifiers:
 //   - MUST NOT contain underscores
 //   - MUST NOT match the syntax of an IP address
 //   - MUST end in a public suffix
@@ -433,25 +482,33 @@ func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error
 //   - MUST NOT be a label-wise suffix match for a name on the block list,
 //     where comparison is case-independent (normalized to lower case)
 //
-// If a domain contains a *, we additionally require:
+// If a DNS identifier contains a *, we additionally require:
 //   - There is at most one `*` wildcard character
 //   - That the wildcard character is the leftmost label
 //   - That the wildcard label is not immediately adjacent to a top level ICANN
 //     TLD
 //
-// If multiple domains are invalid, the error will contain suberrors specific to
-// each domain.
+// For IP identifiers:
+//   - MUST match the syntax of an IP address
+//   - MUST NOT be in an IANA special-purpose address registry
+//
+// If multiple identifiers are invalid, the error will contain suberrors
+// specific to each identifier.
 func WellFormedIdentifiers(idents identifier.ACMEIdentifiers) error {
 	var subErrors []berrors.SubBoulderError
 	for _, ident := range idents {
-		// TODO(#7311): When this gets a third case for TypeIP, this will be
-		// more elegant as a switch/case.
-		if ident.Type == identifier.TypeDNS {
+		switch ident.Type {
+		case identifier.TypeDNS:
 			err := ValidDomain(ident.Value)
 			if err != nil {
 				subErrors = append(subErrors, subError(ident, err))
 			}
-		} else {
+		case identifier.TypeIP:
+			err := validIP(ident.Value)
+			if err != nil {
+				subErrors = append(subErrors, subError(ident, err))
+			}
+		default:
 			subErrors = append(subErrors, subError(ident, errUnsupportedIdent))
 		}
 	}
@@ -530,26 +587,34 @@ func (pa *AuthorityImpl) checkHostLists(domain string) error {
 // filtering can happen dynamically at request rather than being set in stone
 // at creation time.
 func (pa *AuthorityImpl) ChallengeTypesFor(ident identifier.ACMEIdentifier) ([]core.AcmeChallenge, error) {
-	// If the identifier is for a DNS wildcard name we only provide a DNS-01
-	// challenge, to comply with the BRs Sections 3.2.2.4.19 and 3.2.2.4.20
-	// stating that ACME HTTP-01 and TLS-ALPN-01 are not suitable for validating
-	// Wildcard Domains.
-	if ident.Type == identifier.TypeDNS && strings.HasPrefix(ident.Value, "*.") {
-		return []core.AcmeChallenge{core.ChallengeTypeDNS01}, nil
-	}
+	switch ident.Type {
+	case identifier.TypeDNS:
+		// If the identifier is for a DNS wildcard name we only provide a DNS-01
+		// challenge, to comply with the BRs Sections 3.2.2.4.19 and 3.2.2.4.20
+		// stating that ACME HTTP-01 and TLS-ALPN-01 are not suitable for validating
+		// Wildcard Domains.
+		if strings.HasPrefix(ident.Value, "*.") {
+			return []core.AcmeChallenge{core.ChallengeTypeDNS01}, nil
+		}
 
-	// Return all challenge types we support for non-wildcard DNS identifiers.
-	if ident.Type == identifier.TypeDNS {
+		// Return all challenge types we support for non-wildcard DNS identifiers.
 		return []core.AcmeChallenge{
 			core.ChallengeTypeHTTP01,
 			core.ChallengeTypeDNS01,
 			core.ChallengeTypeTLSALPN01,
 		}, nil
+	case identifier.TypeIP:
+		// Only HTTP-01 and TLS-ALPN-01 are suitable for IP address identifiers
+		// per RFC 8738, Sec. 4.
+		return []core.AcmeChallenge{
+			core.ChallengeTypeHTTP01,
+			core.ChallengeTypeTLSALPN01,
+		}, nil
+	default:
+		// Otherwise return an error because we don't support any challenges for this
+		// identifier type.
+		return nil, fmt.Errorf("unrecognized identifier type %q", ident.Type)
 	}
-
-	// Otherwise return an error because we don't support any challenges for this
-	// identifier type.
-	return nil, fmt.Errorf("unrecognized identifier type %q", ident.Type)
 }
 
 // ChallengeTypeEnabled returns whether the specified challenge type is enabled
@@ -583,3 +648,10 @@ func (pa *AuthorityImpl) CheckAuthzChallenges(authz *core.Authorization) error {
 
 	return nil
 }
+
+// IdentifierTypeEnabled returns whether the specified identifier type is enabled
+func (pa *AuthorityImpl) IdentifierTypeEnabled(t identifier.IdentifierType) bool {
+	pa.blocklistMu.RLock()
+	defer pa.blocklistMu.RUnlock()
+	return pa.enabledIdentifiers[t]
+}
diff --git a/ra/ra.go b/ra/ra.go
index 002c9dab4..48865efe0 100644
--- a/ra/ra.go
+++ b/ra/ra.go
@@ -79,33 +79,34 @@ type RegistrationAuthorityImpl struct {
 	PA        core.PolicyAuthority
 	publisher pubpb.PublisherClient
 
-	clk                 clock.Clock
-	log                 blog.Logger
-	keyPolicy           goodkey.KeyPolicy
-	profiles            *validationProfiles
-	mustStapleAllowList *allowlist.List[int64]
-	maxContactsPerReg   int
-	limiter             *ratelimits.Limiter
-	txnBuilder          *ratelimits.TransactionBuilder
-	finalizeTimeout     time.Duration
-	drainWG             sync.WaitGroup
+	clk               clock.Clock
+	log               blog.Logger
+	keyPolicy         goodkey.KeyPolicy
+	profiles          *validationProfiles
+	maxContactsPerReg int
+	limiter           *ratelimits.Limiter
+	txnBuilder        *ratelimits.TransactionBuilder
+	finalizeTimeout   time.Duration
+	drainWG           sync.WaitGroup
 
 	issuersByNameID map[issuance.NameID]*issuance.Certificate
 	purger          akamaipb.AkamaiPurgerClient
 
 	ctpolicy *ctpolicy.CTPolicy
 
-	ctpolicyResults           *prometheus.HistogramVec
-	revocationReasonCounter   *prometheus.CounterVec
-	namesPerCert              *prometheus.HistogramVec
-	newRegCounter             prometheus.Counter
-	recheckCAACounter         prometheus.Counter
-	newCertCounter            prometheus.Counter
-	authzAges                 *prometheus.HistogramVec
-	orderAges                 *prometheus.HistogramVec
-	inflightFinalizes         prometheus.Gauge
-	certCSRMismatch           prometheus.Counter
-	pauseCounter              *prometheus.CounterVec
+	ctpolicyResults         *prometheus.HistogramVec
+	revocationReasonCounter *prometheus.CounterVec
+	namesPerCert            *prometheus.HistogramVec
+	newRegCounter           prometheus.Counter
+	recheckCAACounter       prometheus.Counter
+	newCertCounter          prometheus.Counter
+	authzAges               *prometheus.HistogramVec
+	orderAges               *prometheus.HistogramVec
+	inflightFinalizes       prometheus.Gauge
+	certCSRMismatch         prometheus.Counter
+	pauseCounter            *prometheus.CounterVec
+	// TODO(#8177): Remove once the rate of requests failing to finalize due to
+	// requesting Must-Staple has diminished.
 	mustStapleRequestsCounter *prometheus.CounterVec
 	// TODO(#7966): Remove once the rate of registrations with contacts has been
 	// determined.
@@ -125,7 +126,6 @@ func NewRegistrationAuthorityImpl(
 	txnBuilder *ratelimits.TransactionBuilder,
 	maxNames int,
 	profiles *validationProfiles,
-	mustStapleAllowList *allowlist.List[int64],
 	pubc pubpb.PublisherClient,
 	finalizeTimeout time.Duration,
 	ctp *ctpolicy.CTPolicy,
@@ -244,7 +244,6 @@ func NewRegistrationAuthorityImpl(
 		clk:                        clk,
 		log:                        logger,
 		profiles:                   profiles,
-		mustStapleAllowList:        mustStapleAllowList,
 		maxContactsPerReg:          maxContactsPerReg,
 		keyPolicy:                  keyPolicy,
 		limiter:                    limiter,
@@ -1086,16 +1085,11 @@ func (ra *RegistrationAuthorityImpl) validateFinalizeRequest(
 		return nil, berrors.BadCSRError("unable to parse CSR: %s", err.Error())
 	}
 
-	if ra.mustStapleAllowList != nil && issuance.ContainsMustStaple(csr.Extensions) {
-		if !ra.mustStapleAllowList.Contains(req.Order.RegistrationID) {
-			ra.mustStapleRequestsCounter.WithLabelValues("denied").Inc()
-			return nil, berrors.UnauthorizedError(
-				"OCSP must-staple extension is no longer available: see https://letsencrypt.org/2024/12/05/ending-ocsp",
-			)
-		} else {
-			ra.mustStapleRequestsCounter.WithLabelValues("allowed").Inc()
-		}
-
+	if issuance.ContainsMustStaple(csr.Extensions) {
+		ra.mustStapleRequestsCounter.WithLabelValues("denied").Inc()
+		return nil, berrors.UnauthorizedError(
+			"OCSP must-staple extension is no longer available: see https://letsencrypt.org/2024/12/05/ending-ocsp",
+		)
 	}
 
 	err = csrlib.VerifyCSR(ctx, csr, profile.maxNames, &ra.keyPolicy, ra.PA)



Test patch:
diff --git a/ca/ca_test.go b/ca/ca_test.go
index df549663b..c6614848c 100644
--- a/ca/ca_test.go
+++ b/ca/ca_test.go
@@ -147,7 +147,7 @@ func setup(t *testing.T) *testCtx {
 	fc := clock.NewFake()
 	fc.Add(1 * time.Hour)
 
-	pa, err := policy.New(nil, blog.NewMock())
+	pa, err := policy.New(nil, nil, blog.NewMock())
 	test.AssertNotError(t, err, "Couldn't create PA")
 	err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml")
 	test.AssertNotError(t, err, "Couldn't set hostname policy")
diff --git a/cmd/cert-checker/main_test.go b/cmd/cert-checker/main_test.go
index d8f9e8aaa..4a79d1b80 100644
--- a/cmd/cert-checker/main_test.go
+++ b/cmd/cert-checker/main_test.go
@@ -31,6 +31,7 @@ import (
 	"github.com/letsencrypt/boulder/ctpolicy/loglist"
 	"github.com/letsencrypt/boulder/goodkey"
 	"github.com/letsencrypt/boulder/goodkey/sagoodkey"
+	"github.com/letsencrypt/boulder/identifier"
 	"github.com/letsencrypt/boulder/linter"
 	blog "github.com/letsencrypt/boulder/log"
 	"github.com/letsencrypt/boulder/metrics"
@@ -52,7 +53,10 @@ var (
 
 func init() {
 	var err error
-	pa, err = policy.New(map[core.AcmeChallenge]bool{}, blog.NewMock())
+	pa, err = policy.New(
+		map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
+		map[core.AcmeChallenge]bool{},
+		blog.NewMock())
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/cmd/shell_test.go b/cmd/shell_test.go
index baaaaba0d..073eaf5fa 100644
--- a/cmd/shell_test.go
+++ b/cmd/shell_test.go
@@ -23,22 +23,24 @@ var (
 	validPAConfig = []byte(`{
   "dbConnect": "dummyDBConnect",
   "enforcePolicyWhitelist": false,
-  "challenges": { "http-01": true }
+  "challenges": { "http-01": true },
+  "identifiers": { "dns": true, "ip": true }
 }`)
 	invalidPAConfig = []byte(`{
   "dbConnect": "dummyDBConnect",
   "enforcePolicyWhitelist": false,
-  "challenges": { "nonsense": true }
+  "challenges": { "nonsense": true },
+  "identifiers": { "openpgp": true }
 }`)
-	noChallengesPAConfig = []byte(`{
+	noChallengesIdentsPAConfig = []byte(`{
   "dbConnect": "dummyDBConnect",
   "enforcePolicyWhitelist": false
 }`)
-
-	emptyChallengesPAConfig = []byte(`{
+	emptyChallengesIdentsPAConfig = []byte(`{
   "dbConnect": "dummyDBConnect",
   "enforcePolicyWhitelist": false,
-  "challenges": {}
+  "challenges": {},
+  "identifiers": {}
 }`)
 )
 
@@ -47,21 +49,25 @@ func TestPAConfigUnmarshal(t *testing.T) {
 	err := json.Unmarshal(validPAConfig, &pc1)
 	test.AssertNotError(t, err, "Failed to unmarshal PAConfig")
 	test.AssertNotError(t, pc1.CheckChallenges(), "Flagged valid challenges as bad")
+	test.AssertNotError(t, pc1.CheckIdentifiers(), "Flagged valid identifiers as bad")
 
 	var pc2 PAConfig
 	err = json.Unmarshal(invalidPAConfig, &pc2)
 	test.AssertNotError(t, err, "Failed to unmarshal PAConfig")
 	test.AssertError(t, pc2.CheckChallenges(), "Considered invalid challenges as good")
+	test.AssertError(t, pc2.CheckIdentifiers(), "Considered invalid identifiers as good")
 
 	var pc3 PAConfig
-	err = json.Unmarshal(noChallengesPAConfig, &pc3)
+	err = json.Unmarshal(noChallengesIdentsPAConfig, &pc3)
 	test.AssertNotError(t, err, "Failed to unmarshal PAConfig")
 	test.AssertError(t, pc3.CheckChallenges(), "Disallow empty challenges map")
+	test.AssertNotError(t, pc3.CheckIdentifiers(), "Disallowed empty identifiers map")
 
 	var pc4 PAConfig
-	err = json.Unmarshal(emptyChallengesPAConfig, &pc4)
+	err = json.Unmarshal(emptyChallengesIdentsPAConfig, &pc4)
 	test.AssertNotError(t, err, "Failed to unmarshal PAConfig")
 	test.AssertError(t, pc4.CheckChallenges(), "Disallow empty challenges map")
+	test.AssertNotError(t, pc4.CheckIdentifiers(), "Disallowed empty identifiers map")
 }
 
 func TestMysqlLogger(t *testing.T) {
diff --git a/policy/pa_test.go b/policy/pa_test.go
index d7c586b4f..ac1d07ac0 100644
--- a/policy/pa_test.go
+++ b/policy/pa_test.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"net/netip"
 	"os"
+	"strings"
 	"testing"
 
 	"gopkg.in/yaml.v3"
@@ -23,7 +24,12 @@ func paImpl(t *testing.T) *AuthorityImpl {
 		core.ChallengeTypeTLSALPN01: true,
 	}
 
-	pa, err := New(enabledChallenges, blog.NewMock())
+	enabledIdentifiers := map[identifier.IdentifierType]bool{
+		identifier.TypeDNS: true,
+		identifier.TypeIP:  true,
+	}
+
+	pa, err := New(enabledIdentifiers, enabledChallenges, blog.NewMock())
 	if err != nil {
 		t.Fatalf("Couldn't create policy implementation: %s", err)
 	}
@@ -32,110 +38,146 @@ func paImpl(t *testing.T) *AuthorityImpl {
 
 func TestWellFormedIdentifiers(t *testing.T) {
 	testCases := []struct {
-		domain string
-		err    error
+		ident identifier.ACMEIdentifier
+		err   error
 	}{
-		{``, errEmptyName},                    // Empty name
-		{`zomb!.com`, errInvalidDNSCharacter}, // ASCII character out of range
-		{`emailaddress@myseriously.present.com`, errInvalidDNSCharacter},
-		{`user:pass@myseriously.present.com`, errInvalidDNSCharacter},
-		{`zömbo.com`, errInvalidDNSCharacter},                              // non-ASCII character
-		{`127.0.0.1`, errIPAddress},                                        // IPv4 address
-		{`fe80::1:1`, errInvalidDNSCharacter},                              // IPv6 addresses
-		{`[2001:db8:85a3:8d3:1319:8a2e:370:7348]`, errInvalidDNSCharacter}, // unexpected IPv6 variants
-		{`[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443`, errInvalidDNSCharacter},
-		{`2001:db8::/32`, errInvalidDNSCharacter},
-		{`a.b.c.d.e.f.g.h.i.j.k`, errTooManyLabels}, // Too many labels (>10)
-
-		{`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`, errNameTooLong}, // Too long (254 characters)
-
-		{`www.ef0123456789abcdef013456789abcdef012345.789abcdef012345679abcdef0123456789abcdef01234.6789abcdef0123456789abcdef0.23456789abcdef0123456789a.cdef0123456789abcdef0123456789ab.def0123456789abcdef0123456789.bcdef0123456789abcdef012345.com`, nil}, // OK, not too long (240 characters)
-
-		{`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`, errLabelTooLong}, // Label too long (>63 characters)
-
-		{`www.-ombo.com`, errInvalidDNSCharacter}, // Label starts with '-'
-		{`www.zomb-.com`, errInvalidDNSCharacter}, // Label ends with '-'
-		{`xn--.net`, errInvalidDNSCharacter},      // Label ends with '-'
-		{`-0b.net`, errInvalidDNSCharacter},       // First label begins with '-'
-		{`-0.net`, errInvalidDNSCharacter},        // First label begins with '-'
-		{`-.net`, errInvalidDNSCharacter},         // First label is only '-'
-		{`---.net`, errInvalidDNSCharacter},       // First label is only hyphens
-		{`0`, errTooFewLabels},
-		{`1`, errTooFewLabels},
-		{`*`, errMalformedWildcard},
-		{`**`, errTooManyWildcards},
-		{`*.*`, errTooManyWildcards},
-		{`zombo*com`, errMalformedWildcard},
-		{`*.com`, errICANNTLDWildcard},
-		{`..a`, errLabelTooShort},
-		{`a..a`, errLabelTooShort},
-		{`.a..a`, errLabelTooShort},
-		{`..foo.com`, errLabelTooShort},
-		{`.`, errNameEndsInDot},
-		{`..`, errNameEndsInDot},
-		{`a..`, errNameEndsInDot},
-		{`.....`, errNameEndsInDot},
-		{`.a.`, errNameEndsInDot},
-		{`www.zombo.com.`, errNameEndsInDot},
-		{`www.zombo_com.com`, errInvalidDNSCharacter},
-		{`\uFEFF`, errInvalidDNSCharacter}, // Byte order mark
-		{`\uFEFFwww.zombo.com`, errInvalidDNSCharacter},
-		{`www.zom\u202Ebo.com`, errInvalidDNSCharacter}, // Right-to-Left Override
-		{`\u202Ewww.zombo.com`, errInvalidDNSCharacter},
-		{`www.zom\u200Fbo.com`, errInvalidDNSCharacter}, // Right-to-Left Mark
-		{`\u200Fwww.zombo.com`, errInvalidDNSCharacter},
+		// Invalid identifier types
+		{identifier.ACMEIdentifier{}, errUnsupportedIdent}, // Empty identifier type
+		{identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}, errUnsupportedIdent},
+
+		// Empty identifier values
+		{identifier.NewDNS(``), errEmptyIdentifier},                 // Empty DNS identifier
+		{identifier.ACMEIdentifier{Type: "ip"}, errEmptyIdentifier}, // Empty IP identifier
+
+		// DNS follies
+
+		{identifier.NewDNS(`zomb!.com`), errInvalidDNSCharacter}, // ASCII character out of range
+		{identifier.NewDNS(`emailaddress@myseriously.present.com`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`user:pass@myseriously.present.com`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`zömbo.com`), errInvalidDNSCharacter},                              // non-ASCII character
+		{identifier.NewDNS(`127.0.0.1`), errIPAddressInDNS},                                   // IPv4 address
+		{identifier.NewDNS(`fe80::1:1`), errInvalidDNSCharacter},                              // IPv6 address
+		{identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]`), errInvalidDNSCharacter}, // unexpected IPv6 variants
+		{identifier.NewDNS(`[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`2001:db8::/32`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`a.b.c.d.e.f.g.h.i.j.k`), errTooManyLabels}, // Too many labels (>10)
+
+		{identifier.NewDNS(`www.0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345.com`), errNameTooLong}, // Too long (254 characters)
+
+		{identifier.NewDNS(`www.ef0123456789abcdef013456789abcdef012345.789abcdef012345679abcdef0123456789abcdef01234.6789abcdef0123456789abcdef0.23456789abcdef0123456789a.cdef0123456789abcdef0123456789ab.def0123456789abcdef0123456789.bcdef0123456789abcdef012345.com`), nil}, // OK, not too long (240 characters)
+
+		{identifier.NewDNS(`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`), errLabelTooLong}, // Label too long (>63 characters)
+
+		{identifier.NewDNS(`www.-ombo.com`), errInvalidDNSCharacter}, // Label starts with '-'
+		{identifier.NewDNS(`www.zomb-.com`), errInvalidDNSCharacter}, // Label ends with '-'
+		{identifier.NewDNS(`xn--.net`), errInvalidDNSCharacter},      // Label ends with '-'
+		{identifier.NewDNS(`-0b.net`), errInvalidDNSCharacter},       // First label begins with '-'
+		{identifier.NewDNS(`-0.net`), errInvalidDNSCharacter},        // First label begins with '-'
+		{identifier.NewDNS(`-.net`), errInvalidDNSCharacter},         // First label is only '-'
+		{identifier.NewDNS(`---.net`), errInvalidDNSCharacter},       // First label is only hyphens
+		{identifier.NewDNS(`0`), errTooFewLabels},
+		{identifier.NewDNS(`1`), errTooFewLabels},
+		{identifier.NewDNS(`*`), errMalformedWildcard},
+		{identifier.NewDNS(`**`), errTooManyWildcards},
+		{identifier.NewDNS(`*.*`), errTooManyWildcards},
+		{identifier.NewDNS(`zombo*com`), errMalformedWildcard},
+		{identifier.NewDNS(`*.com`), errICANNTLDWildcard},
+		{identifier.NewDNS(`..a`), errLabelTooShort},
+		{identifier.NewDNS(`a..a`), errLabelTooShort},
+		{identifier.NewDNS(`.a..a`), errLabelTooShort},
+		{identifier.NewDNS(`..foo.com`), errLabelTooShort},
+		{identifier.NewDNS(`.`), errNameEndsInDot},
+		{identifier.NewDNS(`..`), errNameEndsInDot},
+		{identifier.NewDNS(`a..`), errNameEndsInDot},
+		{identifier.NewDNS(`.....`), errNameEndsInDot},
+		{identifier.NewDNS(`.a.`), errNameEndsInDot},
+		{identifier.NewDNS(`www.zombo.com.`), errNameEndsInDot},
+		{identifier.NewDNS(`www.zombo_com.com`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`\uFEFF`), errInvalidDNSCharacter}, // Byte order mark
+		{identifier.NewDNS(`\uFEFFwww.zombo.com`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`www.zom\u202Ebo.com`), errInvalidDNSCharacter}, // Right-to-Left Override
+		{identifier.NewDNS(`\u202Ewww.zombo.com`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`www.zom\u200Fbo.com`), errInvalidDNSCharacter}, // Right-to-Left Mark
+		{identifier.NewDNS(`\u200Fwww.zombo.com`), errInvalidDNSCharacter},
 		// Underscores are technically disallowed in DNS. Some DNS
 		// implementations accept them but we will be conservative.
-		{`www.zom_bo.com`, errInvalidDNSCharacter},
-		{`zombocom`, errTooFewLabels},
-		{`localhost`, errTooFewLabels},
-		{`mail`, errTooFewLabels},
+		{identifier.NewDNS(`www.zom_bo.com`), errInvalidDNSCharacter},
+		{identifier.NewDNS(`zombocom`), errTooFewLabels},
+		{identifier.NewDNS(`localhost`), errTooFewLabels},
+		{identifier.NewDNS(`mail`), errTooFewLabels},
 
 		// disallow capitalized letters for #927
-		{`CapitalizedLetters.com`, errInvalidDNSCharacter},
+		{identifier.NewDNS(`CapitalizedLetters.com`), errInvalidDNSCharacter},
 
-		{`example.acting`, errNonPublic},
-		{`example.internal`, errNonPublic},
+		{identifier.NewDNS(`example.acting`), errNonPublic},
+		{identifier.NewDNS(`example.internal`), errNonPublic},
 		// All-numeric final label not okay.
-		{`www.zombo.163`, errNonPublic},
-		{`xn--109-3veba6djs1bfxlfmx6c9g.xn--f1awi.xn--p1ai`, errMalformedIDN}, // Not in Unicode NFC
-		{`bq--abwhky3f6fxq.jakacomo.com`, errInvalidRLDH},
+		{identifier.NewDNS(`www.zombo.163`), errNonPublic},
+		{identifier.NewDNS(`xn--109-3veba6djs1bfxlfmx6c9g.xn--f1awi.xn--p1ai`), errMalformedIDN}, // Not in Unicode NFC
+		{identifier.NewDNS(`bq--abwhky3f6fxq.jakacomo.com`), errInvalidRLDH},
 		// Three hyphens starting at third second char of first label.
-		{`bq---abwhky3f6fxq.jakacomo.com`, errInvalidRLDH},
+		{identifier.NewDNS(`bq---abwhky3f6fxq.jakacomo.com`), errInvalidRLDH},
 		// Three hyphens starting at second char of first label.
-		{`h---test.hk2yz.org`, errInvalidRLDH},
-		{`co.uk`, errICANNTLD},
-		{`foo.bd`, errICANNTLD},
+		{identifier.NewDNS(`h---test.hk2yz.org`), errInvalidRLDH},
+		{identifier.NewDNS(`co.uk`), errICANNTLD},
+		{identifier.NewDNS(`foo.bd`), errICANNTLD},
+
+		// IP oopsies
+
+		{identifier.ACMEIdentifier{Type: "ip", Value: `zombo.com`}, errIPInvalid}, // That's DNS!
+
+		// Unexpected IPv4 variants
+		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1.1`}, errIPInvalid},            // extra octet
+		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.256`}, errIPInvalid},            // octet out of range
+		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.a1`}, errIPInvalid},             // character out of range
+		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.0/24`}, errIPInvalid},           // with CIDR
+		{identifier.ACMEIdentifier{Type: "ip", Value: `192.168.1.1:443`}, errIPInvalid},          // with port
+		{identifier.ACMEIdentifier{Type: "ip", Value: `0xc0a80101`}, errIPInvalid},               // as hex
+		{identifier.ACMEIdentifier{Type: "ip", Value: `1.1.168.192.in-addr.arpa`}, errIPInvalid}, // reverse DNS
+
+		// Unexpected IPv6 variants
+		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:deed:ffff`}, errIPInvalid},                                       // extra octet
+		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:mead`}, errIPInvalid},                                            // character out of range
+		{identifier.ACMEIdentifier{Type: "ip", Value: `2001:db8::/32`}, errIPInvalid},                                                            // with CIDR
+		{identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]`}, errIPInvalid},                                          // in brackets
+		{identifier.ACMEIdentifier{Type: "ip", Value: `[3fff:aaa:a:c0ff:ee:a:bad:deed]:443`}, errIPInvalid},                                      // in brackets, with port
+		{identifier.ACMEIdentifier{Type: "ip", Value: `0x3fff0aaa000ac0ff00ee000a0baddeed`}, errIPInvalid},                                       // as hex
+		{identifier.ACMEIdentifier{Type: "ip", Value: `d.e.e.d.d.a.b.0.a.0.0.0.e.e.0.0.f.f.0.c.a.0.0.0.a.a.a.0.f.f.f.3.ip6.arpa`}, errIPInvalid}, // reverse DNS
+		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:0aaa:a:c0ff:ee:a:bad:deed`}, errIPInvalid},                                           // leading 0 in 2nd octet (RFC 5952, Sec. 4.1)
+		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:0:0:0:a:bad:deed`}, errIPInvalid},                                                // lone 0s in 3rd-5th octets, :: not used (RFC 5952, Sec. 4.2.1)
+		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa::c0ff:ee:a:bad:deed`}, errIPInvalid},                                             // :: used for just one empty octet (RFC 5952, Sec. 4.2.2)
+		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa::ee:0:0:0`}, errIPInvalid},                                                       // :: used for the shorter of two possible collapses (RFC 5952, Sec. 4.2.3)
+		{identifier.ACMEIdentifier{Type: "ip", Value: `fe80:0:0:0:a::`}, errIPInvalid},                                                           // :: used for the last of two possible equal-length collapses (RFC 5952, Sec. 4.2.3)
+		{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:C0FF:EE:a:bad:deed`}, errIPInvalid},                                            // alpha characters capitalized (RFC 5952, Sec. 4.3)
+		{identifier.ACMEIdentifier{Type: "ip", Value: `::ffff:192.168.1.1`}, errIPInvalid},                                                       // IPv6-encapsulated IPv4
+
+		// IANA special-purpose address blocks
+		{identifier.NewIP(netip.MustParseAddr("192.0.2.129")), errIPSpecialPurpose},                        // Documentation (TEST-NET-1)
+		{identifier.NewIP(netip.MustParseAddr("2001:db8:eee:eeee:eeee:eeee:d01:f1")), errIPSpecialPurpose}, // Documentation
 	}
 
 	// Test syntax errors
 	for _, tc := range testCases {
-		err := WellFormedIdentifiers(identifier.ACMEIdentifiers{identifier.NewDNS(tc.domain)})
+		err := WellFormedIdentifiers(identifier.ACMEIdentifiers{tc.ident})
 		if tc.err == nil {
-			test.AssertNil(t, err, fmt.Sprintf("Unexpected error for domain %q, got %s", tc.domain, err))
+			test.AssertNil(t, err, fmt.Sprintf("Unexpected error for %q identifier %q, got %s", tc.ident.Type, tc.ident.Value, err))
 		} else {
-			test.AssertError(t, err, fmt.Sprintf("Expected error for domain %q, but got none", tc.domain))
+			test.AssertError(t, err, fmt.Sprintf("Expected error for %q identifier %q, but got none", tc.ident.Type, tc.ident.Value))
 			var berr *berrors.BoulderError
 			test.AssertErrorWraps(t, err, &berr)
 			test.AssertContains(t, berr.Error(), tc.err.Error())
 		}
 	}
-
-	err := WellFormedIdentifiers(identifier.ACMEIdentifiers{identifier.NewIP(netip.MustParseAddr("9.9.9.9"))})
-	test.AssertError(t, err, "Expected error for IP, but got none")
-	var berr *berrors.BoulderError
-	test.AssertErrorWraps(t, err, &berr)
-	test.AssertContains(t, berr.Error(), errUnsupportedIdent.Error())
 }
 
 func TestWillingToIssue(t *testing.T) {
-	shouldBeBlocked := []string{
-		`highvalue.website1.org`,
-		`website2.co.uk`,
-		`www.website3.com`,
-		`lots.of.labels.website4.com`,
-		`banned.in.dc.com`,
-		`bad.brains.banned.in.dc.com`,
+	shouldBeBlocked := identifier.ACMEIdentifiers{
+		identifier.NewDNS(`highvalue.website1.org`),
+		identifier.NewDNS(`website2.co.uk`),
+		identifier.NewDNS(`www.website3.com`),
+		identifier.NewDNS(`lots.of.labels.website4.com`),
+		identifier.NewDNS(`banned.in.dc.com`),
+		identifier.NewDNS(`bad.brains.banned.in.dc.com`),
 	}
 	blocklistContents := []string{
 		`website2.com`,
@@ -153,15 +195,17 @@ func TestWillingToIssue(t *testing.T) {
 		`banned.in.dc.com`,
 	}
 
-	shouldBeAccepted := []string{
-		`lowvalue.website1.org`,
-		`website4.sucks`,
-		"www.unrelated.com",
-		"unrelated.com",
-		"www.8675309.com",
-		"8675309.com",
-		"web5ite2.com",
-		"www.web-site2.com",
+	shouldBeAccepted := identifier.ACMEIdentifiers{
+		identifier.NewDNS(`lowvalue.website1.org`),
+		identifier.NewDNS(`website4.sucks`),
+		identifier.NewDNS(`www.unrelated.com`),
+		identifier.NewDNS(`unrelated.com`),
+		identifier.NewDNS(`www.8675309.com`),
+		identifier.NewDNS(`8675309.com`),
+		identifier.NewDNS(`web5ite2.com`),
+		identifier.NewDNS(`www.web-site2.com`),
+		identifier.NewIP(netip.MustParseAddr(`9.9.9.9`)),
+		identifier.NewIP(netip.MustParseAddr(`2620:fe::fe`)),
 	}
 
 	policy := blockedNamesPolicy{
@@ -186,8 +230,8 @@ func TestWillingToIssue(t *testing.T) {
 	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--m.com")})
 	test.AssertError(t, err, "WillingToIssue didn't fail on a malformed IDN")
 	// Invalid identifier type
-	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewIP(netip.MustParseAddr("1.1.1.1"))})
-	test.AssertError(t, err, "WillingToIssue didn't fail on an IP address")
+	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"}})
+	test.AssertError(t, err, "WillingToIssue didn't fail on an invalid identifier type")
 	// Valid encoding
 	err = pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS("www.xn--mnich-kva.com")})
 	test.AssertNotError(t, err, "WillingToIssue failed on a properly formed IDN")
@@ -197,18 +241,18 @@ func TestWillingToIssue(t *testing.T) {
 	features.Reset()
 
 	// Test expected blocked domains
-	for _, domain := range shouldBeBlocked {
-		err := pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS(domain)})
-		test.AssertError(t, err, "domain was not correctly forbidden")
+	for _, ident := range shouldBeBlocked {
+		err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident})
+		test.AssertError(t, err, "identifier was not correctly forbidden")
 		var berr *berrors.BoulderError
 		test.AssertErrorWraps(t, err, &berr)
 		test.AssertContains(t, berr.Detail, errPolicyForbidden.Error())
 	}
 
 	// Test acceptance of good names
-	for _, domain := range shouldBeAccepted {
-		err := pa.WillingToIssue(identifier.ACMEIdentifiers{identifier.NewDNS(domain)})
-		test.AssertNotError(t, err, "domain was incorrectly forbidden")
+	for _, ident := range shouldBeAccepted {
+		err := pa.WillingToIssue(identifier.ACMEIdentifiers{ident})
+		test.AssertNotError(t, err, "identifier was incorrectly forbidden")
 	}
 }
 
@@ -415,15 +459,22 @@ func TestChallengeTypesFor(t *testing.T) {
 			},
 		},
 		{
-			name:  "wildcard",
+			name:  "dns wildcard",
 			ident: identifier.NewDNS("*.example.com"),
 			wantChalls: []core.AcmeChallenge{
 				core.ChallengeTypeDNS01,
 			},
 		},
 		{
-			name:    "other",
-			ident:   identifier.NewIP(netip.MustParseAddr("1.2.3.4")),
+			name:  "ip",
+			ident: identifier.NewIP(netip.MustParseAddr("1.2.3.4")),
+			wantChalls: []core.AcmeChallenge{
+				core.ChallengeTypeHTTP01, core.ChallengeTypeTLSALPN01,
+			},
+		},
+		{
+			name:    "invalid",
+			ident:   identifier.ACMEIdentifier{Type: "fnord", Value: "uh-oh, Spaghetti-Os[tm]"},
 			wantErr: "unrecognized identifier type",
 		},
 	}
@@ -573,3 +624,109 @@ func TestCheckAuthzChallenges(t *testing.T) {
 		})
 	}
 }
+
+func TestWillingToIssue_IdentifierType(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name    string
+		ident   identifier.ACMEIdentifier
+		enabled map[identifier.IdentifierType]bool
+		wantErr string
+	}{
+		{
+			name:    "DNS identifier, none enabled",
+			ident:   identifier.NewDNS("example.com"),
+			enabled: nil,
+			wantErr: "The ACME server has disabled this identifier type",
+		},
+		{
+			name:    "DNS identifier, DNS enabled",
+			ident:   identifier.NewDNS("example.com"),
+			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true},
+			wantErr: "",
+		},
+		{
+			name:    "DNS identifier, DNS & IP enabled",
+			ident:   identifier.NewDNS("example.com"),
+			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
+			wantErr: "",
+		},
+		{
+			name:    "DNS identifier, IP enabled",
+			ident:   identifier.NewDNS("example.com"),
+			enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true},
+			wantErr: "The ACME server has disabled this identifier type",
+		},
+		{
+			name:    "IP identifier, none enabled",
+			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
+			enabled: nil,
+			wantErr: "The ACME server has disabled this identifier type",
+		},
+		{
+			name:    "IP identifier, DNS enabled",
+			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
+			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true},
+			wantErr: "The ACME server has disabled this identifier type",
+		},
+		{
+			name:    "IP identifier, DNS & IP enabled",
+			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
+			enabled: map[identifier.IdentifierType]bool{identifier.TypeDNS: true, identifier.TypeIP: true},
+			wantErr: "",
+		},
+		{
+			name:    "IP identifier, IP enabled",
+			ident:   identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
+			enabled: map[identifier.IdentifierType]bool{identifier.TypeIP: true},
+			wantErr: "",
+		},
+		{
+			name:    "invalid identifier type",
+			ident:   identifier.ACMEIdentifier{Type: "drywall", Value: "oh yeah!"},
+			enabled: map[identifier.IdentifierType]bool{"drywall": true},
+			wantErr: "Invalid identifier type",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			policy := blockedNamesPolicy{
+				HighRiskBlockedNames: []string{"zombo.gov.us"},
+				ExactBlockedNames:    []string{`highvalue.website1.org`},
+				AdminBlockedNames:    []string{`banned.in.dc.com`},
+			}
+
+			yamlPolicyBytes, err := yaml.Marshal(policy)
+			test.AssertNotError(t, err, "Couldn't YAML serialize blocklist")
+			yamlPolicyFile, _ := os.CreateTemp("", "test-blocklist.*.yaml")
+			defer os.Remove(yamlPolicyFile.Name())
+			err = os.WriteFile(yamlPolicyFile.Name(), yamlPolicyBytes, 0640)
+			test.AssertNotError(t, err, "Couldn't write YAML blocklist")
+
+			pa := paImpl(t)
+
+			err = pa.LoadHostnamePolicyFile(yamlPolicyFile.Name())
+			test.AssertNotError(t, err, "Couldn't load rules")
+
+			pa.enabledIdentifiers = tc.enabled
+
+			err = pa.WillingToIssue(identifier.ACMEIdentifiers{tc.ident})
+
+			if tc.wantErr == "" {
+				if err != nil {
+					t.Errorf("should have succeeded, but got error: %s", err.Error())
+				}
+			} else {
+				if err == nil {
+					t.Errorf("should have failed")
+				} else if !strings.Contains(err.Error(), tc.wantErr) {
+					t.Errorf("wrong error; wanted '%s', but got '%s'", tc.wantErr, err.Error())
+				}
+			}
+		})
+	}
+}
diff --git a/ra/ra_test.go b/ra/ra_test.go
index 90828aa05..275887d0e 100644
--- a/ra/ra_test.go
+++ b/ra/ra_test.go
@@ -301,10 +301,16 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
 	}
 	va := va.RemoteClients{VAClient: dummyVA, CAAClient: dummyVA}
 
-	pa, err := policy.New(map[core.AcmeChallenge]bool{
-		core.ChallengeTypeHTTP01: true,
-		core.ChallengeTypeDNS01:  true,
-	}, blog.NewMock())
+	pa, err := policy.New(
+		map[identifier.IdentifierType]bool{
+			identifier.TypeDNS: true,
+			identifier.TypeIP:  true,
+		},
+		map[core.AcmeChallenge]bool{
+			core.ChallengeTypeHTTP01: true,
+			core.ChallengeTypeDNS01:  true,
+		},
+		blog.NewMock())
 	test.AssertNotError(t, err, "Couldn't create PA")
 	err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml")
 	test.AssertNotError(t, err, "Couldn't set hostname policy")
@@ -354,7 +360,7 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
 	ra := NewRegistrationAuthorityImpl(
 		fc, log, stats,
 		1, testKeyPolicy, limiter, txnBuilder, 100,
-		profiles, nil, nil, 5*time.Minute, ctp, nil, nil)
+		profiles, nil, 5*time.Minute, ctp, nil, nil)
 	ra.SA = sa
 	ra.VA = va
 	ra.CA = ca
@@ -2795,10 +2801,16 @@ func TestFinalizeOrderDisabledChallenge(t *testing.T) {
 	test.AssertNotError(t, err, "Error creating policy forbid CSR")
 
 	// Replace the Policy Authority with one which has this challenge type disabled
-	pa, err := policy.New(map[core.AcmeChallenge]bool{
-		core.ChallengeTypeDNS01:     true,
-		core.ChallengeTypeTLSALPN01: true,
-	}, ra.log)
+	pa, err := policy.New(
+		map[identifier.IdentifierType]bool{
+			identifier.TypeDNS: true,
+			identifier.TypeIP:  true,
+		},
+		map[core.AcmeChallenge]bool{
+			core.ChallengeTypeDNS01:     true,
+			core.ChallengeTypeTLSALPN01: true,
+		},
+		ra.log)
 	test.AssertNotError(t, err, "creating test PA")
 	err = pa.LoadHostnamePolicyFile("../test/hostname-policy.yaml")
 	test.AssertNotError(t, err, "loading test hostname policy")
@@ -2832,107 +2844,56 @@ func TestFinalizeWithMustStaple(t *testing.T) {
 		Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05},
 	}
 
-	testCases := []struct {
-		name                  string
-		mustStapleAllowList   *allowlist.List[int64]
-		expectSuccess         bool
-		expectErrorContains   string
-		expectMetricWithLabel prometheus.Labels
-	}{
-		{
-			name:                  "Allow only Registration.ID",
-			mustStapleAllowList:   allowlist.NewList([]int64{Registration.Id}),
-			expectSuccess:         true,
-			expectMetricWithLabel: prometheus.Labels{"allowlist": "allowed"},
-		},
-		{
-			name:                  "Deny all but account Id 1337",
-			mustStapleAllowList:   allowlist.NewList([]int64{1337}),
-			expectSuccess:         false,
-			expectErrorContains:   "no longer available",
-			expectMetricWithLabel: prometheus.Labels{"allowlist": "denied"},
-		},
-		{
-			name:                  "Deny all account Ids",
-			mustStapleAllowList:   allowlist.NewList([]int64{}),
-			expectSuccess:         false,
-			expectErrorContains:   "no longer available",
-			expectMetricWithLabel: prometheus.Labels{"allowlist": "denied"},
-		},
-		{
-			name:                "Allow all account Ids",
-			mustStapleAllowList: nil,
-			expectSuccess:       true,
-			// We don't expect this metric to be be emitted if the allowlist is nil.
-			expectMetricWithLabel: nil,
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			ra.mustStapleAllowList = tc.mustStapleAllowList
-
-			domain := randomDomain()
+	domain := randomDomain()
 
-			authzID := createFinalizedAuthorization(
-				t, sa, identifier.NewDNS(domain), fc.Now().Add(24*time.Hour), core.ChallengeTypeHTTP01, fc.Now().Add(-1*time.Hour))
+	authzID := createFinalizedAuthorization(
+		t, sa, identifier.NewDNS(domain), fc.Now().Add(24*time.Hour), core.ChallengeTypeHTTP01, fc.Now().Add(-1*time.Hour))
 
-			order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{
-				RegistrationID: Registration.Id,
-				Identifiers:    []*corepb.Identifier{identifier.NewDNS(domain).ToProto()},
-			})
-			test.AssertNotError(t, err, "creating test order")
-			test.AssertEquals(t, order.V2Authorizations[0], authzID)
-
-			testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-			test.AssertNotError(t, err, "generating test key")
-
-			csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
-				PublicKey:       testKey.Public(),
-				DNSNames:        []string{domain},
-				ExtraExtensions: []pkix.Extension{ocspMustStapleExt},
-			}, testKey)
-			test.AssertNotError(t, err, "creating must-staple CSR")
-
-			serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
-			test.AssertNotError(t, err, "generating random serial number")
-			template := &x509.Certificate{
-				SerialNumber:          serial,
-				Subject:               pkix.Name{CommonName: domain},
-				DNSNames:              []string{domain},
-				NotBefore:             fc.Now(),
-				NotAfter:              fc.Now().Add(365 * 24 * time.Hour),
-				BasicConstraintsValid: true,
-				ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
-				ExtraExtensions:       []pkix.Extension{ocspMustStapleExt},
-			}
-			cert, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey)
-			test.AssertNotError(t, err, "creating certificate")
-			ra.CA = &mocks.MockCA{
-				PEM: pem.EncodeToMemory(&pem.Block{
-					Bytes: cert,
-					Type:  "CERTIFICATE",
-				}),
-			}
+	order, err := ra.NewOrder(context.Background(), &rapb.NewOrderRequest{
+		RegistrationID: Registration.Id,
+		Identifiers:    []*corepb.Identifier{identifier.NewDNS(domain).ToProto()},
+	})
+	test.AssertNotError(t, err, "creating test order")
+	test.AssertEquals(t, order.V2Authorizations[0], authzID)
 
-			_, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{
-				Order: order,
-				Csr:   csr,
-			})
+	testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	test.AssertNotError(t, err, "generating test key")
 
-			if tc.expectSuccess {
-				test.AssertNotError(t, err, "finalization should succeed")
-			} else {
-				test.AssertError(t, err, "finalization should fail")
-				test.AssertContains(t, err.Error(), tc.expectErrorContains)
-			}
+	csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
+		PublicKey:       testKey.Public(),
+		DNSNames:        []string{domain},
+		ExtraExtensions: []pkix.Extension{ocspMustStapleExt},
+	}, testKey)
+	test.AssertNotError(t, err, "creating must-staple CSR")
 
-			if tc.expectMetricWithLabel != nil {
-				test.AssertMetricWithLabelsEquals(t, ra.mustStapleRequestsCounter, tc.expectMetricWithLabel, 1)
-			}
-			ra.mustStapleRequestsCounter.Reset()
-		})
+	serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
+	test.AssertNotError(t, err, "generating random serial number")
+	template := &x509.Certificate{
+		SerialNumber:          serial,
+		Subject:               pkix.Name{CommonName: domain},
+		DNSNames:              []string{domain},
+		NotBefore:             fc.Now(),
+		NotAfter:              fc.Now().Add(365 * 24 * time.Hour),
+		BasicConstraintsValid: true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+		ExtraExtensions:       []pkix.Extension{ocspMustStapleExt},
 	}
+	cert, err := x509.CreateCertificate(rand.Reader, template, template, testKey.Public(), testKey)
+	test.AssertNotError(t, err, "creating certificate")
+	ra.CA = &mocks.MockCA{
+		PEM: pem.EncodeToMemory(&pem.Block{
+			Bytes: cert,
+			Type:  "CERTIFICATE",
+		}),
+	}
+
+	_, err = ra.FinalizeOrder(context.Background(), &rapb.FinalizeOrderRequest{
+		Order: order,
+		Csr:   csr,
+	})
+	test.AssertError(t, err, "finalization should fail")
+	test.AssertContains(t, err.Error(), "no longer available")
+	test.AssertMetricWithLabelsEquals(t, ra.mustStapleRequestsCounter, prometheus.Labels{"allowlist": "denied"}, 1)
 }
 
 func TestIssueCertificateAuditLog(t *testing.T) {
@@ -3199,7 +3160,7 @@ func TestUpdateMissingAuthorization(t *testing.T) {
 func TestPerformValidationBadChallengeType(t *testing.T) {
 	_, _, ra, _, fc, cleanUp := initAuthorities(t)
 	defer cleanUp()
-	pa, err := policy.New(map[core.AcmeChallenge]bool{}, blog.NewMock())
+	pa, err := policy.New(map[identifier.IdentifierType]bool{}, map[core.AcmeChallenge]bool{}, blog.NewMock())
 	test.AssertNotError(t, err, "Couldn't create PA")
 	ra.PA = pa
 
@@ -4091,10 +4052,16 @@ func TestGetAuthorization(t *testing.T) {
 	}
 
 	// With HTTP01 enabled, GetAuthorization should pass the mock challenge through.
-	pa, err := policy.New(map[core.AcmeChallenge]bool{
-		core.ChallengeTypeHTTP01: true,
-		core.ChallengeTypeDNS01:  true,
-	}, blog.NewMock())
+	pa, err := policy.New(
+		map[identifier.IdentifierType]bool{
+			identifier.TypeDNS: true,
+			identifier.TypeIP:  true,
+		},
+		map[core.AcmeChallenge]bool{
+			core.ChallengeTypeHTTP01: true,
+			core.ChallengeTypeDNS01:  true,
+		},
+		blog.NewMock())
 	test.AssertNotError(t, err, "Couldn't create PA")
 	ra.PA = pa
 	authz, err := ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{Id: 1})
@@ -4103,9 +4070,15 @@ func TestGetAuthorization(t *testing.T) {
 	test.AssertEquals(t, authz.Challenges[0].Type, string(core.ChallengeTypeHTTP01))
 
 	// With HTTP01 disabled, GetAuthorization should filter out the mock challenge.
-	pa, err = policy.New(map[core.AcmeChallenge]bool{
-		core.ChallengeTypeDNS01: true,
-	}, blog.NewMock())
+	pa, err = policy.New(
+		map[identifier.IdentifierType]bool{
+			identifier.TypeDNS: true,
+			identifier.TypeIP:  true,
+		},
+		map[core.AcmeChallenge]bool{
+			core.ChallengeTypeDNS01: true,
+		},
+		blog.NewMock())
 	test.AssertNotError(t, err, "Couldn't create PA")
 	ra.PA = pa
 	authz, err = ra.GetAuthorization(context.Background(), &rapb.GetAuthorizationRequest{Id: 1})
diff --git a/ratelimits/names_test.go b/ratelimits/names_test.go
index 89eca6baa..0dff15415 100644
--- a/ratelimits/names_test.go
+++ b/ratelimits/names_test.go
@@ -205,7 +205,7 @@ func TestValidateIdForName(t *testing.T) {
 			limit: CertificatesPerDomain,
 			desc:  "empty domain",
 			id:    "",
-			err:   "name is empty",
+			err:   "Identifier value (name) is empty",
 		},
 		{
 			limit: CertificatesPerFQDNSet,
diff --git a/test/config-next/ca.json b/test/config-next/ca.json
index 742f686db..756d17ad5 100644
--- a/test/config-next/ca.json
+++ b/test/config-next/ca.json
@@ -190,6 +190,10 @@
 			"http-01": true,
 			"dns-01": true,
 			"tls-alpn-01": true
+		},
+		"identifiers": {
+			"dns": true,
+			"ip": true
 		}
 	},
 	"syslog": {
diff --git a/test/config-next/cert-checker.json b/test/config-next/cert-checker.json
index 723912baf..47ab4d6de 100644
--- a/test/config-next/cert-checker.json
+++ b/test/config-next/cert-checker.json
@@ -29,6 +29,10 @@
 			"http-01": true,
 			"dns-01": true,
 			"tls-alpn-01": true
+		},
+		"identifiers": {
+			"dns": true,
+			"ip": true
 		}
 	},
 	"syslog": {
diff --git a/test/config-next/ra.json b/test/config-next/ra.json
index faa5c1170..cd5423f13 100644
--- a/test/config-next/ra.json
+++ b/test/config-next/ra.json
@@ -181,6 +181,10 @@
 			"http-01": true,
 			"dns-01": true,
 			"tls-alpn-01": true
+		},
+		"identifiers": {
+			"dns": true,
+			"ip": true
 		}
 	},
 	"syslog": {
diff --git a/test/ocsp/checkocsp/checkocsp.go b/test/ocsp/checkocsp/checkocsp.go
index 52a52f9b4..4a42659ff 100644
--- a/test/ocsp/checkocsp/checkocsp.go
+++ b/test/ocsp/checkocsp/checkocsp.go
@@ -48,7 +48,6 @@ stale.
 			}
 			serialNumber := big.NewInt(0).SetBytes(bytes)
 			_, err = helper.ReqSerial(serialNumber, config)
-
 		} else {
 			_, err = helper.ReqFile(a, config)
 		}