2211 lines
78 KiB
Go
2211 lines
78 KiB
Go
package ra
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"net/mail"
|
|
"net/url"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jmhodges/clock"
|
|
"github.com/letsencrypt/boulder/akamai"
|
|
akamaipb "github.com/letsencrypt/boulder/akamai/proto"
|
|
caPB "github.com/letsencrypt/boulder/ca/proto"
|
|
"github.com/letsencrypt/boulder/core"
|
|
corepb "github.com/letsencrypt/boulder/core/proto"
|
|
csrlib "github.com/letsencrypt/boulder/csr"
|
|
"github.com/letsencrypt/boulder/ctpolicy"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
"github.com/letsencrypt/boulder/features"
|
|
"github.com/letsencrypt/boulder/goodkey"
|
|
bgrpc "github.com/letsencrypt/boulder/grpc"
|
|
"github.com/letsencrypt/boulder/iana"
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/metrics"
|
|
"github.com/letsencrypt/boulder/probs"
|
|
rapb "github.com/letsencrypt/boulder/ra/proto"
|
|
"github.com/letsencrypt/boulder/ratelimit"
|
|
"github.com/letsencrypt/boulder/reloader"
|
|
"github.com/letsencrypt/boulder/revocation"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
vaPB "github.com/letsencrypt/boulder/va/proto"
|
|
"github.com/letsencrypt/boulder/web"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/weppos/publicsuffix-go/publicsuffix"
|
|
grpc "google.golang.org/grpc"
|
|
)
|
|
|
|
type caaChecker interface {
|
|
IsCAAValid(
|
|
ctx context.Context,
|
|
in *vaPB.IsCAAValidRequest,
|
|
opts ...grpc.CallOption,
|
|
) (*vaPB.IsCAAValidResponse, error)
|
|
}
|
|
|
|
// RegistrationAuthorityImpl defines an RA.
|
|
//
|
|
// NOTE: All of the fields in RegistrationAuthorityImpl need to be
|
|
// populated, or there is a risk of panic.
|
|
type RegistrationAuthorityImpl struct {
|
|
CA core.CertificateAuthority
|
|
VA core.ValidationAuthority
|
|
SA core.StorageAuthority
|
|
PA core.PolicyAuthority
|
|
publisher core.Publisher
|
|
caa caaChecker
|
|
|
|
stats metrics.Scope
|
|
clk clock.Clock
|
|
log blog.Logger
|
|
keyPolicy goodkey.KeyPolicy
|
|
// How long before a newly created authorization expires.
|
|
authorizationLifetime time.Duration
|
|
pendingAuthorizationLifetime time.Duration
|
|
rlPolicies ratelimit.Limits
|
|
maxContactsPerReg int
|
|
maxNames int
|
|
forceCNFromSAN bool
|
|
reuseValidAuthz bool
|
|
orderLifetime time.Duration
|
|
|
|
issuer *x509.Certificate
|
|
purger akamaipb.AkamaiPurgerClient
|
|
|
|
regByIPStats metrics.Scope
|
|
regByIPRangeStats metrics.Scope
|
|
pendAuthByRegIDStats metrics.Scope
|
|
pendOrdersByRegIDStats metrics.Scope
|
|
newOrderByRegIDStats metrics.Scope
|
|
certsForDomainStats metrics.Scope
|
|
|
|
ctpolicy *ctpolicy.CTPolicy
|
|
ctpolicyResults *prometheus.HistogramVec
|
|
|
|
namesPerCert *prometheus.HistogramVec
|
|
}
|
|
|
|
// NewRegistrationAuthorityImpl constructs a new RA object.
|
|
func NewRegistrationAuthorityImpl(
|
|
clk clock.Clock,
|
|
logger blog.Logger,
|
|
stats metrics.Scope,
|
|
maxContactsPerReg int,
|
|
keyPolicy goodkey.KeyPolicy,
|
|
maxNames int,
|
|
forceCNFromSAN bool,
|
|
reuseValidAuthz bool,
|
|
authorizationLifetime time.Duration,
|
|
pendingAuthorizationLifetime time.Duration,
|
|
pubc core.Publisher,
|
|
caaClient caaChecker,
|
|
orderLifetime time.Duration,
|
|
ctp *ctpolicy.CTPolicy,
|
|
purger akamaipb.AkamaiPurgerClient,
|
|
issuer *x509.Certificate,
|
|
) *RegistrationAuthorityImpl {
|
|
ctpolicyResults := prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Name: "ctpolicy_results",
|
|
Help: "Histogram of latencies of ctpolicy.GetSCTs calls with success/failure/deadlineExceeded labels",
|
|
Buckets: metrics.InternetFacingBuckets,
|
|
},
|
|
[]string{"result"},
|
|
)
|
|
stats.MustRegister(ctpolicyResults)
|
|
|
|
namesPerCert := prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Name: "names_per_cert",
|
|
Help: "Histogram of the number of SANs in requested and issued certificates",
|
|
// The namesPerCert buckets are chosen based on the current Let's Encrypt
|
|
// limit of 100 SANs per certificate.
|
|
Buckets: []float64{1, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100},
|
|
},
|
|
// Type label value is either "requested" or "issued".
|
|
[]string{"type"},
|
|
)
|
|
stats.MustRegister(namesPerCert)
|
|
|
|
ra := &RegistrationAuthorityImpl{
|
|
stats: stats,
|
|
clk: clk,
|
|
log: logger,
|
|
authorizationLifetime: authorizationLifetime,
|
|
pendingAuthorizationLifetime: pendingAuthorizationLifetime,
|
|
rlPolicies: ratelimit.New(),
|
|
maxContactsPerReg: maxContactsPerReg,
|
|
keyPolicy: keyPolicy,
|
|
maxNames: maxNames,
|
|
forceCNFromSAN: forceCNFromSAN,
|
|
reuseValidAuthz: reuseValidAuthz,
|
|
regByIPStats: stats.NewScope("RateLimit", "RegistrationsByIP"),
|
|
regByIPRangeStats: stats.NewScope("RateLimit", "RegistrationsByIPRange"),
|
|
pendAuthByRegIDStats: stats.NewScope("RateLimit", "PendingAuthorizationsByRegID"),
|
|
pendOrdersByRegIDStats: stats.NewScope("RateLimit", "PendingOrdersByRegID"),
|
|
newOrderByRegIDStats: stats.NewScope("RateLimit", "NewOrdersByRegID"),
|
|
certsForDomainStats: stats.NewScope("RateLimit", "CertificatesForDomain"),
|
|
publisher: pubc,
|
|
caa: caaClient,
|
|
orderLifetime: orderLifetime,
|
|
ctpolicy: ctp,
|
|
ctpolicyResults: ctpolicyResults,
|
|
purger: purger,
|
|
issuer: issuer,
|
|
namesPerCert: namesPerCert,
|
|
}
|
|
return ra
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) SetRateLimitPoliciesFile(filename string) error {
|
|
_, err := reloader.New(filename, ra.rlPolicies.LoadPolicies, ra.rateLimitPoliciesLoadError)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) rateLimitPoliciesLoadError(err error) {
|
|
ra.log.Errf("error reloading rate limit policy: %s", err)
|
|
}
|
|
|
|
// certificateRequestAuthz is a struct for holding information about a valid
|
|
// authz referenced during a certificateRequestEvent. It holds both the
|
|
// authorization ID and the challenge type that made the authorization valid. We
|
|
// specifically include the challenge type that solved the authorization to make
|
|
// some common analysis easier.
|
|
type certificateRequestAuthz struct {
|
|
ID string
|
|
ChallengeType string
|
|
}
|
|
|
|
// certificateRequestEvent is a struct for holding information that is logged as
|
|
// JSON to the audit log as the result of an issuance event.
|
|
type certificateRequestEvent struct {
|
|
ID string `json:",omitempty"`
|
|
// Requester is the associated account ID
|
|
Requester int64 `json:",omitempty"`
|
|
// OrderID is the associated order ID (may be empty for an ACME v1 issuance)
|
|
OrderID int64 `json:",omitempty"`
|
|
// SerialNumber is the string representation of the issued certificate's
|
|
// serial number
|
|
SerialNumber string `json:",omitempty"`
|
|
// VerifiedFields are required by the baseline requirements and are always
|
|
// a static value for Boulder.
|
|
VerifiedFields []string `json:",omitempty"`
|
|
// CommonName is the subject common name from the issued cert
|
|
CommonName string `json:",omitempty"`
|
|
// Names are the DNS SAN entries from the issued cert
|
|
Names []string `json:",omitempty"`
|
|
// NotBefore is the starting timestamp of the issued cert's validity period
|
|
NotBefore time.Time `json:",omitempty"`
|
|
// NotAfter is the ending timestamp of the issued cert's validity period
|
|
NotAfter time.Time `json:",omitempty"`
|
|
// RequestTime and ResponseTime are for tracking elapsed time during issuance
|
|
RequestTime time.Time `json:",omitempty"`
|
|
ResponseTime time.Time `json:",omitempty"`
|
|
// Error contains any encountered errors
|
|
Error string `json:",omitempty"`
|
|
// Authorizations is a map of identifier names to certificateRequestAuthz
|
|
// objects. It can be used to understand how the names in a certificate
|
|
// request were authorized.
|
|
Authorizations map[string]certificateRequestAuthz
|
|
}
|
|
|
|
// noRegistrationID is used for the regID parameter to GetThreshold when no
|
|
// registration-based overrides are necessary.
|
|
const noRegistrationID = -1
|
|
|
|
// registrationCounter is a type to abstract the use of
|
|
// ra.SA.CountRegistrationsByIP or ra.SA.CountRegistrationsByIPRange
|
|
type registrationCounter func(context.Context, net.IP, time.Time, time.Time) (int, error)
|
|
|
|
// checkRegistrationIPLimit checks a specific registraton limit by using the
|
|
// provided registrationCounter function to determine if the limit has been
|
|
// exceeded for a given IP or IP range
|
|
func (ra *RegistrationAuthorityImpl) checkRegistrationIPLimit(
|
|
ctx context.Context,
|
|
limit ratelimit.RateLimitPolicy,
|
|
ip net.IP,
|
|
counter registrationCounter) error {
|
|
|
|
if !limit.Enabled() {
|
|
return nil
|
|
}
|
|
|
|
now := ra.clk.Now()
|
|
windowBegin := limit.WindowBegin(now)
|
|
count, err := counter(ctx, ip, windowBegin, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if count >= limit.GetThreshold(ip.String(), noRegistrationID) {
|
|
return berrors.RateLimitError("too many registrations for this IP")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkRegistrationLimits enforces the RegistrationsPerIP and
|
|
// RegistrationsPerIPRange limits
|
|
func (ra *RegistrationAuthorityImpl) checkRegistrationLimits(ctx context.Context, ip net.IP) error {
|
|
// Check the registrations per IP limit using the CountRegistrationsByIP SA
|
|
// function that matches IP addresses exactly
|
|
exactRegLimit := ra.rlPolicies.RegistrationsPerIP()
|
|
err := ra.checkRegistrationIPLimit(ctx, exactRegLimit, ip, ra.SA.CountRegistrationsByIP)
|
|
if err != nil {
|
|
ra.regByIPStats.Inc("Exceeded", 1)
|
|
ra.log.Infof("Rate limit exceeded, RegistrationsByIP, IP: %s", ip)
|
|
return err
|
|
}
|
|
ra.regByIPStats.Inc("Pass", 1)
|
|
|
|
// We only apply the fuzzy reg limit to IPv6 addresses.
|
|
// Per https://golang.org/pkg/net/#IP.To4 "If ip is not an IPv4 address, To4
|
|
// returns nil"
|
|
if ip.To4() != nil {
|
|
return nil
|
|
}
|
|
|
|
// Check the registrations per IP range limit using the
|
|
// CountRegistrationsByIPRange SA function that fuzzy-matches IPv6 addresses
|
|
// within a larger address range
|
|
fuzzyRegLimit := ra.rlPolicies.RegistrationsPerIPRange()
|
|
err = ra.checkRegistrationIPLimit(ctx, fuzzyRegLimit, ip, ra.SA.CountRegistrationsByIPRange)
|
|
if err != nil {
|
|
ra.regByIPRangeStats.Inc("Exceeded", 1)
|
|
ra.log.Infof("Rate limit exceeded, RegistrationsByIPRange, IP: %s", ip)
|
|
// For the fuzzyRegLimit we use a new error message that specifically
|
|
// mentions that the limit being exceeded is applied to a *range* of IPs
|
|
return berrors.RateLimitError("too many registrations for this IP range")
|
|
}
|
|
ra.regByIPRangeStats.Inc("Pass", 1)
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewRegistration constructs a new Registration from a request.
|
|
func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, init core.Registration) (core.Registration, error) {
|
|
if err := ra.keyPolicy.GoodKey(init.Key.Key); err != nil {
|
|
return core.Registration{}, berrors.MalformedError("invalid public key: %s", err.Error())
|
|
}
|
|
if err := ra.checkRegistrationLimits(ctx, init.InitialIP); err != nil {
|
|
return core.Registration{}, err
|
|
}
|
|
|
|
reg := core.Registration{
|
|
Key: init.Key,
|
|
Status: core.StatusValid,
|
|
}
|
|
_ = mergeUpdate(®, init)
|
|
|
|
// This field isn't updatable by the end user, so it isn't copied by
|
|
// MergeUpdate. But we need to fill it in for new registrations.
|
|
reg.InitialIP = init.InitialIP
|
|
|
|
if err := ra.validateContacts(ctx, reg.Contact); err != nil {
|
|
return core.Registration{}, err
|
|
}
|
|
|
|
// Store the authorization object, then return it
|
|
reg, err := ra.SA.NewRegistration(ctx, reg)
|
|
if err != nil {
|
|
return core.Registration{}, err
|
|
}
|
|
|
|
ra.stats.Inc("NewRegistrations", 1)
|
|
return reg, nil
|
|
}
|
|
|
|
// validateContacts checks the provided list of contacts, returning an error if
|
|
// any are not acceptable. Unacceptable contacts lists include:
|
|
// * An empty list
|
|
// * A list has more than maxContactsPerReg contacts
|
|
// * A list containing an empty contact
|
|
// * A list containing a contact that does not parse as a URL
|
|
// * A list containing a contact that has a URL scheme other than mailto
|
|
// * A list containing a contact that has non-ascii characters
|
|
// * A list containing a contact that doesn't pass `validateEmail`
|
|
func (ra *RegistrationAuthorityImpl) validateContacts(ctx context.Context, contacts *[]string) error {
|
|
if contacts == nil || len(*contacts) == 0 {
|
|
return nil // Nothing to validate
|
|
}
|
|
if ra.maxContactsPerReg > 0 && len(*contacts) > ra.maxContactsPerReg {
|
|
return berrors.MalformedError(
|
|
"too many contacts provided: %d > %d",
|
|
len(*contacts),
|
|
ra.maxContactsPerReg,
|
|
)
|
|
}
|
|
|
|
for _, contact := range *contacts {
|
|
if contact == "" {
|
|
return berrors.MalformedError("empty contact")
|
|
}
|
|
parsed, err := url.Parse(contact)
|
|
if err != nil {
|
|
return berrors.MalformedError("invalid contact")
|
|
}
|
|
if parsed.Scheme != "mailto" {
|
|
return berrors.MalformedError("contact method %s is not supported", parsed.Scheme)
|
|
}
|
|
if !core.IsASCII(contact) {
|
|
return berrors.MalformedError(
|
|
"contact email [%s] contains non-ASCII characters",
|
|
contact,
|
|
)
|
|
}
|
|
if err := validateEmail(parsed.Opaque); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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
|
|
// messages and subscriber communications reaching the user that created the
|
|
// registration if allowed.
|
|
var forbiddenMailDomains = map[string]bool{
|
|
// https://tools.ietf.org/html/rfc2606#section-3
|
|
"example.com": true,
|
|
"example.net": true,
|
|
"example.org": true,
|
|
}
|
|
|
|
// validateEmail returns an error if the given address is not parseable as an
|
|
// email address or if the domain portion of the email address is a member of
|
|
// the forbiddenMailDomains map.
|
|
func validateEmail(address string) error {
|
|
email, err := mail.ParseAddress(address)
|
|
if err != nil {
|
|
if len(address) > 254 {
|
|
address = address[:254]
|
|
}
|
|
return berrors.InvalidEmailError("%q is not a valid e-mail address", address)
|
|
}
|
|
splitEmail := strings.SplitN(email.Address, "@", -1)
|
|
domain := strings.ToLower(splitEmail[len(splitEmail)-1])
|
|
if forbiddenMailDomains[domain] {
|
|
return berrors.InvalidEmailError(
|
|
"invalid contact domain. Contact emails @%s are forbidden",
|
|
domain)
|
|
}
|
|
if _, err := iana.ExtractSuffix(domain); err != nil {
|
|
return berrors.InvalidEmailError("email domain name does not end in a IANA suffix")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) checkPendingAuthorizationLimit(ctx context.Context, regID int64) error {
|
|
limit := ra.rlPolicies.PendingAuthorizationsPerAccount()
|
|
if limit.Enabled() {
|
|
var count int
|
|
var err error
|
|
if features.Enabled(features.NewAuthorizationSchema) {
|
|
countPB, err := ra.SA.CountPendingAuthorizations2(ctx, &sapb.RegistrationID{
|
|
Id: ®ID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
count = int(*countPB.Count)
|
|
} else {
|
|
count, err = ra.SA.CountPendingAuthorizations(ctx, regID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Most rate limits have a key for overrides, but there is no meaningful key
|
|
// here.
|
|
noKey := ""
|
|
if count >= limit.GetThreshold(noKey, regID) {
|
|
ra.pendAuthByRegIDStats.Inc("Exceeded", 1)
|
|
ra.log.Infof("Rate limit exceeded, PendingAuthorizationsByRegID, regID: %d", regID)
|
|
return berrors.RateLimitError("too many currently pending authorizations")
|
|
}
|
|
ra.pendAuthByRegIDStats.Inc("Pass", 1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) checkInvalidAuthorizationLimit(ctx context.Context, regID int64, hostname string) error {
|
|
limit := ra.rlPolicies.InvalidAuthorizationsPerAccount()
|
|
// The SA.CountInvalidAuthorizations method is not implemented on the wrapper
|
|
// interface, because we want to move towards using gRPC interfaces more
|
|
// directly. So we type-assert the wrapper to a gRPC-specific type.
|
|
saGRPC, ok := ra.SA.(*bgrpc.StorageAuthorityClientWrapper)
|
|
if !limit.Enabled() || !ok {
|
|
return nil
|
|
}
|
|
latest := ra.clk.Now().Add(ra.pendingAuthorizationLifetime)
|
|
earliest := latest.Add(-limit.Window.Duration)
|
|
latestNanos := latest.UnixNano()
|
|
earliestNanos := earliest.UnixNano()
|
|
var count *sapb.Count
|
|
var err error
|
|
req := &sapb.CountInvalidAuthorizationsRequest{
|
|
RegistrationID: ®ID,
|
|
Hostname: &hostname,
|
|
Range: &sapb.Range{
|
|
Earliest: &earliestNanos,
|
|
Latest: &latestNanos,
|
|
},
|
|
}
|
|
if features.Enabled(features.NewAuthorizationSchema) {
|
|
count, err = saGRPC.CountInvalidAuthorizations2(ctx, req)
|
|
} else {
|
|
count, err = saGRPC.CountInvalidAuthorizations(ctx, req)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count == nil {
|
|
return fmt.Errorf("nil count")
|
|
}
|
|
// Most rate limits have a key for overrides, but there is no meaningful key
|
|
// here.
|
|
noKey := ""
|
|
if *count.Count >= int64(limit.GetThreshold(noKey, regID)) {
|
|
ra.log.Infof("Rate limit exceeded, InvalidAuthorizationsByRegID, regID: %d", regID)
|
|
return berrors.RateLimitError("too many failed authorizations recently")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkNewOrdersPerAccountLimit enforces the rlPolicies `NewOrdersPerAccount`
|
|
// rate limit. This rate limit ensures a client can not create more than the
|
|
// specified threshold of new orders within the specified time window.
|
|
func (ra *RegistrationAuthorityImpl) checkNewOrdersPerAccountLimit(ctx context.Context, acctID int64) error {
|
|
limit := ra.rlPolicies.NewOrdersPerAccount()
|
|
if !limit.Enabled() {
|
|
return nil
|
|
}
|
|
latest := ra.clk.Now()
|
|
earliest := latest.Add(-limit.Window.Duration)
|
|
count, err := ra.SA.CountOrders(ctx, acctID, earliest, latest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// There is no meaningful override key to use for this rate limit
|
|
noKey := ""
|
|
if count >= limit.GetThreshold(noKey, acctID) {
|
|
ra.newOrderByRegIDStats.Inc("Exceeded", 1)
|
|
return berrors.RateLimitError("too many new orders recently")
|
|
}
|
|
ra.newOrderByRegIDStats.Inc("Pass", 1)
|
|
return nil
|
|
}
|
|
|
|
// NewAuthorization constructs a new Authz from a request. Values (domains) in
|
|
// request.Identifier will be lowercased before storage.
|
|
func (ra *RegistrationAuthorityImpl) NewAuthorization(ctx context.Context, request core.Authorization, regID int64) (core.Authorization, error) {
|
|
identifier := request.Identifier
|
|
identifier.Value = strings.ToLower(identifier.Value)
|
|
|
|
// Check that the identifier is present and appropriate
|
|
if err := ra.PA.WillingToIssue(identifier); err != nil {
|
|
return core.Authorization{}, err
|
|
}
|
|
|
|
if err := ra.checkPendingAuthorizationLimit(ctx, regID); err != nil {
|
|
return core.Authorization{}, err
|
|
}
|
|
|
|
if err := ra.checkInvalidAuthorizationLimit(ctx, regID, identifier.Value); err != nil {
|
|
return core.Authorization{}, err
|
|
}
|
|
|
|
if ra.reuseValidAuthz {
|
|
var auths map[string]*core.Authorization
|
|
var err error
|
|
if features.Enabled(features.NewAuthorizationSchema) {
|
|
now := ra.clk.Now().UnixNano()
|
|
authzMapPB, err := ra.SA.GetValidAuthorizations2(ctx, &sapb.GetValidAuthorizationsRequest{
|
|
RegistrationID: ®ID,
|
|
Domains: []string{identifier.Value},
|
|
Now: &now,
|
|
})
|
|
if err != nil {
|
|
outErr := berrors.InternalServerError(
|
|
"unable to get existing validations for regID: %d, identifier: %s, %s",
|
|
regID,
|
|
identifier.Value,
|
|
err,
|
|
)
|
|
ra.log.Warning(outErr.Error())
|
|
return core.Authorization{}, outErr
|
|
}
|
|
auths, err = bgrpc.PBToAuthzMap(authzMapPB)
|
|
if err != nil {
|
|
return core.Authorization{}, err
|
|
}
|
|
} else {
|
|
auths, err = ra.SA.GetValidAuthorizations(ctx, regID, []string{identifier.Value}, ra.clk.Now())
|
|
if err != nil {
|
|
outErr := berrors.InternalServerError(
|
|
"unable to get existing validations for regID: %d, identifier: %s, %s",
|
|
regID,
|
|
identifier.Value,
|
|
err,
|
|
)
|
|
ra.log.Warning(outErr.Error())
|
|
return core.Authorization{}, outErr
|
|
}
|
|
}
|
|
|
|
if existingAuthz, ok := auths[identifier.Value]; ok {
|
|
if ra.authzValidChallengeEnabled(existingAuthz) {
|
|
// The existing authorization must not expire within the next 24 hours for
|
|
// it to be OK for reuse
|
|
reuseCutOff := ra.clk.Now().Add(time.Hour * 24)
|
|
if existingAuthz.Expires.After(reuseCutOff) {
|
|
ra.stats.Inc("ReusedValidAuthz", 1)
|
|
return *existingAuthz, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
nowishNano := ra.clk.Now().Add(time.Hour).UnixNano()
|
|
identifierTypeString := string(identifier.Type)
|
|
req := &sapb.GetPendingAuthorizationRequest{
|
|
RegistrationID: ®ID,
|
|
IdentifierType: &identifierTypeString,
|
|
IdentifierValue: &identifier.Value,
|
|
ValidUntil: &nowishNano,
|
|
}
|
|
if features.Enabled(features.NewAuthorizationSchema) {
|
|
pendingPB, err := ra.SA.GetPendingAuthorization2(ctx, req)
|
|
if err != nil && !berrors.Is(err, berrors.NotFound) {
|
|
return core.Authorization{}, berrors.InternalServerError(
|
|
"unable to get pending authorization for regID: %d, identifier: %s: %s",
|
|
regID,
|
|
identifier.Value,
|
|
err)
|
|
} else if err == nil {
|
|
return bgrpc.PBToAuthz(pendingPB)
|
|
}
|
|
} else {
|
|
pendingAuth, err := ra.SA.GetPendingAuthorization(ctx, req)
|
|
if err != nil && !berrors.Is(err, berrors.NotFound) {
|
|
return core.Authorization{}, berrors.InternalServerError(
|
|
"unable to get pending authorization for regID: %d, identifier: %s: %s",
|
|
regID,
|
|
identifier.Value,
|
|
err)
|
|
} else if err == nil {
|
|
return *pendingAuth, nil
|
|
}
|
|
}
|
|
|
|
v2 := features.Enabled(features.NewAuthorizationSchema)
|
|
authzPB, err := ra.createPendingAuthz(ctx, regID, identifier, v2)
|
|
if err != nil {
|
|
return core.Authorization{}, err
|
|
}
|
|
|
|
if v2 {
|
|
authzIDs, err := ra.SA.NewAuthorizations2(ctx, &sapb.AddPendingAuthorizationsRequest{
|
|
Authz: []*corepb.Authorization{authzPB},
|
|
})
|
|
if err != nil {
|
|
return core.Authorization{}, err
|
|
}
|
|
if len(authzIDs.Ids) != 1 {
|
|
return core.Authorization{}, berrors.InternalServerError("unexpected number of authorization IDs returned from NewAuthorizations2: expected 1, got %d", len(authzIDs.Ids))
|
|
}
|
|
// The current internal authorization objects use a string for the ID, the new
|
|
// storage format uses a integer ID. In order to maintain compatibility we
|
|
// convert the integer ID to a string.
|
|
id := fmt.Sprintf("%d", authzIDs.Ids[0])
|
|
authzPB.Id = &id
|
|
return bgrpc.PBToAuthz(authzPB)
|
|
}
|
|
|
|
authz, err := bgrpc.PBToAuthz(authzPB)
|
|
if err != nil {
|
|
return core.Authorization{}, err
|
|
}
|
|
result, err := ra.SA.NewPendingAuthorization(ctx, authz)
|
|
if err != nil {
|
|
// berrors.InternalServerError since the user-data was validated before being
|
|
// passed to the SA.
|
|
err = berrors.InternalServerError("failed to store new pending authorization: %s", err)
|
|
return core.Authorization{}, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// MatchesCSR tests the contents of a generated certificate to make sure
|
|
// that the PublicKey, CommonName, and DNSNames match those provided in
|
|
// the CSR that was used to generate the certificate. It also checks the
|
|
// following fields for:
|
|
// * notBefore is not more than 24 hours ago
|
|
// * BasicConstraintsValid is true
|
|
// * IsCA is false
|
|
// * ExtKeyUsage only contains ExtKeyUsageServerAuth & ExtKeyUsageClientAuth
|
|
// * Subject only contains CommonName & Names
|
|
func (ra *RegistrationAuthorityImpl) MatchesCSR(parsedCertificate *x509.Certificate, csr *x509.CertificateRequest) error {
|
|
// Check issued certificate matches what was expected from the CSR
|
|
hostNames := make([]string, len(csr.DNSNames))
|
|
copy(hostNames, csr.DNSNames)
|
|
if len(csr.Subject.CommonName) > 0 {
|
|
hostNames = append(hostNames, csr.Subject.CommonName)
|
|
}
|
|
hostNames = core.UniqueLowerNames(hostNames)
|
|
|
|
if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) {
|
|
return berrors.InternalServerError("generated certificate public key doesn't match CSR public key")
|
|
}
|
|
if !ra.forceCNFromSAN && len(csr.Subject.CommonName) > 0 &&
|
|
parsedCertificate.Subject.CommonName != strings.ToLower(csr.Subject.CommonName) {
|
|
return berrors.InternalServerError("generated certificate CommonName doesn't match CSR CommonName")
|
|
}
|
|
// Sort both slices of names before comparison.
|
|
parsedNames := parsedCertificate.DNSNames
|
|
sort.Strings(parsedNames)
|
|
sort.Strings(hostNames)
|
|
if !reflect.DeepEqual(parsedNames, hostNames) {
|
|
return berrors.InternalServerError("generated certificate DNSNames don't match CSR DNSNames")
|
|
}
|
|
if !reflect.DeepEqual(parsedCertificate.IPAddresses, csr.IPAddresses) {
|
|
return berrors.InternalServerError("generated certificate IPAddresses don't match CSR IPAddresses")
|
|
}
|
|
if !reflect.DeepEqual(parsedCertificate.EmailAddresses, csr.EmailAddresses) {
|
|
return berrors.InternalServerError("generated certificate EmailAddresses don't match CSR EmailAddresses")
|
|
}
|
|
if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 ||
|
|
len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 ||
|
|
len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 ||
|
|
len(parsedCertificate.Subject.PostalCode) > 0 {
|
|
return berrors.InternalServerError("generated certificate Subject contains fields other than CommonName, or SerialNumber")
|
|
}
|
|
now := ra.clk.Now()
|
|
if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 {
|
|
return berrors.InternalServerError("generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore))
|
|
}
|
|
if !parsedCertificate.BasicConstraintsValid {
|
|
return berrors.InternalServerError("generated certificate doesn't have basic constraints set")
|
|
}
|
|
if parsedCertificate.IsCA {
|
|
return berrors.InternalServerError("generated certificate can sign other certificates")
|
|
}
|
|
if !reflect.DeepEqual(parsedCertificate.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
|
|
return berrors.InternalServerError("generated certificate doesn't have correct key usage extensions")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkOrderAuthorizations verifies that a provided set of names associated
|
|
// with a specific order and account has all of the required valid, unexpired
|
|
// authorizations to proceed with issuance. It is the ACME v2 equivalent of
|
|
// `checkAuthorizations`. It returns the authorizations that satisfied the set
|
|
// of names or it returns an error. If it returns an error, it will be of type
|
|
// BoulderError.
|
|
func (ra *RegistrationAuthorityImpl) checkOrderAuthorizations(
|
|
ctx context.Context,
|
|
names []string,
|
|
acctID accountID,
|
|
orderID orderID) (map[string]*core.Authorization, error) {
|
|
acctIDInt := int64(acctID)
|
|
orderIDInt := int64(orderID)
|
|
// Get all of the valid authorizations for this account/order
|
|
var authzs map[string]*core.Authorization
|
|
var err error
|
|
req := &sapb.GetValidOrderAuthorizationsRequest{
|
|
Id: &orderIDInt,
|
|
AcctID: &acctIDInt,
|
|
}
|
|
if features.Enabled(features.NewAuthorizationSchema) {
|
|
authzMapPB, err := ra.SA.GetValidOrderAuthorizations2(ctx, req)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError("error in GetValidOrderAuthorizations: %s", err)
|
|
}
|
|
authzs, err = bgrpc.PBToAuthzMap(authzMapPB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
authzs, err = ra.SA.GetValidOrderAuthorizations(ctx, req)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError("error in GetValidOrderAuthorizations: %s", err)
|
|
}
|
|
}
|
|
// Ensure the names from the CSR are free of duplicates & lowercased.
|
|
names = core.UniqueLowerNames(names)
|
|
// Check the authorizations to ensure validity for the names required.
|
|
if err = ra.checkAuthorizationsCAA(ctx, names, authzs, acctIDInt, ra.clk.Now()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return authzs, nil
|
|
}
|
|
|
|
// checkAuthorizations checks that each requested name has a valid authorization
|
|
// that won't expire before the certificate expires. It returns the
|
|
// authorizations that satisfied the set of names or it returns an error.
|
|
// If it returns an error, it will be of type BoulderError.
|
|
func (ra *RegistrationAuthorityImpl) checkAuthorizations(ctx context.Context, names []string, regID int64) (map[string]*core.Authorization, error) {
|
|
now := ra.clk.Now()
|
|
for i := range names {
|
|
names[i] = strings.ToLower(names[i])
|
|
}
|
|
var auths map[string]*core.Authorization
|
|
var err error
|
|
if features.Enabled(features.NewAuthorizationSchema) {
|
|
nowUnix := now.UnixNano()
|
|
authMapPB, err := ra.SA.GetValidAuthorizations2(ctx, &sapb.GetValidAuthorizationsRequest{
|
|
RegistrationID: ®ID,
|
|
Domains: names,
|
|
Now: &nowUnix,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
auths, err = bgrpc.PBToAuthzMap(authMapPB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
auths, err = ra.SA.GetValidAuthorizations(ctx, regID, names, now)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError("error in GetValidAuthorizations: %s", err)
|
|
}
|
|
}
|
|
|
|
if err = ra.checkAuthorizationsCAA(ctx, names, auths, regID, now); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return auths, nil
|
|
}
|
|
|
|
// checkAuthorizationsCAA implements the common logic of validating a set of
|
|
// authorizations against a set of names that is used by both
|
|
// `checkAuthorizations` and `checkOrderAuthorizations`. If required CAA will be
|
|
// rechecked for authorizations that are too old.
|
|
// If it returns an error, it will be of type BoulderError.
|
|
func (ra *RegistrationAuthorityImpl) checkAuthorizationsCAA(
|
|
ctx context.Context,
|
|
names []string,
|
|
authzs map[string]*core.Authorization,
|
|
regID int64,
|
|
now time.Time) error {
|
|
// badNames contains the names that were unauthorized
|
|
var badNames []string
|
|
// recheckAuthzs is a list of authorizations that must have their CAA records rechecked
|
|
var recheckAuthzs []*core.Authorization
|
|
// 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)
|
|
for _, name := range names {
|
|
authz := authzs[name]
|
|
if authz == nil {
|
|
badNames = append(badNames, name)
|
|
} else if authz.Expires == nil {
|
|
return berrors.InternalServerError("found an authorization with a nil Expires field: id %s", authz.ID)
|
|
} else if authz.Expires.Before(now) {
|
|
badNames = append(badNames, name)
|
|
} else if authz.Expires.Before(caaRecheckTime) {
|
|
// Ensure that CAA is rechecked for this name
|
|
recheckAuthzs = append(recheckAuthzs, authz)
|
|
}
|
|
}
|
|
|
|
if len(recheckAuthzs) > 0 {
|
|
if err := ra.recheckCAA(ctx, recheckAuthzs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(badNames) > 0 {
|
|
return berrors.UnauthorizedError(
|
|
"authorizations for these names not found or expired: %s",
|
|
strings.Join(badNames, ", "),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// recheckCAA accepts a list of of names that need to have their CAA records
|
|
// rechecked because their associated authorizations are sufficiently old and
|
|
// performs the CAA checks required for each. If any of the rechecks fail an
|
|
// error is returned.
|
|
func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*core.Authorization) error {
|
|
ra.stats.Inc("recheck_caa", 1)
|
|
ra.stats.Inc("recheck_caa_authzs", int64(len(authzs)))
|
|
|
|
type authzCAAResult struct {
|
|
authz *core.Authorization
|
|
err error
|
|
}
|
|
ch := make(chan authzCAAResult, len(authzs))
|
|
for _, authz := range authzs {
|
|
go func(authz *core.Authorization) {
|
|
name := authz.Identifier.Value
|
|
|
|
// If an authorization has multiple valid challenges,
|
|
// the type of the first valid challenge is used for
|
|
// the purposes of CAA rechecking.
|
|
var method string
|
|
for _, challenge := range authz.Challenges {
|
|
if challenge.Status == core.StatusValid {
|
|
method = challenge.Type
|
|
break
|
|
}
|
|
}
|
|
if method == "" {
|
|
ch <- authzCAAResult{
|
|
authz: authz,
|
|
err: berrors.InternalServerError(
|
|
"Internal error determining validation method for authorization ID %v (%v)",
|
|
authz.ID, name),
|
|
}
|
|
return
|
|
}
|
|
|
|
resp, err := ra.caa.IsCAAValid(ctx, &vaPB.IsCAAValidRequest{
|
|
Domain: &name,
|
|
ValidationMethod: &method,
|
|
AccountURIID: &authz.RegistrationID,
|
|
})
|
|
if err != nil {
|
|
ra.log.AuditErrf("Rechecking CAA: %s", err)
|
|
err = berrors.InternalServerError(
|
|
"Internal error rechecking CAA for authorization ID %v (%v)",
|
|
authz.ID, name,
|
|
)
|
|
} else if resp.Problem != nil {
|
|
err = berrors.CAAError(*resp.Problem.Detail)
|
|
}
|
|
ch <- authzCAAResult{
|
|
authz: authz,
|
|
err: err,
|
|
}
|
|
}(authz)
|
|
}
|
|
var subErrors []berrors.SubBoulderError
|
|
// Read a recheckResult for each authz from the results channel
|
|
for i := 0; i < len(authzs); i++ {
|
|
recheckResult := <-ch
|
|
// If the result had a CAA boulder error, construct a suberror with the
|
|
// identifier from the authorization that was checked.
|
|
if err := recheckResult.err; err != nil {
|
|
if bErr, _ := err.(*berrors.BoulderError); berrors.Is(err, berrors.CAA) {
|
|
subErrors = append(subErrors, berrors.SubBoulderError{
|
|
Identifier: recheckResult.authz.Identifier,
|
|
BoulderError: bErr})
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if len(subErrors) > 0 {
|
|
var detail string
|
|
if len(subErrors) == 1 {
|
|
detail = fmt.Sprintf(
|
|
"Rechecking CAA for %q: %s",
|
|
subErrors[0].Identifier.Value,
|
|
subErrors[0].BoulderError.Detail)
|
|
} else {
|
|
detail = fmt.Sprintf(
|
|
"Rechecking CAA for %q and %d more identifiers failed. "+
|
|
"Refer to sub-problems for more information",
|
|
subErrors[0].Identifier.Value,
|
|
len(subErrors)-1)
|
|
}
|
|
return (&berrors.BoulderError{
|
|
Type: berrors.CAA,
|
|
Detail: detail,
|
|
}).WithSubErrors(subErrors)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// failOrder marks an order as failed by setting the problem details field of
|
|
// the order & persisting it through the SA. If an error occurs doing this we
|
|
// log it and return the order as-is. There aren't any alternatives if we can't
|
|
// add the error to the order.
|
|
func (ra *RegistrationAuthorityImpl) failOrder(
|
|
ctx context.Context,
|
|
order *corepb.Order,
|
|
prob *probs.ProblemDetails) *corepb.Order {
|
|
|
|
// Convert the problem to a protobuf problem for the *corepb.Order field
|
|
pbProb, err := bgrpc.ProblemDetailsToPB(prob)
|
|
if err != nil {
|
|
ra.log.AuditErrf("Could not convert order error problem to PB: %q", err)
|
|
return order
|
|
}
|
|
|
|
// Assign the protobuf problem to the field and save it via the SA
|
|
order.Error = pbProb
|
|
if err := ra.SA.SetOrderError(ctx, order); err != nil {
|
|
ra.log.AuditErrf("Could not persist order error: %q", err)
|
|
}
|
|
return order
|
|
}
|
|
|
|
// FinalizeOrder accepts a request to finalize an order object and, if possible,
|
|
// issues a certificate to satisfy the order. If an order does not have valid,
|
|
// unexpired authorizations for all of its associated names an error is
|
|
// returned. Similarly we vet that all of the names in the order are acceptable
|
|
// based on current policy and return an error if the order can't be fulfilled.
|
|
// If successful the order will be returned in processing status for the client
|
|
// to poll while awaiting finalization to occur.
|
|
func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rapb.FinalizeOrderRequest) (*corepb.Order, error) {
|
|
order := req.Order
|
|
|
|
if *order.Status != string(core.StatusReady) {
|
|
return nil, berrors.OrderNotReadyError(
|
|
"Order's status (%q) is not acceptable for finalization",
|
|
*order.Status)
|
|
}
|
|
|
|
// There should never be an order with 0 names at the stage the RA is
|
|
// processing the order but we check to be on the safe side, throwing an
|
|
// internal server error if this assumption is ever violated.
|
|
if len(order.Names) == 0 {
|
|
return nil, berrors.InternalServerError("Order has no associated names")
|
|
}
|
|
|
|
// Parse the CSR from the request
|
|
csrOb, err := x509.ParseCertificateRequest(req.Csr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := csrlib.VerifyCSR(csrOb, ra.maxNames, &ra.keyPolicy, ra.PA, ra.forceCNFromSAN, *req.Order.RegistrationID); err != nil {
|
|
return nil, berrors.MalformedError(err.Error())
|
|
}
|
|
|
|
// Dedupe, lowercase and sort both the names from the CSR and the names in the
|
|
// order.
|
|
csrNames := core.UniqueLowerNames(csrOb.DNSNames)
|
|
orderNames := core.UniqueLowerNames(order.Names)
|
|
|
|
// Immediately reject the request if the number of names differ
|
|
if len(orderNames) != len(csrNames) {
|
|
return nil, berrors.UnauthorizedError("Order includes different number of names than CSR specifies")
|
|
}
|
|
|
|
// Check that the order names and the CSR names are an exact match
|
|
for i, name := range orderNames {
|
|
if name != csrNames[i] {
|
|
return nil, berrors.UnauthorizedError("CSR is missing Order domain %q", name)
|
|
}
|
|
}
|
|
|
|
// Update the order to be status processing - we issue synchronously at the
|
|
// present time so this is somewhat artificial/unnecessary but allows planning
|
|
// for the future.
|
|
//
|
|
// NOTE(@cpu): After this point any errors that are encountered must update
|
|
// the state of the order to invalid by setting the order's error field.
|
|
// Otherwise the order will be "stuck" in processing state. It can not be
|
|
// finalized because it isn't pending, but we aren't going to process it
|
|
// further because we already did and encountered an error.
|
|
if err := ra.SA.SetOrderProcessing(ctx, order); err != nil {
|
|
// Fail the order with a server internal error - we weren't able to set the
|
|
// status to processing and that's unexpected & weird.
|
|
ra.failOrder(ctx, order, probs.ServerInternal("Error setting order processing"))
|
|
return nil, err
|
|
}
|
|
|
|
// Attempt issuance for the order. If the order isn't fully authorized this
|
|
// will return an error.
|
|
issueReq := core.CertificateRequest{
|
|
Bytes: req.Csr,
|
|
CSR: csrOb,
|
|
}
|
|
cert, err := ra.issueCertificate(ctx, issueReq, accountID(*order.RegistrationID), orderID(*order.Id))
|
|
if err != nil {
|
|
// Fail the order. The problem is computed using
|
|
// `web.ProblemDetailsForError`, the same function the WFE uses to convert
|
|
// between `berrors` and problems. This will turn normal expected berrors like
|
|
// berrors.UnauthorizedError into the correct
|
|
// `urn:ietf:params:acme:error:unauthorized` problem while not letting
|
|
// anything like a server internal error through with sensitive info.
|
|
ra.failOrder(ctx, order, web.ProblemDetailsForError(err, "Error finalizing order"))
|
|
return nil, err
|
|
}
|
|
|
|
// Parse the issued certificate to get the serial
|
|
parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
|
|
if err != nil {
|
|
// Fail the order with a server internal error. The certificate we failed
|
|
// to parse was from our own CA. Bad news!
|
|
ra.failOrder(ctx, order, probs.ServerInternal("Error parsing certificate DER"))
|
|
return nil, err
|
|
}
|
|
serial := core.SerialToString(parsedCertificate.SerialNumber)
|
|
|
|
// Finalize the order with its new CertificateSerial
|
|
order.CertificateSerial = &serial
|
|
if err := ra.SA.FinalizeOrder(ctx, order); err != nil {
|
|
// Fail the order with a server internal error. We weren't able to persist
|
|
// the certificate serial and that's unexpected & weird.
|
|
ra.failOrder(ctx, order, probs.ServerInternal("Error persisting finalized order"))
|
|
return nil, err
|
|
}
|
|
|
|
// Note how many names were in this finalized certificate order.
|
|
ra.namesPerCert.With(
|
|
prometheus.Labels{"type": "issued"},
|
|
).Observe(float64(len(order.Names)))
|
|
|
|
// Update the order status locally since the SA doesn't return the updated
|
|
// order itself after setting the status
|
|
validStatus := string(core.StatusValid)
|
|
order.Status = &validStatus
|
|
return order, nil
|
|
}
|
|
|
|
// NewCertificate requests the issuance of a certificate.
|
|
func (ra *RegistrationAuthorityImpl) NewCertificate(ctx context.Context, req core.CertificateRequest, regID int64) (core.Certificate, error) {
|
|
// Verify the CSR
|
|
if err := csrlib.VerifyCSR(req.CSR, ra.maxNames, &ra.keyPolicy, ra.PA, ra.forceCNFromSAN, regID); err != nil {
|
|
return core.Certificate{}, berrors.MalformedError(err.Error())
|
|
}
|
|
// NewCertificate provides an order ID of 0, indicating this is a classic ACME
|
|
// v1 issuance request from the new certificate endpoint that is not
|
|
// associated with an ACME v2 order.
|
|
return ra.issueCertificate(ctx, req, accountID(regID), orderID(0))
|
|
}
|
|
|
|
// To help minimize the chance that an accountID would be used as an order ID
|
|
// (or vice versa) when calling `issueCertificate` we define internal
|
|
// `accountID` and `orderID` types so that callers must explicitly cast.
|
|
type accountID int64
|
|
type orderID int64
|
|
|
|
// issueCertificate sets up a log event structure and captures any errors
|
|
// encountered during issuance, then calls issueCertificateInner.
|
|
func (ra *RegistrationAuthorityImpl) issueCertificate(
|
|
ctx context.Context,
|
|
req core.CertificateRequest,
|
|
acctID accountID,
|
|
oID orderID) (core.Certificate, error) {
|
|
// Construct the log event
|
|
logEvent := certificateRequestEvent{
|
|
ID: core.NewToken(),
|
|
OrderID: int64(oID),
|
|
Requester: int64(acctID),
|
|
RequestTime: ra.clk.Now(),
|
|
}
|
|
var result string
|
|
cert, err := ra.issueCertificateInner(ctx, req, acctID, oID, &logEvent)
|
|
if err != nil {
|
|
logEvent.Error = err.Error()
|
|
result = "error"
|
|
} else {
|
|
result = "successful"
|
|
}
|
|
logEvent.ResponseTime = ra.clk.Now()
|
|
ra.log.AuditObject(fmt.Sprintf("Certificate request - %s", result), logEvent)
|
|
return cert, err
|
|
}
|
|
|
|
// issueCertificateInner handles the common aspects of certificate issuance used by
|
|
// both the "classic" NewCertificate endpoint (for ACME v1) and the
|
|
// FinalizeOrder endpoint (for ACME v2).
|
|
func (ra *RegistrationAuthorityImpl) issueCertificateInner(
|
|
ctx context.Context,
|
|
req core.CertificateRequest,
|
|
acctID accountID,
|
|
oID orderID,
|
|
logEvent *certificateRequestEvent) (core.Certificate, error) {
|
|
emptyCert := core.Certificate{}
|
|
if acctID <= 0 {
|
|
return emptyCert, berrors.MalformedError("invalid account ID: %d", acctID)
|
|
}
|
|
|
|
// OrderID can be 0 if `issueCertificate` is called by `NewCertificate` for
|
|
// the classic issuance flow. It should never be less than 0.
|
|
if oID < 0 {
|
|
return emptyCert, berrors.MalformedError("invalid order ID: %d", oID)
|
|
}
|
|
|
|
account, err := ra.SA.GetRegistration(ctx, int64(acctID))
|
|
if err != nil {
|
|
return emptyCert, err
|
|
}
|
|
|
|
csr := req.CSR
|
|
logEvent.CommonName = csr.Subject.CommonName
|
|
logEvent.Names = csr.DNSNames
|
|
|
|
// Validate that authorization key is authorized for all domains in the CSR
|
|
names := make([]string, len(csr.DNSNames))
|
|
copy(names, csr.DNSNames)
|
|
|
|
if core.KeyDigestEquals(csr.PublicKey, account.Key) {
|
|
return emptyCert, berrors.MalformedError("certificate public key must be different than account key")
|
|
}
|
|
|
|
// Check rate limits before checking authorizations. If someone is unable to
|
|
// issue a cert due to rate limiting, we don't want to tell them to go get the
|
|
// necessary authorizations, only to later fail the rate limit check.
|
|
err = ra.checkLimits(ctx, names, account.ID)
|
|
if err != nil {
|
|
return emptyCert, err
|
|
}
|
|
|
|
var authzs map[string]*core.Authorization
|
|
// If the orderID is 0 then this is a classic issuance and we need to check
|
|
// that the account is authorized for the names in the CSR.
|
|
if oID == 0 {
|
|
authzs, err = ra.checkAuthorizations(ctx, names, account.ID)
|
|
} else {
|
|
// Otherwise, if the orderID is not 0 we need to follow the order based
|
|
// issuance process and check that this specific order is fully authorized
|
|
// and associated with the expected account ID
|
|
authzs, err = ra.checkOrderAuthorizations(ctx, names, acctID, oID)
|
|
}
|
|
if err != nil {
|
|
// Pass through the error without wrapping it because the called functions
|
|
// return BoulderError and we don't want to lose the type.
|
|
return emptyCert, err
|
|
}
|
|
|
|
// Collect up a certificateRequestAuthz that stores the ID and challenge type
|
|
// of each of the valid authorizations we used for this issuance.
|
|
logEventAuthzs := make(map[string]certificateRequestAuthz, len(names))
|
|
for name, authz := range authzs {
|
|
var solvedByChallengeType string
|
|
// If the authz has no solved by challenge type there has been an internal
|
|
// consistency violation worth logging a warning about. In this case the
|
|
// solvedByChallengeType will be logged as the emtpy string.
|
|
if solvedByChallengeType = authz.SolvedBy(); solvedByChallengeType == "" {
|
|
ra.log.Warningf("Authz %q has status %q but empty SolvedBy()", authz.ID, authz.Status)
|
|
}
|
|
logEventAuthzs[name] = certificateRequestAuthz{
|
|
ID: authz.ID,
|
|
ChallengeType: solvedByChallengeType,
|
|
}
|
|
}
|
|
logEvent.Authorizations = logEventAuthzs
|
|
|
|
// Mark that we verified the CN and SANs
|
|
logEvent.VerifiedFields = []string{"subject.commonName", "subjectAltName"}
|
|
|
|
// Create the certificate and log the result
|
|
acctIDInt := int64(acctID)
|
|
orderIDInt := int64(oID)
|
|
issueReq := &caPB.IssueCertificateRequest{
|
|
Csr: csr.Raw,
|
|
RegistrationID: &acctIDInt,
|
|
OrderID: &orderIDInt,
|
|
}
|
|
|
|
// wrapError adds a prefix to an error. If the error is a boulder error then
|
|
// the problem detail is updated with the prefix. Otherwise a new error is
|
|
// returned with the message prefixed using `fmt.Errorf`
|
|
wrapError := func(e error, prefix string) error {
|
|
if berr, ok := e.(*berrors.BoulderError); ok {
|
|
berr.Detail = fmt.Sprintf("%s: %s", prefix, berr.Detail)
|
|
return berr
|
|
}
|
|
return fmt.Errorf("%s: %s", prefix, e)
|
|
}
|
|
|
|
precert, err := ra.CA.IssuePrecertificate(ctx, issueReq)
|
|
if err != nil {
|
|
return emptyCert, wrapError(err, "issuing precertificate")
|
|
}
|
|
parsedPrecert, err := x509.ParseCertificate(precert.DER)
|
|
if err != nil {
|
|
return emptyCert, wrapError(err, "parsing precertificate")
|
|
}
|
|
scts, err := ra.getSCTs(ctx, precert.DER, parsedPrecert.NotAfter)
|
|
if err != nil {
|
|
return emptyCert, wrapError(err, "getting SCTs")
|
|
}
|
|
cert, err := ra.CA.IssueCertificateForPrecertificate(ctx, &caPB.IssueCertificateForPrecertificateRequest{
|
|
DER: precert.DER,
|
|
SCTs: scts,
|
|
RegistrationID: &acctIDInt,
|
|
OrderID: &orderIDInt,
|
|
})
|
|
if err != nil {
|
|
return emptyCert, wrapError(err, "issuing certificate for precertificate")
|
|
}
|
|
|
|
parsedCertificate, err := x509.ParseCertificate([]byte(cert.DER))
|
|
if err != nil {
|
|
// berrors.InternalServerError because the certificate from the CA should be
|
|
// parseable.
|
|
return emptyCert, berrors.InternalServerError("failed to parse certificate: %s", err.Error())
|
|
}
|
|
|
|
// Asynchronously submit the final certificate to any configured logs
|
|
go ra.ctpolicy.SubmitFinalCert(cert.DER, parsedCertificate.NotAfter)
|
|
|
|
err = ra.MatchesCSR(parsedCertificate, csr)
|
|
if err != nil {
|
|
return emptyCert, err
|
|
}
|
|
|
|
logEvent.SerialNumber = core.SerialToString(parsedCertificate.SerialNumber)
|
|
logEvent.CommonName = parsedCertificate.Subject.CommonName
|
|
logEvent.NotBefore = parsedCertificate.NotBefore
|
|
logEvent.NotAfter = parsedCertificate.NotAfter
|
|
|
|
ra.stats.Inc("NewCertificates", 1)
|
|
return cert, nil
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, cert []byte, expiration time.Time) (core.SCTDERs, error) {
|
|
started := ra.clk.Now()
|
|
scts, err := ra.ctpolicy.GetSCTs(ctx, cert, expiration)
|
|
took := ra.clk.Since(started)
|
|
// The final cert has already been issued so actually return it to the
|
|
// user even if this fails since we aren't actually doing anything with
|
|
// the SCTs yet.
|
|
if err != nil {
|
|
state := "failure"
|
|
if err == context.DeadlineExceeded {
|
|
state = "deadlineExceeded"
|
|
// Convert the error to a missingSCTsError to communicate the timeout,
|
|
// otherwise it will be a generic serverInternalError
|
|
err = berrors.MissingSCTsError(err.Error())
|
|
}
|
|
ra.log.Warningf("ctpolicy.GetSCTs failed: %s", err)
|
|
ra.ctpolicyResults.With(prometheus.Labels{"result": state}).Observe(took.Seconds())
|
|
return nil, err
|
|
}
|
|
ra.ctpolicyResults.With(prometheus.Labels{"result": "success"}).Observe(took.Seconds())
|
|
return scts, nil
|
|
}
|
|
|
|
// domainsForRateLimiting transforms a list of FQDNs into a list of eTLD+1's
|
|
// for the purpose of rate limiting. It also de-duplicates the output
|
|
// domains. Exact public suffix matches are not included.
|
|
func domainsForRateLimiting(names []string) ([]string, error) {
|
|
var domains []string
|
|
for _, name := range names {
|
|
domain, err := publicsuffix.Domain(name)
|
|
if err != nil {
|
|
// The only possible errors are:
|
|
// (1) publicsuffix.Domain is giving garbage values
|
|
// (2) the public suffix is the domain itself
|
|
// We assume 2 and do not include it in the result.
|
|
continue
|
|
}
|
|
domains = append(domains, domain)
|
|
}
|
|
return core.UniqueLowerNames(domains), nil
|
|
}
|
|
|
|
// suffixesForRateLimiting returns the unique subset of input names that are
|
|
// exactly equal to a public suffix.
|
|
func suffixesForRateLimiting(names []string) ([]string, error) {
|
|
var suffixMatches []string
|
|
for _, name := range names {
|
|
_, err := publicsuffix.Domain(name)
|
|
if err != nil {
|
|
// Like `domainsForRateLimiting`, the only possible errors here are:
|
|
// (1) publicsuffix.Domain is giving garbage values
|
|
// (2) the public suffix is the domain itself
|
|
// We assume 2 and collect it into the result
|
|
suffixMatches = append(suffixMatches, name)
|
|
}
|
|
}
|
|
return core.UniqueLowerNames(suffixMatches), nil
|
|
}
|
|
|
|
// certCountRPC abstracts the choice of the SA.CountCertificatesByExactNames or
|
|
// the SA.CountCertificatesByNames RPC.
|
|
type certCountRPC func(ctx context.Context, names []string, earliest, lastest time.Time) ([]*sapb.CountByNames_MapElement, error)
|
|
|
|
// enforceNameCounts uses the provided count RPC to find a count of certificates
|
|
// for each of the names. If the count for any of the names exceeds the limit
|
|
// for the given registration then the names out of policy are returned to be
|
|
// used for a rate limit error.
|
|
func (ra *RegistrationAuthorityImpl) enforceNameCounts(
|
|
ctx context.Context,
|
|
names []string,
|
|
limit ratelimit.RateLimitPolicy,
|
|
regID int64,
|
|
countFunc certCountRPC) ([]string, error) {
|
|
|
|
now := ra.clk.Now()
|
|
windowBegin := limit.WindowBegin(now)
|
|
counts, err := countFunc(ctx, names, windowBegin, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var badNames []string
|
|
for _, entry := range counts {
|
|
// Should not happen, but be defensive.
|
|
if entry.Count == nil || entry.Name == nil {
|
|
return nil, fmt.Errorf("CountByNames_MapElement had nil Count or Name")
|
|
}
|
|
if int(*entry.Count) >= limit.GetThreshold(*entry.Name, regID) {
|
|
badNames = append(badNames, *entry.Name)
|
|
}
|
|
}
|
|
return badNames, nil
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) checkCertificatesPerNameLimit(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) error {
|
|
if features.Enabled(features.CheckRenewalFirst) {
|
|
// check if there is already an existing certificate for
|
|
// the exact name set we are issuing for. If so bypass the
|
|
// the certificatesPerName limit.
|
|
exists, err := ra.SA.FQDNSetExists(ctx, names)
|
|
if err != nil {
|
|
return fmt.Errorf("checking renewal exemption for %q: %s", names, err)
|
|
}
|
|
if exists {
|
|
ra.certsForDomainStats.Inc("FQDNSetBypass", 1)
|
|
return nil
|
|
}
|
|
}
|
|
tldNames, err := domainsForRateLimiting(names)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exactPublicSuffixes, err := suffixesForRateLimiting(names)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var badNames []string
|
|
// Domains that are exactly equal to a public suffix are treated differently
|
|
// by enforcing the limit against only exact matches to the names, not
|
|
// matches to subdomains as well. This allows the owners of such domains to
|
|
// issue certificates even though issuance from their subdomains may
|
|
// constantly exceed the rate limit.
|
|
if len(exactPublicSuffixes) > 0 {
|
|
psNamesOutOfLimit, err := ra.enforceNameCounts(ctx, exactPublicSuffixes, limit, regID, ra.SA.CountCertificatesByExactNames)
|
|
if err != nil {
|
|
return fmt.Errorf("checking certificates per name limit (exact) for %q: %s",
|
|
names, err)
|
|
}
|
|
badNames = append(badNames, psNamesOutOfLimit...)
|
|
}
|
|
|
|
// If there are any tldNames, enforce the certificate count rate limit against
|
|
// them and any subdomains.
|
|
if len(tldNames) > 0 {
|
|
namesOutOfLimit, err := ra.enforceNameCounts(ctx, tldNames, limit, regID, ra.SA.CountCertificatesByNames)
|
|
if err != nil {
|
|
return fmt.Errorf("checking certificates per name limit for %q: %s",
|
|
names, err)
|
|
}
|
|
badNames = append(badNames, namesOutOfLimit...)
|
|
}
|
|
|
|
if len(badNames) > 0 {
|
|
if !features.Enabled(features.CheckRenewalFirst) {
|
|
// check if there is already an existing certificate for
|
|
// the exact name set we are issuing for. If so bypass the
|
|
// the certificatesPerName limit.
|
|
exists, err := ra.SA.FQDNSetExists(ctx, names)
|
|
if err != nil {
|
|
return fmt.Errorf("checking renewal exemption for %q: %s", names, err)
|
|
}
|
|
if exists {
|
|
ra.certsForDomainStats.Inc("FQDNSetBypass", 1)
|
|
return nil
|
|
}
|
|
}
|
|
domains := strings.Join(badNames, ", ")
|
|
ra.certsForDomainStats.Inc("Exceeded", 1)
|
|
ra.log.Infof("Rate limit exceeded, CertificatesForDomain, regID: %d, domains: %s", regID, domains)
|
|
return berrors.RateLimitError(
|
|
"too many certificates already issued for: %s",
|
|
domains,
|
|
)
|
|
}
|
|
ra.certsForDomainStats.Inc("Pass", 1)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) checkCertificatesPerFQDNSetLimit(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) error {
|
|
count, err := ra.SA.CountFQDNSets(ctx, limit.Window.Duration, names)
|
|
if err != nil {
|
|
return fmt.Errorf("checking duplicate certificate limit for %q: %s", names, err)
|
|
}
|
|
names = core.UniqueLowerNames(names)
|
|
if int(count) >= limit.GetThreshold(strings.Join(names, ","), regID) {
|
|
return berrors.RateLimitError(
|
|
"too many certificates already issued for exact set of domains: %s",
|
|
strings.Join(names, ","),
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ra *RegistrationAuthorityImpl) checkLimits(ctx context.Context, names []string, regID int64) error {
|
|
certNameLimits := ra.rlPolicies.CertificatesPerName()
|
|
if certNameLimits.Enabled() {
|
|
err := ra.checkCertificatesPerNameLimit(ctx, names, certNameLimits, regID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
fqdnLimits := ra.rlPolicies.CertificatesPerFQDNSet()
|
|
if fqdnLimits.Enabled() {
|
|
err := ra.checkCertificatesPerFQDNSetLimit(ctx, names, fqdnLimits, regID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateRegistration updates an existing Registration with new values. Caller
|
|
// is responsible for making sure that update.Key is only different from base.Key
|
|
// if it is being called from the WFE key change endpoint.
|
|
func (ra *RegistrationAuthorityImpl) UpdateRegistration(ctx context.Context, base core.Registration, update core.Registration) (core.Registration, error) {
|
|
if changed := mergeUpdate(&base, update); !changed {
|
|
// If merging the update didn't actually change the base then our work is
|
|
// done, we can return before calling ra.SA.UpdateRegistration since theres
|
|
// nothing for the SA to do
|
|
return base, nil
|
|
}
|
|
|
|
err := ra.validateContacts(ctx, base.Contact)
|
|
if err != nil {
|
|
return core.Registration{}, err
|
|
}
|
|
|
|
err = ra.SA.UpdateRegistration(ctx, base)
|
|
if err != nil {
|
|
// berrors.InternalServerError since the user-data was validated before being
|
|
// passed to the SA.
|
|
err = berrors.InternalServerError("Could not update registration: %s", err)
|
|
return core.Registration{}, err
|
|
}
|
|
|
|
ra.stats.Inc("UpdatedRegistrations", 1)
|
|
return base, nil
|
|
}
|
|
|
|
func contactsEqual(r *core.Registration, other core.Registration) bool {
|
|
// If there is no existing contact slice, or the contact slice lengths
|
|
// differ, then the other contact is not equal
|
|
if r.Contact == nil || len(*other.Contact) != len(*r.Contact) {
|
|
return false
|
|
}
|
|
|
|
// If there is an existing contact slice and it has the same length as the
|
|
// new contact slice we need to look at each contact to determine if there
|
|
// is a change being made. Use `sort.Strings` here to ensure a consistent
|
|
// comparison
|
|
a := *other.Contact
|
|
b := *r.Contact
|
|
sort.Strings(a)
|
|
sort.Strings(b)
|
|
for i := 0; i < len(a); i++ {
|
|
// If the contact's string representation differs at any index they aren't
|
|
// equal
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// They are equal!
|
|
return true
|
|
}
|
|
|
|
// MergeUpdate copies a subset of information from the input Registration
|
|
// into the Registration r. It returns true if an update was performed and the base object
|
|
// was changed, and false if no change was made.
|
|
func mergeUpdate(r *core.Registration, input core.Registration) bool {
|
|
var changed bool
|
|
|
|
// Note: we allow input.Contact to overwrite r.Contact even if the former is
|
|
// empty in order to allow users to remove the contact associated with
|
|
// a registration. Since the field type is a pointer to slice of pointers we
|
|
// can perform a nil check to differentiate between an empty value and a nil
|
|
// (e.g. not provided) value
|
|
if input.Contact != nil && !contactsEqual(r, input) {
|
|
r.Contact = input.Contact
|
|
changed = true
|
|
}
|
|
|
|
// If there is an agreement in the input and it's not the same as the base,
|
|
// then we update the base
|
|
if len(input.Agreement) > 0 && input.Agreement != r.Agreement {
|
|
r.Agreement = input.Agreement
|
|
changed = true
|
|
}
|
|
|
|
if input.Key != nil {
|
|
if r.Key != nil {
|
|
sameKey, _ := core.PublicKeysEqual(r.Key.Key, input.Key.Key)
|
|
if !sameKey {
|
|
r.Key = input.Key
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return changed
|
|
}
|
|
|
|
// recordValidation records an authorization validation event,
|
|
// it should only be used on v2 style authorizations.
|
|
func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authID string, authExpires *time.Time, challenge *core.Challenge) error {
|
|
authzID, err := strconv.ParseInt(authID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status := string(challenge.Status)
|
|
var expires int64
|
|
if challenge.Status == core.StatusInvalid {
|
|
expires = authExpires.UnixNano()
|
|
} else {
|
|
expires = authExpires.Add(ra.authorizationLifetime).UnixNano()
|
|
}
|
|
vr, err := bgrpc.ValidationResultToPB(challenge.ValidationRecord, challenge.Error)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ra.SA.FinalizeAuthorization2(ctx, &sapb.FinalizeAuthorizationRequest{
|
|
Id: &authzID,
|
|
Status: &status,
|
|
Expires: &expires,
|
|
Attempted: &challenge.Type,
|
|
ValidationRecords: vr.Records,
|
|
ValidationError: vr.Problems,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PerformValidation initiates validation for a specific challenge associated
|
|
// with the given base authorization. The authorization and challenge are
|
|
// updated based on the results.
|
|
func (ra *RegistrationAuthorityImpl) PerformValidation(
|
|
ctx context.Context,
|
|
req *rapb.PerformValidationRequest) (*corepb.Authorization, error) {
|
|
base, err := bgrpc.PBToAuthz(req.Authz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Refuse to update expired authorizations
|
|
if base.Expires == nil || base.Expires.Before(ra.clk.Now()) {
|
|
return nil, berrors.MalformedError("expired authorization")
|
|
}
|
|
|
|
authz := base
|
|
challIndex := int(*req.ChallengeIndex)
|
|
if challIndex >= len(authz.Challenges) {
|
|
return nil,
|
|
berrors.MalformedError("invalid challenge index '%d'", challIndex)
|
|
}
|
|
|
|
ch := &authz.Challenges[challIndex]
|
|
|
|
// This challenge type may have been disabled since the challenge was created.
|
|
if !ra.PA.ChallengeTypeEnabled(ch.Type) {
|
|
return nil, berrors.MalformedError("challenge type %q no longer allowed", ch.Type)
|
|
}
|
|
|
|
// When configured with `reuseValidAuthz` we can expect some clients to try
|
|
// and update a challenge for an authorization that is already valid. In this
|
|
// case we don't need to process the challenge update. It wouldn't be helpful,
|
|
// the overall authorization is already good! We increment a stat for this
|
|
// case and return early.
|
|
if ra.reuseValidAuthz && authz.Status == core.StatusValid {
|
|
ra.stats.Inc("ReusedValidAuthzChallenge", 1)
|
|
return req.Authz, nil
|
|
}
|
|
|
|
if authz.Status != core.StatusPending {
|
|
return nil, berrors.WrongAuthorizationStateError("authorization must be pending")
|
|
}
|
|
|
|
// Look up the account key for this authorization
|
|
reg, err := ra.SA.GetRegistration(ctx, authz.RegistrationID)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError(err.Error())
|
|
}
|
|
|
|
// Compute the key authorization field based on the registration key
|
|
expectedKeyAuthorization, err := ch.ExpectedKeyAuthorization(reg.Key)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError("could not compute expected key authorization value")
|
|
}
|
|
|
|
// Populate the ProvidedKeyAuthorization such that the VA can confirm the
|
|
// expected vs actual without needing the registration key. Historically this
|
|
// was done with the value from the challenge response and so the field name
|
|
// is called "ProvidedKeyAuthorization", in reality this is just
|
|
// "KeyAuthorization".
|
|
// TODO(@cpu): Rename ProvidedKeyAuthorization to KeyAuthorization
|
|
ch.ProvidedKeyAuthorization = expectedKeyAuthorization
|
|
|
|
// Double check before sending to VA
|
|
if cErr := ch.CheckConsistencyForValidation(); cErr != nil {
|
|
return nil, berrors.MalformedError(cErr.Error())
|
|
}
|
|
|
|
ra.stats.Inc("NewPendingAuthorizations", 1)
|
|
|
|
// Dispatch to the VA for service
|
|
vaCtx := context.Background()
|
|
go func(authz core.Authorization) {
|
|
// We will mutate challenges later in this goroutine to change status and
|
|
// add error, but we also return a copy of authz immediately. To avoid a
|
|
// data race, make a copy of the challenges slice here for mutation.
|
|
challenges := make([]core.Challenge, len(authz.Challenges))
|
|
copy(challenges, authz.Challenges)
|
|
authz.Challenges = challenges
|
|
|
|
records, err := ra.VA.PerformValidation(vaCtx, authz.Identifier.Value, authz.Challenges[challIndex], authz)
|
|
var prob *probs.ProblemDetails
|
|
if p, ok := err.(*probs.ProblemDetails); ok {
|
|
prob = p
|
|
} else if err != nil {
|
|
prob = probs.ServerInternal("Could not communicate with VA")
|
|
ra.log.AuditErrf("Could not communicate with VA: %s", err)
|
|
}
|
|
|
|
// Save the updated records
|
|
challenge := &authz.Challenges[challIndex]
|
|
challenge.ValidationRecord = records
|
|
|
|
if !challenge.RecordsSane() && prob == nil {
|
|
prob = probs.ServerInternal("Records for validation failed sanity check")
|
|
}
|
|
|
|
if prob != nil {
|
|
challenge.Status = core.StatusInvalid
|
|
challenge.Error = prob
|
|
} else {
|
|
challenge.Status = core.StatusValid
|
|
}
|
|
authz.Challenges[challIndex] = *challenge
|
|
|
|
if authz.V2 {
|
|
if err := ra.recordValidation(vaCtx, authz.ID, authz.Expires, challenge); err != nil {
|
|
ra.log.AuditErrf("Could not record updated validation: err=[%s] regID=[%d] authzID=[%s]",
|
|
err, authz.RegistrationID, authz.ID)
|
|
}
|
|
} else {
|
|
err = ra.onValidationUpdate(vaCtx, authz)
|
|
if err != nil {
|
|
ra.log.AuditErrf("Could not record updated validation: err=[%s] regID=[%d] authzID=[%s]",
|
|
err, authz.RegistrationID, authz.ID)
|
|
}
|
|
}
|
|
}(authz)
|
|
ra.stats.Inc("UpdatedPendingAuthorizations", 1)
|
|
return bgrpc.AuthzToPB(authz)
|
|
}
|
|
|
|
func revokeEvent(state, serial, cn string, names []string, revocationCode revocation.Reason) string {
|
|
return fmt.Sprintf(
|
|
"Revocation - State: %s, Serial: %s, CN: %s, DNS Names: %s, Reason: %s",
|
|
state,
|
|
serial,
|
|
cn,
|
|
names,
|
|
revocation.ReasonToString[revocationCode],
|
|
)
|
|
}
|
|
|
|
// revokeCertificate generates a revoked OCSP response for the given certificate, stores
|
|
// the revocation information, and purges OCSP request URLs from Akamai.
|
|
func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, cert x509.Certificate, code revocation.Reason) error {
|
|
now := time.Now()
|
|
signRequest := core.OCSPSigningRequest{
|
|
CertDER: cert.Raw,
|
|
Status: string(core.OCSPStatusRevoked),
|
|
Reason: code,
|
|
RevokedAt: now,
|
|
}
|
|
ocspResponse, err := ra.CA.GenerateOCSP(ctx, signRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serial := core.SerialToString(cert.SerialNumber)
|
|
nowUnix := now.UnixNano()
|
|
reason := int64(code)
|
|
err = ra.SA.RevokeCertificate(ctx, &sapb.RevokeCertificateRequest{
|
|
Serial: &serial,
|
|
Reason: &reason,
|
|
Date: &nowUnix,
|
|
Response: ocspResponse,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
purgeURLs, err := akamai.GeneratePurgeURLs(cert.Raw, ra.issuer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = ra.purger.Purge(ctx, &akamaipb.PurgeRequest{Urls: purgeURLs})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RevokeCertificateWithReg terminates trust in the certificate provided.
|
|
func (ra *RegistrationAuthorityImpl) RevokeCertificateWithReg(ctx context.Context, cert x509.Certificate, revocationCode revocation.Reason, regID int64) error {
|
|
serialString := core.SerialToString(cert.SerialNumber)
|
|
err := ra.revokeCertificate(ctx, cert, revocationCode)
|
|
|
|
state := "Failure"
|
|
defer func() {
|
|
// Needed:
|
|
// Serial
|
|
// CN
|
|
// DNS names
|
|
// Revocation reason
|
|
// Registration ID of requester
|
|
// Error (if there was one)
|
|
ra.log.AuditInfof("%s, Request by registration ID: %d",
|
|
revokeEvent(state, serialString, cert.Subject.CommonName, cert.DNSNames, revocationCode),
|
|
regID)
|
|
}()
|
|
|
|
if err != nil {
|
|
state = fmt.Sprintf("Failure -- %s", err)
|
|
return err
|
|
}
|
|
|
|
state = "Success"
|
|
ra.stats.Inc("RevokedCertificates", 1)
|
|
return nil
|
|
}
|
|
|
|
// AdministrativelyRevokeCertificate terminates trust in the certificate provided and
|
|
// does not require the registration ID of the requester since this method is only
|
|
// called from the admin-revoker tool.
|
|
func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx context.Context, cert x509.Certificate, revocationCode revocation.Reason, user string) error {
|
|
serialString := core.SerialToString(cert.SerialNumber)
|
|
err := ra.revokeCertificate(ctx, cert, revocationCode)
|
|
|
|
state := "Failure"
|
|
defer func() {
|
|
// Needed:
|
|
// Serial
|
|
// CN
|
|
// DNS names
|
|
// Revocation reason
|
|
// Name of admin-revoker user
|
|
// Error (if there was one)
|
|
ra.log.AuditInfof("%s, admin-revoker user: %s",
|
|
revokeEvent(state, serialString, cert.Subject.CommonName, cert.DNSNames, revocationCode),
|
|
user)
|
|
}()
|
|
|
|
if err != nil {
|
|
state = fmt.Sprintf("Failure -- %s", err)
|
|
return err
|
|
}
|
|
|
|
state = "Success"
|
|
ra.stats.Inc("RevokedCertificates", 1)
|
|
return nil
|
|
}
|
|
|
|
// onValidationUpdate saves a validation's new status after receiving an
|
|
// authorization back from the VA.
|
|
func (ra *RegistrationAuthorityImpl) onValidationUpdate(ctx context.Context, authz core.Authorization) error {
|
|
// Consider validation successful if any of the challenges
|
|
// specified in the authorization has been fulfilled
|
|
for _, ch := range authz.Challenges {
|
|
if ch.Status == core.StatusValid {
|
|
authz.Status = core.StatusValid
|
|
}
|
|
}
|
|
|
|
// If no validation succeeded, then the authorization is invalid
|
|
if authz.Status != core.StatusValid {
|
|
authz.Status = core.StatusInvalid
|
|
} else {
|
|
exp := ra.clk.Now().Add(ra.authorizationLifetime)
|
|
authz.Expires = &exp
|
|
}
|
|
|
|
// Finalize the authorization
|
|
if err := ra.SA.FinalizeAuthorization(ctx, authz); err != nil {
|
|
return err
|
|
}
|
|
|
|
ra.stats.Inc("FinalizedAuthorizations", 1)
|
|
return nil
|
|
}
|
|
|
|
// DeactivateRegistration deactivates a valid registration
|
|
func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context, reg core.Registration) error {
|
|
if reg.Status != core.StatusValid {
|
|
return berrors.MalformedError("only valid registrations can be deactivated")
|
|
}
|
|
err := ra.SA.DeactivateRegistration(ctx, reg.ID)
|
|
if err != nil {
|
|
return berrors.InternalServerError(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeactivateAuthorization deactivates a currently valid authorization
|
|
func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context, auth core.Authorization) error {
|
|
if auth.Status != core.StatusValid && auth.Status != core.StatusPending {
|
|
return berrors.MalformedError("only valid and pending authorizations can be deactivated")
|
|
}
|
|
if auth.V2 {
|
|
authzID, err := strconv.ParseInt(auth.ID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := ra.SA.DeactivateAuthorization2(ctx, &sapb.AuthorizationID2{Id: &authzID}); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := ra.SA.DeactivateAuthorization(ctx, auth.ID); err != nil {
|
|
return berrors.InternalServerError(err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkOrderNames validates that the RA's policy authority allows issuing for
|
|
// each of the names in an order. If any of the names are unacceptable a
|
|
// malformed or rejectedIdentifier error with suberrors for each rejected
|
|
// identifier is returned.
|
|
func (ra *RegistrationAuthorityImpl) checkOrderNames(names []string) error {
|
|
idents := make([]identifier.ACMEIdentifier, len(names))
|
|
for i, name := range names {
|
|
idents[i] = identifier.DNSIdentifier(name)
|
|
}
|
|
if err := ra.PA.WillingToIssueWildcards(idents); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewOrder creates a new order object
|
|
func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.NewOrderRequest) (*corepb.Order, error) {
|
|
order := &corepb.Order{
|
|
RegistrationID: req.RegistrationID,
|
|
Names: core.UniqueLowerNames(req.Names),
|
|
}
|
|
|
|
// Validate that our policy allows issuing for each of the names in the order
|
|
if err := ra.checkOrderNames(order.Names); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := wildcardOverlap(order.Names); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// See if there is an existing unexpired pending (or ready) order that can be reused
|
|
// for this account
|
|
useV2Authzs := features.Enabled(features.NewAuthorizationSchema)
|
|
existingOrder, err := ra.SA.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{
|
|
AcctID: order.RegistrationID,
|
|
Names: order.Names,
|
|
UseV2Authorizations: &useV2Authzs,
|
|
})
|
|
// If there was an error and it wasn't an acceptable "NotFound" error, return
|
|
// immediately
|
|
if err != nil && !berrors.Is(err, berrors.NotFound) {
|
|
return nil, err
|
|
}
|
|
// If there was an order, return it
|
|
if existingOrder != nil {
|
|
return existingOrder, nil
|
|
}
|
|
// Otherwise we were unable to find an order to reuse, continue creating a new
|
|
// order
|
|
|
|
// Check if there is rate limit space for a new order within the current window
|
|
if err := ra.checkNewOrdersPerAccountLimit(ctx, *order.RegistrationID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if features.Enabled(features.EarlyOrderRateLimit) {
|
|
// Check if there is rate limit space for issuing a certificate for the new
|
|
// order's names. If there isn't then it doesn't make sense to allow creating
|
|
// an order - it will just fail when finalization checks the same limits.
|
|
if err := ra.checkLimits(ctx, order.Names, *order.RegistrationID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// An order's lifetime is effectively bound by the shortest remaining lifetime
|
|
// of its associated authorizations. For that reason it would be Uncool if
|
|
// `sa.GetAuthorizations` returned an authorization that was very close to
|
|
// expiry. The resulting pending order that references it would itself end up
|
|
// expiring very soon.
|
|
// To prevent this we only return authorizations that are at least 1 day away
|
|
// from expiring.
|
|
authzExpiryCutoff := ra.clk.Now().AddDate(0, 0, 1).UnixNano()
|
|
|
|
// We do not want any legacy V1 API authorizations not associated with an
|
|
// order to be returned from the SA so we set requireV2Authzs to true
|
|
requireV2Authzs := true
|
|
var existingAuthz *sapb.Authorizations
|
|
getAuthReq := &sapb.GetAuthorizationsRequest{
|
|
RegistrationID: order.RegistrationID,
|
|
Now: &authzExpiryCutoff,
|
|
Domains: order.Names,
|
|
RequireV2Authzs: &requireV2Authzs,
|
|
}
|
|
if features.Enabled(features.NewAuthorizationSchema) {
|
|
existingAuthz, err = ra.SA.GetAuthorizations2(ctx, getAuthReq)
|
|
} else {
|
|
existingAuthz, err = ra.SA.GetAuthorizations(ctx, getAuthReq)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Collect up the authorizations we found into a map keyed by the domains the
|
|
// authorizations correspond to
|
|
nameToExistingAuthz := make(map[string]*corepb.Authorization, len(order.Names))
|
|
for _, v := range existingAuthz.Authz {
|
|
// Don't reuse a valid authorization if the reuseValidAuthz flag is
|
|
// disabled.
|
|
if *v.Authz.Status == string(core.StatusValid) && !ra.reuseValidAuthz {
|
|
continue
|
|
}
|
|
nameToExistingAuthz[*v.Domain] = v.Authz
|
|
}
|
|
|
|
// For each of the names in the order, if there is an acceptable
|
|
// existing authz, append it to the order to reuse it. Otherwise track
|
|
// that there is a missing authz for that name.
|
|
var missingAuthzNames []string
|
|
for _, name := range order.Names {
|
|
// If there isn't an existing authz, note that its missing and continue
|
|
if _, exists := nameToExistingAuthz[name]; !exists {
|
|
missingAuthzNames = append(missingAuthzNames, name)
|
|
continue
|
|
}
|
|
authz := nameToExistingAuthz[name]
|
|
// If the identifier is a wildcard and the existing authz only has one
|
|
// DNS-01 type challenge we can reuse it. In theory we will
|
|
// never get back an authorization for a domain with a wildcard prefix
|
|
// that doesn't meet this criteria from SA.GetAuthorizations but we verify
|
|
// again to be safe.
|
|
if strings.HasPrefix(name, "*.") &&
|
|
len(authz.Challenges) == 1 && *authz.Challenges[0].Type == core.ChallengeTypeDNS01 {
|
|
if *authz.V2 {
|
|
authzID, err := strconv.ParseInt(*authz.Id, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
order.V2Authorizations = append(order.V2Authorizations, authzID)
|
|
} else {
|
|
order.Authorizations = append(order.Authorizations, *authz.Id)
|
|
}
|
|
continue
|
|
} else if !strings.HasPrefix(name, "*.") {
|
|
// If the identifier isn't a wildcard, we can reuse any authz
|
|
if *authz.V2 {
|
|
authzID, err := strconv.ParseInt(*authz.Id, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
order.V2Authorizations = append(order.V2Authorizations, authzID)
|
|
} else {
|
|
order.Authorizations = append(order.Authorizations, *authz.Id)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Delete the authz from the nameToExistingAuthz map since we are not reusing it.
|
|
delete(nameToExistingAuthz, name)
|
|
// If we reached this point then the existing authz was not acceptable for
|
|
// reuse and we need to mark the name as requiring a new pending authz
|
|
missingAuthzNames = append(missingAuthzNames, name)
|
|
}
|
|
|
|
// If the order isn't fully authorized we need to check that the client has
|
|
// rate limit room for more pending authorizations
|
|
if len(missingAuthzNames) > 0 {
|
|
if err := ra.checkPendingAuthorizationLimit(ctx, *order.RegistrationID); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Loop through each of the names missing authzs and create a new pending
|
|
// authorization for each.
|
|
var newAuthzs []*corepb.Authorization
|
|
v2 := features.Enabled(features.NewAuthorizationSchema)
|
|
for _, name := range missingAuthzNames {
|
|
// TODO(#3069): Batch this check
|
|
if err := ra.checkInvalidAuthorizationLimit(ctx, *order.RegistrationID, name); err != nil {
|
|
return nil, err
|
|
}
|
|
pb, err := ra.createPendingAuthz(ctx, *order.RegistrationID, identifier.ACMEIdentifier{
|
|
Type: identifier.DNS,
|
|
Value: name,
|
|
}, v2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newAuthzs = append(newAuthzs, pb)
|
|
}
|
|
|
|
// Start with the order's own expiry as the minExpiry. We only care
|
|
// about authz expiries that are sooner than the order's expiry
|
|
minExpiry := ra.clk.Now().Add(ra.orderLifetime)
|
|
|
|
// Check the reused authorizations to see if any have an expiry before the
|
|
// minExpiry (the order's lifetime)
|
|
for _, authz := range nameToExistingAuthz {
|
|
// An authz without an expiry is an unexpected internal server event
|
|
if authz.Expires == nil {
|
|
return nil, berrors.InternalServerError(
|
|
"SA.GetAuthorizations returned an authz (%s) with nil expiry",
|
|
*authz.Id)
|
|
}
|
|
// If the reused authorization expires before the minExpiry, it's expiry
|
|
// is the new minExpiry.
|
|
authzExpiry := time.Unix(0, *authz.Expires)
|
|
if authzExpiry.Before(minExpiry) {
|
|
minExpiry = authzExpiry
|
|
}
|
|
}
|
|
|
|
// If new authorizations are needed, call AddPendingAuthorizations. Also check
|
|
// whether the newly created pending authz's have an expiry lower than minExpiry
|
|
if len(newAuthzs) > 0 {
|
|
req := sapb.AddPendingAuthorizationsRequest{Authz: newAuthzs}
|
|
if v2 {
|
|
authzIDs, err := ra.SA.NewAuthorizations2(ctx, &req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
order.V2Authorizations = append(order.V2Authorizations, authzIDs.Ids...)
|
|
} else {
|
|
authzIDs, err := ra.SA.AddPendingAuthorizations(ctx, &req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
order.Authorizations = append(order.Authorizations, authzIDs.Ids...)
|
|
}
|
|
// If the newly created pending authz's have an expiry closer than the
|
|
// minExpiry the minExpiry is the pending authz expiry.
|
|
newPendingAuthzExpires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime)
|
|
if newPendingAuthzExpires.Before(minExpiry) {
|
|
minExpiry = newPendingAuthzExpires
|
|
}
|
|
}
|
|
|
|
// Note how many names are being requested in this certificate order.
|
|
ra.namesPerCert.With(
|
|
prometheus.Labels{"type": "requested"},
|
|
).Observe(float64(len(order.Names)))
|
|
|
|
// Set the order's expiry to the minimum expiry
|
|
minExpiryTS := minExpiry.UnixNano()
|
|
order.Expires = &minExpiryTS
|
|
storedOrder, err := ra.SA.NewOrder(ctx, order)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return storedOrder, nil
|
|
}
|
|
|
|
// createPendingAuthz checks that a name is allowed for issuance and creates the
|
|
// necessary challenges for it and puts this and all of the relevant information
|
|
// into a corepb.Authorization for transmission to the SA to be stored
|
|
func (ra *RegistrationAuthorityImpl) createPendingAuthz(ctx context.Context, reg int64, identifier identifier.ACMEIdentifier, v2 bool) (*corepb.Authorization, error) {
|
|
expires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime).Truncate(time.Second).UnixNano()
|
|
status := string(core.StatusPending)
|
|
authz := &corepb.Authorization{
|
|
Identifier: &identifier.Value,
|
|
RegistrationID: ®,
|
|
Status: &status,
|
|
Expires: &expires,
|
|
V2: &v2,
|
|
}
|
|
|
|
// Create challenges. The WFE will update them with URIs before sending them out.
|
|
challenges, err := ra.PA.ChallengesFor(identifier)
|
|
if err != nil {
|
|
// The only time ChallengesFor errors it is a fatal configuration error
|
|
// where challenges required by policy for an identifier are not enabled. We
|
|
// want to treat this as an internal server error.
|
|
return nil, berrors.InternalServerError(err.Error())
|
|
}
|
|
// Check each challenge for sanity.
|
|
for _, challenge := range challenges {
|
|
if err := challenge.CheckConsistencyForClientOffer(); err != nil {
|
|
// berrors.InternalServerError because we generated these challenges, they should
|
|
// be OK.
|
|
err = berrors.InternalServerError("challenge didn't pass sanity check: %+v", challenge)
|
|
return nil, err
|
|
}
|
|
challPB, err := bgrpc.ChallengeToPB(challenge)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authz.Challenges = append(authz.Challenges, challPB)
|
|
}
|
|
return authz, nil
|
|
}
|
|
|
|
// authzValidChallengeEnabled checks whether the valid challenge in an authorization uses a type
|
|
// which is still enabled for given regID
|
|
func (ra *RegistrationAuthorityImpl) authzValidChallengeEnabled(authz *core.Authorization) bool {
|
|
for _, chall := range authz.Challenges {
|
|
if chall.Status == core.StatusValid {
|
|
return ra.PA.ChallengeTypeEnabled(chall.Type)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// wildcardOverlap takes a slice of domain names and returns an error if any of
|
|
// them is a non-wildcard FQDN that overlaps with a wildcard domain in the map.
|
|
func wildcardOverlap(dnsNames []string) error {
|
|
nameMap := make(map[string]bool, len(dnsNames))
|
|
for _, v := range dnsNames {
|
|
nameMap[v] = true
|
|
}
|
|
for name := range nameMap {
|
|
if name[0] == '*' {
|
|
continue
|
|
}
|
|
labels := strings.Split(name, ".")
|
|
labels[0] = "*"
|
|
if nameMap[strings.Join(labels, ".")] {
|
|
return berrors.MalformedError(
|
|
"Domain name %q is redundant with a wildcard domain in the same request. Remove one or the other from the certificate request.", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|