boulder/sa/sa.go

2547 lines
79 KiB
Go

package sa
import (
"context"
"crypto/sha256"
"crypto/x509"
"database/sql"
"encoding/json"
"fmt"
"math/big"
"net"
"strings"
"sync"
"time"
"github.com/jmhodges/clock"
"gopkg.in/go-gorp/gorp.v2"
jose "gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/identifier"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/revocation"
sapb "github.com/letsencrypt/boulder/sa/proto"
)
type certCountFunc func(db dbSelector, domain string, earliest, latest time.Time) (int, error)
type getChallengesFunc func(db dbSelector, authID string) ([]core.Challenge, error)
// SQLStorageAuthority defines a Storage Authority
type SQLStorageAuthority struct {
dbMap *gorp.DbMap
clk clock.Clock
log blog.Logger
// For RPCs that generate multiple, parallelizable SQL queries, this is the
// max parallelism they will use (to avoid consuming too many MariaDB
// threads).
parallelismPerRPC int
// We use function types here so we can mock out this internal function in
// unittests.
countCertificatesByName certCountFunc
getChallenges getChallengesFunc
}
func digest256(data []byte) []byte {
d := sha256.New()
_, _ = d.Write(data) // Never returns an error
return d.Sum(nil)
}
// Utility models
type pendingauthzModel struct {
core.Authorization
LockCol int64
}
type authzModel struct {
core.Authorization
}
// orderFQDNSet contains the SHA256 hash of the lowercased, comma joined names
// from a new-order request, along with the corresponding orderID, the
// registration ID, and the order expiry. This is used to find
// existing orders for reuse.
type orderFQDNSet struct {
ID int64
SetHash []byte
OrderID int64
RegistrationID int64
Expires time.Time
}
const (
authorizationTable = "authz"
pendingAuthorizationTable = "pendingAuthorizations"
)
var authorizationTables = []string{
authorizationTable,
pendingAuthorizationTable,
}
// NewSQLStorageAuthority provides persistence using a SQL backend for
// Boulder. It will modify the given gorp.DbMap by adding relevant tables.
func NewSQLStorageAuthority(
dbMap *gorp.DbMap,
clk clock.Clock,
logger blog.Logger,
scope metrics.Scope,
parallelismPerRPC int,
) (*SQLStorageAuthority, error) {
SetSQLDebug(dbMap, logger)
ssa := &SQLStorageAuthority{
dbMap: dbMap,
clk: clk,
log: logger,
parallelismPerRPC: parallelismPerRPC,
}
ssa.countCertificatesByName = ssa.countCertificates
ssa.getChallenges = ssa.getChallengesImpl
return ssa, nil
}
func statusIsPending(status core.AcmeStatus) bool {
return status == core.StatusPending || status == core.StatusProcessing || status == core.StatusUnknown
}
func existingPending(db dbOneSelector, id string) bool {
var count int64
_ = db.SelectOne(&count, "SELECT count(*) FROM pendingAuthorizations WHERE id = :id", map[string]interface{}{"id": id})
return count > 0
}
func existingFinal(db dbOneSelector, id string) bool {
var count int64
_ = db.SelectOne(&count, "SELECT count(*) FROM authz WHERE id = :id", map[string]interface{}{"id": id})
return count > 0
}
func existingRegistration(tx *gorp.Transaction, id int64) bool {
var count int64
_ = tx.SelectOne(&count, "SELECT count(*) FROM registrations WHERE id = :id", map[string]interface{}{"id": id})
return count > 0
}
// updateChallenge writes the valid or invalid challenge in a list of challenges
// to the database. It deletes from the database any of those challenges that
// weren't used (i.e. aren't "valid" or "invalid"), so long as the
// DeleteUnusedChallenges flag is set.
func updateChallenges(db dbSelectExecer, authID string, challenges []core.Challenge) error {
var challs []challModel
_, err := db.Select(
&challs,
getChallengesQuery,
map[string]interface{}{"authID": authID},
)
if err != nil {
return err
}
if len(challs) != len(challenges) {
return fmt.Errorf("Invalid number of challenges provided")
}
for i, authChall := range challenges {
if challs[i].AuthorizationID != authID {
return fmt.Errorf("challenge authorization ID %q didn't match associated authorization ID %q", challs[i].AuthorizationID, authID)
}
chall, err := challengeToModel(&authChall, authID)
if err != nil {
return err
}
chall.ID = challs[i].ID
if authChall.Status == core.StatusInvalid || authChall.Status == core.StatusValid {
_, err = db.Exec(
`UPDATE challenges SET
status = ?,
error = ?,
validationRecord = ?
WHERE status = ? AND id = ?`,
string(chall.Status),
chall.Error,
chall.ValidationRecord,
string(core.StatusPending),
chall.ID)
if err != nil {
return err
}
} else if features.Enabled(features.DeleteUnusedChallenges) {
_, err = db.Exec(
`DELETE FROM challenges WHERE id = ?`,
chall.ID)
if err != nil {
return err
}
}
}
return nil
}
// GetRegistration obtains a Registration by ID
func (ssa *SQLStorageAuthority) GetRegistration(ctx context.Context, id int64) (core.Registration, error) {
const query = "WHERE id = ?"
model, err := selectRegistration(ssa.dbMap.WithContext(ctx), query, id)
if err == sql.ErrNoRows {
return core.Registration{}, berrors.NotFoundError("registration with ID '%d' not found", id)
}
if err != nil {
return core.Registration{}, err
}
return modelToRegistration(model)
}
// GetRegistrationByKey obtains a Registration by JWK
func (ssa *SQLStorageAuthority) GetRegistrationByKey(ctx context.Context, key *jose.JSONWebKey) (core.Registration, error) {
const query = "WHERE jwk_sha256 = ?"
if key == nil {
return core.Registration{}, fmt.Errorf("key argument to GetRegistrationByKey must not be nil")
}
sha, err := core.KeyDigest(key.Key)
if err != nil {
return core.Registration{}, err
}
model, err := selectRegistration(ssa.dbMap.WithContext(ctx), query, sha)
if err == sql.ErrNoRows {
return core.Registration{}, berrors.NotFoundError("no registrations with public key sha256 %q", sha)
}
if err != nil {
return core.Registration{}, err
}
return modelToRegistration(model)
}
// GetAuthorization obtains an Authorization by ID
func (ssa *SQLStorageAuthority) GetAuthorization(ctx context.Context, id string) (core.Authorization, error) {
authz, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
pa, err := selectPendingAuthz(txWithCtx, "WHERE id = ?", id)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
var authz core.Authorization
if err == sql.ErrNoRows {
var fa authzModel
err := txWithCtx.SelectOne(&fa, fmt.Sprintf("SELECT %s FROM authz WHERE id = ?", authzFields), id)
if err != nil && err != sql.ErrNoRows {
return nil, err
} else if err == sql.ErrNoRows {
// If there was no result in either the pending authz table or the authz
// table then return a `berrors.NotFound` instance
return nil, berrors.NotFoundError("no authorization found with id %q", id)
}
authz = fa.Authorization
} else {
authz = pa.Authorization
}
authz.Challenges, err = ssa.getChallenges(txWithCtx, authz.ID)
if err != nil {
return nil, err
}
return authz, nil
})
if overallError != nil {
return core.Authorization{}, overallError
}
if authz, ok := authz.(core.Authorization); ok {
return authz, nil
}
return core.Authorization{}, fmt.Errorf("shouldn't happen: casting error in GetAuthorization")
}
// GetValidAuthorizations returns the latest authorization object for all
// domain names from the parameters that the account has authorizations for.
func (ssa *SQLStorageAuthority) GetValidAuthorizations(
ctx context.Context,
registrationID int64,
names []string,
now time.Time) (map[string]*core.Authorization, error) {
return ssa.getAuthorizations(
ctx,
authorizationTable,
string(core.StatusValid),
registrationID,
names,
now,
false)
}
// incrementIP returns a copy of `ip` incremented at a bit index `index`,
// or in other words the first IP of the next highest subnet given a mask of
// length `index`.
// In order to easily account for overflow, we treat ip as a big.Int and add to
// it. If the increment overflows the max size of a net.IP, return the highest
// possible net.IP.
func incrementIP(ip net.IP, index int) net.IP {
bigInt := new(big.Int)
bigInt.SetBytes([]byte(ip))
incr := new(big.Int).Lsh(big.NewInt(1), 128-uint(index))
bigInt.Add(bigInt, incr)
// bigInt.Bytes can be shorter than 16 bytes, so stick it into a
// full-sized net.IP.
resultBytes := bigInt.Bytes()
if len(resultBytes) > 16 {
return net.ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
}
result := make(net.IP, 16)
copy(result[16-len(resultBytes):], resultBytes)
return result
}
// ipRange returns a range of IP addresses suitable for querying MySQL for the
// purpose of rate limiting using a range that is inclusive on the lower end and
// exclusive at the higher end. If ip is an IPv4 address, it returns that address,
// plus the one immediately higher than it. If ip is an IPv6 address, it applies
// a /48 mask to it and returns the lowest IP in the resulting network, and the
// first IP outside of the resulting network.
func ipRange(ip net.IP) (net.IP, net.IP) {
ip = ip.To16()
// For IPv6, match on a certain subnet range, since one person can commonly
// have an entire /48 to themselves.
maskLength := 48
// For IPv4 addresses, do a match on exact address, so begin = ip and end =
// next higher IP.
if ip.To4() != nil {
maskLength = 128
}
mask := net.CIDRMask(maskLength, 128)
begin := ip.Mask(mask)
end := incrementIP(begin, maskLength)
return begin, end
}
// CountRegistrationsByIP returns the number of registrations created in the
// time range for a single IP address.
func (ssa *SQLStorageAuthority) CountRegistrationsByIP(ctx context.Context, ip net.IP, earliest time.Time, latest time.Time) (int, error) {
var count int64
err := ssa.dbMap.WithContext(ctx).SelectOne(
&count,
`SELECT COUNT(1) FROM registrations
WHERE
initialIP = :ip AND
:earliest < createdAt AND
createdAt <= :latest`,
map[string]interface{}{
"ip": []byte(ip),
"earliest": earliest,
"latest": latest,
})
if err != nil {
return -1, err
}
return int(count), nil
}
// CountRegistrationsByIPRange returns the number of registrations created in
// the time range in an IP range. For IPv4 addresses, that range is limited to
// the single IP. For IPv6 addresses, that range is a /48, since it's not
// uncommon for one person to have a /48 to themselves.
func (ssa *SQLStorageAuthority) CountRegistrationsByIPRange(ctx context.Context, ip net.IP, earliest time.Time, latest time.Time) (int, error) {
var count int64
beginIP, endIP := ipRange(ip)
err := ssa.dbMap.WithContext(ctx).SelectOne(
&count,
`SELECT COUNT(1) FROM registrations
WHERE
:beginIP <= initialIP AND
initialIP < :endIP AND
:earliest < createdAt AND
createdAt <= :latest`,
map[string]interface{}{
"earliest": earliest,
"latest": latest,
"beginIP": []byte(beginIP),
"endIP": []byte(endIP),
})
if err != nil {
return -1, err
}
return int(count), nil
}
// CountCertificatesByNames counts, for each input domain, the number of
// certificates issued in the given time range for that domain and its
// subdomains. It returns a map from domains to counts, which is guaranteed to
// contain an entry for each input domain, so long as err is nil.
// Queries will be run in parallel. If any of them error, only one error will
// be returned.
func (ssa *SQLStorageAuthority) CountCertificatesByNames(ctx context.Context, domains []string, earliest, latest time.Time) ([]*sapb.CountByNames_MapElement, error) {
work := make(chan string, len(domains))
type result struct {
err error
count int
domain string
}
results := make(chan result, len(domains))
for _, domain := range domains {
work <- domain
}
close(work)
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// We may perform up to 100 queries, depending on what's in the certificate
// request. Parallelize them so we don't hit our timeout, but limit the
// parallelism so we don't consume too many threads on the database.
for i := 0; i < ssa.parallelismPerRPC; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for domain := range work {
select {
case <-ctx.Done():
results <- result{err: ctx.Err()}
return
default:
}
currentCount, err := ssa.countCertificatesByName(
ssa.dbMap.WithContext(ctx), domain, earliest, latest)
if err != nil {
results <- result{err: err}
// Skip any further work
cancel()
return
}
results <- result{
count: currentCount,
domain: domain,
}
}
}()
}
wg.Wait()
close(results)
var ret []*sapb.CountByNames_MapElement
for r := range results {
if r.err != nil {
return nil, r.err
}
name := string(r.domain)
pbCount := int64(r.count)
ret = append(ret, &sapb.CountByNames_MapElement{
Name: &name,
Count: &pbCount,
})
}
return ret, nil
}
func ReverseName(domain string) string {
labels := strings.Split(domain, ".")
for i, j := 0, len(labels)-1; i < j; i, j = i+1, j-1 {
labels[i], labels[j] = labels[j], labels[i]
}
return strings.Join(labels, ".")
}
// GetCertificate takes a serial number and returns the corresponding
// certificate, or error if it does not exist.
func (ssa *SQLStorageAuthority) GetCertificate(ctx context.Context, serial string) (core.Certificate, error) {
if !core.ValidSerial(serial) {
err := fmt.Errorf("Invalid certificate serial %s", serial)
return core.Certificate{}, err
}
cert, err := SelectCertificate(ssa.dbMap.WithContext(ctx), "WHERE serial = ?", serial)
if err == sql.ErrNoRows {
return core.Certificate{}, berrors.NotFoundError("certificate with serial %q not found", serial)
}
if err != nil {
return core.Certificate{}, err
}
return cert, err
}
// GetPrecertificate takes a serial number and returns the corresponding
// precertificate, or error if it does not exist.
func (ssa *SQLStorageAuthority) GetPrecertificate(ctx context.Context, reqSerial *sapb.Serial) (*corepb.Certificate, error) {
if !core.ValidSerial(*reqSerial.Serial) {
return nil,
fmt.Errorf("Invalid precertificate serial %q", *reqSerial.Serial)
}
cert, err := selectPrecertificate(ssa.dbMap.WithContext(ctx), *reqSerial.Serial)
if err == sql.ErrNoRows {
return nil,
berrors.NotFoundError("precertificate with serial %q not found", *reqSerial.Serial)
}
if err != nil {
return nil, err
}
return bgrpc.CertToPB(cert), nil
}
// GetCertificateStatus takes a hexadecimal string representing the full 128-bit serial
// number of a certificate and returns data about that certificate's current
// validity.
func (ssa *SQLStorageAuthority) GetCertificateStatus(ctx context.Context, serial string) (core.CertificateStatus, error) {
if !core.ValidSerial(serial) {
err := fmt.Errorf("Invalid certificate serial %s", serial)
return core.CertificateStatus{}, err
}
var status core.CertificateStatus
statusObj, err := ssa.dbMap.WithContext(ctx).Get(certStatusModel{}, serial)
if err != nil {
return status, err
}
if statusObj == nil {
return status, nil
}
statusModel := statusObj.(*certStatusModel)
status = core.CertificateStatus{
Serial: statusModel.Serial,
Status: statusModel.Status,
OCSPLastUpdated: statusModel.OCSPLastUpdated,
RevokedDate: statusModel.RevokedDate,
RevokedReason: statusModel.RevokedReason,
LastExpirationNagSent: statusModel.LastExpirationNagSent,
OCSPResponse: statusModel.OCSPResponse,
NotAfter: statusModel.NotAfter,
IsExpired: statusModel.IsExpired,
}
return status, nil
}
// NewRegistration stores a new Registration
func (ssa *SQLStorageAuthority) NewRegistration(ctx context.Context, reg core.Registration) (core.Registration, error) {
reg.CreatedAt = ssa.clk.Now()
rm, err := registrationToModel(&reg)
if err != nil {
return reg, err
}
err = ssa.dbMap.WithContext(ctx).Insert(rm)
if err != nil {
if strings.HasPrefix(err.Error(), "Error 1062: Duplicate entry") {
// duplicate entry error can only happen when jwk_sha256 collides, indicate
// to caller that the provided key is already in use
return reg, berrors.DuplicateError("key is already in use for a different account")
}
return reg, err
}
return modelToRegistration(rm)
}
// UpdateRegistration stores an updated Registration
func (ssa *SQLStorageAuthority) UpdateRegistration(ctx context.Context, reg core.Registration) error {
const query = "WHERE id = ?"
model, err := selectRegistration(ssa.dbMap.WithContext(ctx), query, reg.ID)
if err != nil {
if err == sql.ErrNoRows {
return berrors.NotFoundError("registration with ID '%d' not found", reg.ID)
}
return err
}
updatedRegModel, err := registrationToModel(&reg)
if err != nil {
return err
}
// Copy the existing registration model's LockCol to the new updated
// registration model's LockCol
updatedRegModel.LockCol = model.LockCol
n, err := ssa.dbMap.WithContext(ctx).Update(updatedRegModel)
if err != nil {
if strings.HasPrefix(err.Error(), "Error 1062: Duplicate entry") {
// duplicate entry error can only happen when jwk_sha256 collides, indicate
// to caller that the provided key is already in use
return berrors.DuplicateError("key is already in use for a different account")
}
return err
}
if n == 0 {
return berrors.NotFoundError("registration with ID '%d' not found", reg.ID)
}
return nil
}
// NewPendingAuthorization retrieves a pending authorization for
// authz.Identifier if one exists, or creates a new one otherwise.
func (ssa *SQLStorageAuthority) NewPendingAuthorization(ctx context.Context, authz core.Authorization) (core.Authorization, error) {
output, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
// Create a random ID and check that it doesn't exist already
authz.ID = core.NewToken()
for existingPending(txWithCtx, authz.ID) ||
existingFinal(txWithCtx, authz.ID) {
authz.ID = core.NewToken()
}
// Insert a stub row in pending
pendingAuthz := pendingauthzModel{Authorization: authz}
err := txWithCtx.Insert(&pendingAuthz)
if err != nil {
return nil, err
}
for i, c := range authz.Challenges {
challModel, err := challengeToModel(&c, pendingAuthz.ID)
if err != nil {
return nil, err
}
// Magic happens here: Gorp will modify challModel, setting challModel.ID
// to the auto-increment primary key. This is important because we want
// the challenge objects inside the Authorization we return to know their
// IDs, so they can have proper URLs.
// See https://godoc.org/github.com/coopernurse/gorp#DbMap.Insert
err = txWithCtx.Insert(challModel)
if err != nil {
return nil, err
}
challenge, err := modelToChallenge(challModel)
if err != nil {
return nil, err
}
authz.Challenges[i] = challenge
}
pendingAuthz.Authorization.Challenges = authz.Challenges
return pendingAuthz.Authorization, nil
})
if overallError != nil {
return core.Authorization{}, overallError
}
if output, ok := output.(core.Authorization); ok {
return output, nil
}
return core.Authorization{}, fmt.Errorf("shouldn't happen: casting error in NewPendingAuthorization")
}
// GetPendingAuthorization returns the most recent Pending authorization
// with the given identifier, if available.
func (ssa *SQLStorageAuthority) GetPendingAuthorization(
ctx context.Context,
req *sapb.GetPendingAuthorizationRequest,
) (*core.Authorization, error) {
identifierJSON, err := json.Marshal(identifier.ACMEIdentifier{
Type: identifier.IdentifierType(*req.IdentifierType),
Value: *req.IdentifierValue,
})
if err != nil {
return nil, err
}
// Note: This will use the index on `registrationId`, `expires`, which should
// keep the amount of scanning to a minimum. That index does not include the
// identifier, so accounts with huge numbers of pending authzs may result in
// slow queries here.
pa, err := selectPendingAuthz(ssa.dbMap.WithContext(ctx),
`WHERE registrationID = :regID
AND identifier = :identifierJSON
AND status = :status
AND expires > :validUntil
ORDER BY expires ASC
LIMIT 1`,
map[string]interface{}{
"regID": *req.RegistrationID,
"identifierJSON": identifierJSON,
"status": string(core.StatusPending),
"validUntil": time.Unix(0, *req.ValidUntil),
})
if err == sql.ErrNoRows {
return nil, berrors.NotFoundError("pending authz not found")
} else if err == nil {
// We found an authz, but we still need to fetch its challenges. To
// simplify things, just call GetAuthorization, which takes care of that.
authz, err := ssa.GetAuthorization(ctx, pa.ID)
return &authz, err
} else {
// Any error other than ErrNoRows; return the error
return nil, err
}
}
// FinalizeAuthorization converts a Pending Authorization to a final one. If the
// Authorization is not found a berrors.NotFound result is returned. If the
// Authorization is status pending a berrors.InternalServer error is returned.
func (ssa *SQLStorageAuthority) FinalizeAuthorization(ctx context.Context, authz core.Authorization) error {
_, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
// Check that a pending authz exists
if !existingPending(txWithCtx, authz.ID) {
return nil, berrors.NotFoundError("authorization with ID %q not found", authz.ID)
}
if statusIsPending(authz.Status) {
return nil, berrors.InternalServerError("authorization to finalize is pending (ID %q)", authz.ID)
}
auth := &authzModel{authz}
pa, err := selectPendingAuthz(txWithCtx, "WHERE id = ?", authz.ID)
if err == sql.ErrNoRows {
return nil, berrors.NotFoundError("authorization with ID %q not found", authz.ID)
}
if err != nil {
return nil, err
}
err = txWithCtx.Insert(auth)
if err != nil {
return nil, err
}
_, err = txWithCtx.Delete(pa)
if err != nil {
return nil, err
}
err = updateChallenges(txWithCtx, authz.ID, authz.Challenges)
if err != nil {
return nil, err
}
return nil, nil
})
return overallError
}
func (ssa *SQLStorageAuthority) getAuthorizationIDsByDomain2(ctx context.Context, domain string) ([]int64, error) {
var ids []int64
_, err := ssa.dbMap.Select(
&ids,
`SELECT id FROM authz2 WHERE identifierValue = :ident AND status IN (:pending,:valid) AND expires > :expires LIMIT :limit`,
map[string]interface{}{
"ident": domain,
"pending": statusUint(core.StatusPending),
"valid": statusUint(core.StatusValid),
"expires": ssa.clk.Now(),
"limit": getAuthorizationIDsMax,
},
)
if err != nil {
return nil, err
}
return ids, nil
}
// AddCertificate stores an issued certificate and returns the digest as
// a string, or an error if any occurred.
func (ssa *SQLStorageAuthority) AddCertificate(
ctx context.Context,
certDER []byte,
regID int64,
ocspResponse []byte,
issued *time.Time) (string, error) {
parsedCertificate, err := x509.ParseCertificate(certDER)
if err != nil {
return "", err
}
digest := core.Fingerprint256(certDER)
serial := core.SerialToString(parsedCertificate.SerialNumber)
cert := &core.Certificate{
RegistrationID: regID,
Serial: serial,
Digest: digest,
DER: certDER,
Issued: *issued,
Expires: parsedCertificate.NotAfter,
}
certStatus := &certStatusModel{
Status: core.OCSPStatus("good"),
OCSPLastUpdated: time.Time{},
OCSPResponse: []byte{},
Serial: serial,
RevokedDate: time.Time{},
RevokedReason: 0,
NotAfter: parsedCertificate.NotAfter,
}
if len(ocspResponse) != 0 {
certStatus.OCSPResponse = ocspResponse
certStatus.OCSPLastUpdated = ssa.clk.Now()
}
_, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
err = txWithCtx.Insert(cert)
if err != nil {
if strings.HasPrefix(err.Error(), "Error 1062: Duplicate entry") {
return nil, berrors.DuplicateError("cannot add a duplicate cert")
}
return nil, err
}
err = txWithCtx.Insert(certStatus)
if err != nil {
// We ignore "duplicate entry" on insert to the certificateStatus table
// because we may be inserting a certificate after a call to
// AddPrecertificate, which also adds a certificateStatus entry.
if !strings.HasPrefix(err.Error(), "Error 1062: Duplicate entry") {
return nil, err
}
}
// NOTE(@cpu): When we collect up names to check if an FQDN set exists (e.g.
// that it is a renewal) we use just the DNSNames from the certificate and
// ignore the Subject Common Name (if any). This is a safe assumption because
// if a certificate we issued were to have a Subj. CN not present as a SAN it
// would be a misissuance and miscalculating whether the cert is a renewal or
// not for the purpose of rate limiting is the least of our troubles.
isRenewal, err := ssa.checkFQDNSetExists(
txWithCtx.SelectOne,
parsedCertificate.DNSNames)
if err != nil {
return nil, err
}
err = addIssuedNames(txWithCtx, parsedCertificate, isRenewal)
if err != nil {
return nil, err
}
// Add to the rate limit table, but only for new certificates. Renewals
// don't count against the certificatesPerName limit.
if !isRenewal {
timeToTheHour := parsedCertificate.NotBefore.Round(time.Hour)
err = ssa.addCertificatesPerName(ctx, txWithCtx, parsedCertificate.DNSNames, timeToTheHour)
if err != nil {
return nil, err
}
}
err = addFQDNSet(
txWithCtx,
parsedCertificate.DNSNames,
serial,
parsedCertificate.NotBefore,
parsedCertificate.NotAfter,
)
if err != nil {
return nil, err
}
return digest, nil
})
if overallError != nil {
return "", overallError
}
return digest, nil
}
// CountPendingAuthorizations returns the number of pending, unexpired
// authorizations for the given registration.
func (ssa *SQLStorageAuthority) CountPendingAuthorizations(ctx context.Context, regID int64) (count int, err error) {
err = ssa.dbMap.WithContext(ctx).SelectOne(&count,
`SELECT count(1) FROM pendingAuthorizations
WHERE registrationID = :regID AND
expires > :now AND
status = :pending`,
map[string]interface{}{
"regID": regID,
"now": ssa.clk.Now(),
"pending": string(core.StatusPending),
})
return
}
func (ssa *SQLStorageAuthority) CountOrders(ctx context.Context, acctID int64, earliest, latest time.Time) (int, error) {
var count int
err := ssa.dbMap.WithContext(ctx).SelectOne(&count,
`SELECT count(1) FROM orders
WHERE registrationID = :acctID AND
created >= :windowLeft AND
created < :windowRight`,
map[string]interface{}{
"acctID": acctID,
"windowLeft": earliest,
"windowRight": latest,
})
if err != nil {
return 0, err
}
return count, nil
}
// CountInvalidAuthorizations counts invalid authorizations for a user expiring
// in a given time range.
// authorizations for the give registration.
func (ssa *SQLStorageAuthority) CountInvalidAuthorizations(
ctx context.Context,
req *sapb.CountInvalidAuthorizationsRequest,
) (count *sapb.Count, err error) {
identifier := identifier.ACMEIdentifier{
Type: identifier.DNS,
Value: *req.Hostname,
}
idJSON, err := json.Marshal(identifier)
if err != nil {
return nil, err
}
count = &sapb.Count{
Count: new(int64),
}
err = ssa.dbMap.WithContext(ctx).SelectOne(count.Count,
`SELECT COUNT(1) FROM authz
WHERE registrationID = :regID AND
identifier = :identifier AND
expires > :earliest AND
expires <= :latest AND
status = :invalid`,
map[string]interface{}{
"regID": *req.RegistrationID,
"identifier": idJSON,
"earliest": time.Unix(0, *req.Range.Earliest),
"latest": time.Unix(0, *req.Range.Latest),
"invalid": string(core.StatusInvalid),
})
return
}
func hashNames(names []string) []byte {
names = core.UniqueLowerNames(names)
hash := sha256.Sum256([]byte(strings.Join(names, ",")))
return hash[:]
}
func addFQDNSet(db dbInserter, names []string, serial string, issued time.Time, expires time.Time) error {
return db.Insert(&core.FQDNSet{
SetHash: hashNames(names),
Serial: serial,
Issued: issued,
Expires: expires,
})
}
// addOrderFQDNSet creates a new OrderFQDNSet row using the provided
// information. This function accepts a transaction so that the orderFqdnSet
// addition can take place within the order addition transaction. The caller is
// required to rollback the transaction if an error is returned.
func addOrderFQDNSet(
db dbInserter,
names []string,
orderID int64,
regID int64,
expires time.Time) error {
return db.Insert(&orderFQDNSet{
SetHash: hashNames(names),
OrderID: orderID,
RegistrationID: regID,
Expires: expires,
})
}
// deleteOrderFQDNSet deletes a OrderFQDNSet row that matches the provided
// orderID. This function accepts a transaction so that the deletion can
// take place within the finalization transaction. The caller is required to
// rollback the transaction if an error is returned.
func deleteOrderFQDNSet(
db dbExecer,
orderID int64) error {
result, err := db.Exec(`
DELETE FROM orderFqdnSets
WHERE orderID = ?`,
orderID)
if err != nil {
return err
}
rowsDeleted, err := result.RowsAffected()
if err != nil {
return err
}
// We always expect there to be an order FQDN set row for each
// pending/processing order that is being finalized. If there isn't one then
// something is amiss and should be raised as an internal server error
if rowsDeleted == 0 {
return berrors.InternalServerError("No orderFQDNSet exists to delete")
}
return nil
}
func addIssuedNames(db dbExecer, cert *x509.Certificate, isRenewal bool) error {
var qmarks []string
var values []interface{}
for _, name := range cert.DNSNames {
values = append(values,
ReverseName(name),
core.SerialToString(cert.SerialNumber),
cert.NotBefore,
isRenewal)
qmarks = append(qmarks, "(?, ?, ?, ?)")
}
query := `INSERT INTO issuedNames (reversedName, serial, notBefore, renewal) VALUES ` + strings.Join(qmarks, ", ") + `;`
_, err := db.Exec(query, values...)
return err
}
// CountFQDNSets returns the number of sets with hash |setHash| within the window
// |window|
func (ssa *SQLStorageAuthority) CountFQDNSets(ctx context.Context, window time.Duration, names []string) (int64, error) {
var count int64
err := ssa.dbMap.WithContext(ctx).SelectOne(
&count,
`SELECT COUNT(1) FROM fqdnSets
WHERE setHash = ?
AND issued > ?`,
hashNames(names),
ssa.clk.Now().Add(-window),
)
return count, err
}
// setHash is a []byte representing the hash of an FQDN Set
type setHash []byte
// getFQDNSetsBySerials finds the setHashes corresponding to a set of
// certificate serials. These serials can be used to check whether any
// certificates have been issued for the same set of names previously.
func (ssa *SQLStorageAuthority) getFQDNSetsBySerials(
db dbSelector,
serials []string,
) ([]setHash, error) {
var fqdnSets []setHash
// It is unexpected that this function would be called with no serials
if len(serials) == 0 {
err := fmt.Errorf("getFQDNSetsBySerials called with no serials")
ssa.log.AuditErr(err.Error())
return nil, err
}
qmarks := make([]string, len(serials))
params := make([]interface{}, len(serials))
for i, serial := range serials {
params[i] = serial
qmarks[i] = "?"
}
query := "SELECT setHash FROM fqdnSets " +
"WHERE serial IN (" + strings.Join(qmarks, ",") + ")"
_, err := db.Select(
&fqdnSets,
query,
params...)
if err != nil {
return nil, err
}
// The serials existed when we found them in issuedNames, they should continue
// to exist here. Otherwise an internal consistency violation occured and
// needs to be audit logged
if err == sql.ErrNoRows {
err := fmt.Errorf("getFQDNSetsBySerials returned no rows - internal consistency violation")
ssa.log.AuditErr(err.Error())
return nil, err
}
return fqdnSets, nil
}
// getNewIssuancesByFQDNSet returns a count of new issuances (renewals are not
// included) for a given slice of fqdnSets that occurred after the earliest
// parameter.
func (ssa *SQLStorageAuthority) getNewIssuancesByFQDNSet(
db dbSelector,
fqdnSets []setHash,
earliest time.Time,
) (int, error) {
var results []struct {
Serial string
SetHash setHash
Issued time.Time
}
qmarks := make([]string, len(fqdnSets))
params := make([]interface{}, len(fqdnSets))
for i, setHash := range fqdnSets {
// We have to cast the setHash back to []byte here since the sql package
// isn't able to convert `sa.setHash` for the parameter value itself
params[i] = []byte(setHash)
qmarks[i] = "?"
}
query := "SELECT serial, setHash, issued FROM fqdnSets " +
"WHERE setHash IN (" + strings.Join(qmarks, ",") + ") " +
"ORDER BY setHash, issued"
// First, find the serial, sethash and issued date from the fqdnSets table for
// the given fqdn set hashes
_, err := db.Select(
&results,
query,
params...)
if err != nil && err != sql.ErrNoRows {
return -1, err
}
// If there are no results we have encountered a major error and
// should loudly complain
if err == sql.ErrNoRows || len(results) == 0 {
ssa.log.AuditErrf("Found no results from fqdnSets for setHashes known to exist: %#v", fqdnSets)
return 0, err
}
processedSetHashes := make(map[string]bool)
issuanceCount := 0
// Loop through each set hash result, counting issuances per unique set hash
// that are within the window specified by the earliest parameter
for _, result := range results {
key := string(result.SetHash)
// Skip set hashes that we have already processed - we only care about the
// first issuance
if processedSetHashes[key] {
continue
}
// If the issued date is before our earliest cutoff then skip it
if result.Issued.Before(earliest) {
continue
}
// Otherwise note the issuance and mark the set hash as processed
issuanceCount++
processedSetHashes[key] = true
}
// Return the count of how many non-renewal issuances there were
return issuanceCount, nil
}
// FQDNSetExists returns a bool indicating if one or more FQDN sets |names|
// exists in the database
func (ssa *SQLStorageAuthority) FQDNSetExists(ctx context.Context, names []string) (bool, error) {
exists, err := ssa.checkFQDNSetExists(
ssa.dbMap.WithContext(ctx).SelectOne,
names)
if err != nil {
return false, err
}
return exists, nil
}
// oneSelectorFunc is a func type that matches both gorp.Transaction.SelectOne
// and gorp.DbMap.SelectOne.
type oneSelectorFunc func(holder interface{}, query string, args ...interface{}) error
// checkFQDNSetExists uses the given oneSelectorFunc to check whether an fqdnSet
// for the given names exists.
func (ssa *SQLStorageAuthority) checkFQDNSetExists(selector oneSelectorFunc, names []string) (bool, error) {
var count int64
err := selector(
&count,
`SELECT COUNT(1) FROM fqdnSets
WHERE setHash = ?
LIMIT 1`,
hashNames(names),
)
return count > 0, err
}
// PreviousCertificateExists returns true iff there was at least one certificate
// issued with the provided domain name, and the most recent such certificate
// was issued by the provided registration ID. This method is currently only
// used to determine if a certificate has previously been issued for a given
// domain name in order to determine if validations should be allowed during
// the v1 API shutoff.
func (ssa *SQLStorageAuthority) PreviousCertificateExists(
ctx context.Context,
req *sapb.PreviousCertificateExistsRequest,
) (*sapb.Exists, error) {
t := true
exists := &sapb.Exists{Exists: &t}
f := false
notExists := &sapb.Exists{Exists: &f}
// Find the most recently issued certificate containing this domain name.
var serial string
err := ssa.dbMap.WithContext(ctx).SelectOne(
&serial,
`SELECT serial FROM issuedNames
WHERE reversedName = ?
ORDER BY notBefore DESC
LIMIT 1`,
ReverseName(*req.Domain),
)
if err == sql.ErrNoRows {
return notExists, nil
}
if err != nil {
return nil, err
}
// Check whether that certificate was issued to the specified account.
var count int
err = ssa.dbMap.WithContext(ctx).SelectOne(
&count,
`SELECT COUNT(1) FROM certificates
WHERE serial = ?
AND registrationID = ?`,
serial,
*req.RegID,
)
// If no rows found, that means the certificate we found in issuedNames wasn't
// issued by the registration ID we are checking right now, but is not an
// error.
if err == sql.ErrNoRows {
return notExists, nil
}
if err != nil {
return nil, err
}
if count > 0 {
return exists, nil
}
return notExists, nil
}
// DeactivateRegistration deactivates a currently valid registration
func (ssa *SQLStorageAuthority) DeactivateRegistration(ctx context.Context, id int64) error {
_, err := ssa.dbMap.WithContext(ctx).Exec(
"UPDATE registrations SET status = ? WHERE status = ? AND id = ?",
string(core.StatusDeactivated),
string(core.StatusValid),
id,
)
return err
}
// DeactivateAuthorization deactivates a currently valid or pending authorization
func (ssa *SQLStorageAuthority) DeactivateAuthorization(ctx context.Context, id string) error {
_, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
if existingPending(txWithCtx, id) {
authzObj, err := txWithCtx.Get(&pendingauthzModel{}, id)
if err != nil {
return nil, err
}
if authzObj == nil {
// InternalServerError because existingPending already told us it existed
return nil, berrors.InternalServerError("failure retrieving pending authorization")
}
authz := authzObj.(*pendingauthzModel)
if authz.Status != core.StatusPending {
return nil, berrors.WrongAuthorizationStateError("authorization not pending")
}
authz.Status = core.StatusDeactivated
err = txWithCtx.Insert(&authzModel{authz.Authorization})
if err != nil {
return nil, err
}
result, err := txWithCtx.Delete(authzObj)
if err != nil {
return nil, err
}
if result != 1 {
return nil, berrors.InternalServerError("wrong number of rows deleted: expected 1, got %d", result)
}
} else {
_, err := txWithCtx.Exec(
`UPDATE authz SET status = ? WHERE id = ? and status = ?`,
string(core.StatusDeactivated),
id,
string(core.StatusValid),
)
if err != nil {
return nil, err
}
}
return nil, nil
})
return overallError
}
// DeactivateAuthorization2 deactivates a currently valid or pending authorization.
// This method is intended to deprecate DeactivateAuthorization.
func (ssa *SQLStorageAuthority) DeactivateAuthorization2(ctx context.Context, req *sapb.AuthorizationID2) (*corepb.Empty, error) {
_, err := ssa.dbMap.Exec(
`UPDATE authz2 SET status = :deactivated WHERE id = :id and status IN (:valid,:pending)`,
map[string]interface{}{
"deactivated": statusUint(core.StatusDeactivated),
"id": *req.Id,
"valid": statusUint(core.StatusValid),
"pending": statusUint(core.StatusPending),
},
)
if err != nil {
return nil, err
}
return &corepb.Empty{}, nil
}
// NewOrder adds a new v2 style order to the database
func (ssa *SQLStorageAuthority) NewOrder(ctx context.Context, req *corepb.Order) (*corepb.Order, error) {
order := &orderModel{
RegistrationID: *req.RegistrationID,
Expires: time.Unix(0, *req.Expires),
Created: ssa.clk.Now(),
}
output, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
if err := txWithCtx.Insert(order); err != nil {
return nil, err
}
for _, id := range req.Authorizations {
otoa := &orderToAuthzModel{
OrderID: order.ID,
AuthzID: id,
}
if err := txWithCtx.Insert(otoa); err != nil {
return nil, err
}
}
for _, id := range req.V2Authorizations {
otoa := &orderToAuthz2Model{
OrderID: order.ID,
AuthzID: id,
}
if err := txWithCtx.Insert(otoa); err != nil {
return nil, err
}
}
for _, name := range req.Names {
reqdName := &requestedNameModel{
OrderID: order.ID,
ReversedName: ReverseName(name),
}
if err := txWithCtx.Insert(reqdName); err != nil {
return nil, err
}
}
// Add an FQDNSet entry for the order
if err := addOrderFQDNSet(
txWithCtx, req.Names, order.ID, order.RegistrationID, order.Expires); err != nil {
return nil, err
}
return req, nil
})
if overallError != nil {
return nil, overallError
}
var outputOrder *corepb.Order
var ok bool
if outputOrder, ok = output.(*corepb.Order); !ok {
return nil, fmt.Errorf("shouldn't happen: casting error in NewOrder")
}
// Update the output with the ID that the order received
outputOrder.Id = &order.ID
// Update the output with the created timestamp from the model
createdTS := order.Created.UnixNano()
outputOrder.Created = &createdTS
// A new order is never processing because it can't have been finalized yet
processingStatus := false
outputOrder.BeganProcessing = &processingStatus
// Calculate the order status before returning it. Since it may have reused all
// valid authorizations the order may be "born" in a ready status.
status, err := ssa.statusForOrder(ctx, outputOrder)
if err != nil {
return nil, err
}
outputOrder.Status = &status
return outputOrder, nil
}
// SetOrderProcessing updates a provided *corepb.Order in pending status to be
// in processing status by updating the `beganProcessing` field of the
// corresponding Order table row in the DB.
func (ssa *SQLStorageAuthority) SetOrderProcessing(ctx context.Context, req *corepb.Order) error {
_, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
result, err := txWithCtx.Exec(`
UPDATE orders
SET beganProcessing = ?
WHERE id = ?
AND beganProcessing = ?`,
true,
*req.Id,
false)
if err != nil {
return nil, berrors.InternalServerError("error updating order to beganProcessing status")
}
n, err := result.RowsAffected()
if err != nil || n == 0 {
return nil, berrors.InternalServerError("no order updated to beganProcessing status")
}
return nil, nil
})
return overallError
}
// SetOrderError updates a provided Order's error field.
func (ssa *SQLStorageAuthority) SetOrderError(ctx context.Context, order *corepb.Order) error {
_, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
om, err := orderToModel(order)
if err != nil {
return nil, err
}
result, err := txWithCtx.Exec(`
UPDATE orders
SET error = ?
WHERE id = ?`,
om.Error,
om.ID)
if err != nil {
return nil, berrors.InternalServerError("error updating order error field")
}
n, err := result.RowsAffected()
if err != nil || n == 0 {
return nil, berrors.InternalServerError("no order updated with new error field")
}
return nil, nil
})
return overallError
}
// FinalizeOrder finalizes a provided *corepb.Order by persisting the
// CertificateSerial and a valid status to the database. No fields other than
// CertificateSerial and the order ID on the provided order are processed (e.g.
// this is not a generic update RPC).
func (ssa *SQLStorageAuthority) FinalizeOrder(ctx context.Context, req *corepb.Order) error {
_, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
result, err := txWithCtx.Exec(`
UPDATE orders
SET certificateSerial = ?
WHERE id = ? AND
beganProcessing = true`,
*req.CertificateSerial,
*req.Id)
if err != nil {
return nil, berrors.InternalServerError("error updating order for finalization")
}
n, err := result.RowsAffected()
if err != nil || n == 0 {
return nil, berrors.InternalServerError("no order updated for finalization")
}
// Delete the orderFQDNSet row for the order now that it has been finalized.
// We use this table for order reuse and should not reuse a finalized order.
if err := deleteOrderFQDNSet(txWithCtx, *req.Id); err != nil {
return nil, err
}
return nil, nil
})
return overallError
}
// authzForOrder retrieves the authorization IDs for an order. It returns these
// IDs in two slices: one for v1 style authorizations, and another for
// v2 style authorizations.
func (ssa *SQLStorageAuthority) authzForOrder(ctx context.Context, orderID int64, lookForV2 bool) ([]string, []int64, error) {
var v1IDs []string
var v2IDs []int64
if lookForV2 {
_, err := ssa.dbMap.WithContext(ctx).Select(
&v2IDs,
"SELECT authzID FROM orderToAuthz2 WHERE orderID = ?",
orderID,
)
if err != nil {
return nil, nil, err
}
}
_, err := ssa.dbMap.WithContext(ctx).Select(
&v1IDs, "SELECT authzID FROM orderToAuthz WHERE orderID = ?", orderID)
if err != nil {
return nil, nil, err
}
return v1IDs, v2IDs, nil
}
// namesForOrder finds all of the requested names associated with an order. The
// names are returned in their reversed form (see `sa.ReverseName`).
func (ssa *SQLStorageAuthority) namesForOrder(ctx context.Context, orderID int64) ([]string, error) {
var reversedNames []string
_, err := ssa.dbMap.WithContext(ctx).Select(
&reversedNames,
`SELECT reversedName
FROM requestedNames
WHERE orderID = ?`,
orderID)
if err != nil {
return nil, err
}
return reversedNames, nil
}
// GetOrder is used to retrieve an already existing order object
func (ssa *SQLStorageAuthority) GetOrder(ctx context.Context, req *sapb.OrderRequest) (*corepb.Order, error) {
omObj, err := ssa.dbMap.WithContext(ctx).Get(orderModel{}, *req.Id)
if err == sql.ErrNoRows || omObj == nil {
return nil, berrors.NotFoundError("no order found for ID %d", *req.Id)
}
if err != nil {
return nil, err
}
order, err := modelToOrder(omObj.(*orderModel))
if err != nil {
return nil, err
}
var useV2Authzs bool
if req.UseV2Authorizations != nil {
useV2Authzs = *req.UseV2Authorizations
}
// we set useV2Authzs if DisableAuthz2Orders is enabled as actually looking for v2
// authorizations is the only way to tell if an order contains them, otherwise
// we will fail due to the number of authorizations not matching the number of names
// in the order.
useV2Authzs = useV2Authzs || features.Enabled(features.DisableAuthz2Orders)
v1AuthzIDs, v2AuthzIDs, err := ssa.authzForOrder(ctx, *order.Id, useV2Authzs)
if err != nil {
return nil, err
}
if features.Enabled(features.DisableAuthz2Orders) && len(v2AuthzIDs) > 0 {
return nil, berrors.NotFoundError("no order found for ID %d", *req.Id)
}
order.Authorizations, order.V2Authorizations = v1AuthzIDs, v2AuthzIDs
names, err := ssa.namesForOrder(ctx, *order.Id)
if err != nil {
return nil, err
}
// The requested names are stored reversed to improve indexing performance. We
// need to reverse the reversed names here before giving them back to the
// caller.
reversedNames := make([]string, len(names))
for i, n := range names {
reversedNames[i] = ReverseName(n)
}
order.Names = reversedNames
// Calculate the status for the order
status, err := ssa.statusForOrder(ctx, order)
if err != nil {
return nil, err
}
order.Status = &status
return order, nil
}
// statusForOrder examines the status of a provided order's authorizations to
// determine what the overall status of the order should be. In summary:
// * If the order has an error, the order is invalid
// * If any of the order's authorizations are invalid, the order is invalid.
// * If any of the order's authorizations are expired, the order is invalid.
// * If any of the order's authorizations are deactivated, the order is deactivated.
// * If any of the order's authorizations are pending, the order is pending.
// * If all of the order's authorizations are valid, and there is
// a certificate serial, the order is valid.
// * If all of the order's authorizations are valid, and we have began
// processing, but there is no certificate serial, the order is processing.
// * If all of the order's authorizations are valid, and we haven't begun
// processing, then the order is status ready.
// An error is returned for any other case.
//
// While transitioning between the v1 and v2 authorization storage formats this method
// needs to lookup authorizations using both the authz/pendingAuthorizations and authz2
// tables. Since, if there are any v2 authorizations, we already have their IDs we don't
// need to consult the orderToAuthz2 table a second time. We cannot do this as easily
// for the v1 authorizations as their IDs can refer to one of two tables, whereas all
// v2 authorizations exist in a single table.
func (ssa *SQLStorageAuthority) statusForOrder(ctx context.Context, order *corepb.Order) (string, error) {
// Without any further work we know an order with an error is invalid
if order.Error != nil {
return string(core.StatusInvalid), nil
}
// If the order is expired the status is invalid and we don't need to get
// order authorizations. Its important to exit early in this case because an
// order that references an expired authorization will be itself have been
// expired (because we match the order expiry to the associated authz expiries
// in ra.NewOrder), and expired authorizations may be purged from the DB.
// Because of this purging fetching the authz's for an expired order may
// return fewer authz objects than expected, triggering a 500 error response.
orderExpiry := time.Unix(0, *order.Expires)
if orderExpiry.Before(ssa.clk.Now()) {
return string(core.StatusInvalid), nil
}
// Get the full Authorization objects for the order
authzValidityInfo, err := ssa.getAllOrderAuthorizationStatuses(ctx, *order.Id, *order.RegistrationID, order.V2Authorizations)
// If there was an error getting the authorizations, return it immediately
if err != nil {
return "", err
}
// If getAllOrderAuthorizationStatuses returned a different number of authorization
// objects than the order's slices of v1 and v2 authorization IDs something has gone
// wrong worth raising an internal error about.
if len(authzValidityInfo) != len(order.Authorizations)+len(order.V2Authorizations) {
return "", berrors.InternalServerError(
"getAllOrderAuthorizationStatuses returned the wrong number of authorization statuses "+
"(%d vs expected %d) for order %d",
len(authzValidityInfo), len(order.Authorizations), *order.Id)
}
// Keep a count of the authorizations seen
invalidAuthzs := 0
expiredAuthzs := 0
deactivatedAuthzs := 0
pendingAuthzs := 0
validAuthzs := 0
// Loop over each of the order's authorization objects to examine the authz status
for _, info := range authzValidityInfo {
switch core.AcmeStatus(info.Status) {
case core.StatusInvalid:
invalidAuthzs++
case core.StatusDeactivated:
deactivatedAuthzs++
case core.StatusPending:
pendingAuthzs++
case core.StatusValid:
validAuthzs++
default:
return "", berrors.InternalServerError(
"Order is in an invalid state. Authz has invalid status %s",
info.Status)
}
if info.Expires.Before(ssa.clk.Now()) {
expiredAuthzs++
}
}
// An order is invalid if **any** of its authzs are invalid
if invalidAuthzs > 0 {
return string(core.StatusInvalid), nil
}
// An order is invalid if **any** of its authzs are expired
if expiredAuthzs > 0 {
return string(core.StatusInvalid), nil
}
// An order is deactivated if **any** of its authzs are deactivated
if deactivatedAuthzs > 0 {
return string(core.StatusDeactivated), nil
}
// An order is pending if **any** of its authzs are pending
if pendingAuthzs > 0 {
return string(core.StatusPending), nil
}
// An order is fully authorized if it has valid authzs for each of the order
// names
fullyAuthorized := len(order.Names) == validAuthzs
// If the order isn't fully authorized we've encountered an internal error:
// Above we checked for any invalid or pending authzs and should have returned
// early. Somehow we made it this far but also don't have the correct number
// of valid authzs.
if !fullyAuthorized {
return "", berrors.InternalServerError(
"Order has the incorrect number of valid authorizations & no pending, " +
"deactivated or invalid authorizations")
}
// If the order is fully authorized and the certificate serial is set then the
// order is valid
if fullyAuthorized && order.CertificateSerial != nil && *order.CertificateSerial != "" {
return string(core.StatusValid), nil
}
// If the order is fully authorized, and we have began processing it, then the
// order is processing.
if fullyAuthorized && order.BeganProcessing != nil && *order.BeganProcessing {
return string(core.StatusProcessing), nil
}
if fullyAuthorized && order.BeganProcessing != nil && !*order.BeganProcessing {
return string(core.StatusReady), nil
}
return "", berrors.InternalServerError(
"Order %d is in an invalid state. No state known for this order's "+
"authorizations", *order.Id)
}
type authzValidity struct {
Status string
Expires time.Time
}
func (ssa *SQLStorageAuthority) getAuthorizationStatuses(ctx context.Context, ids []int64) ([]authzValidity, error) {
var qmarks []string
var params []interface{}
for _, id := range ids {
qmarks = append(qmarks, "?")
params = append(params, id)
}
var validityInfo []struct {
Status uint8
Expires time.Time
}
_, err := ssa.dbMap.WithContext(ctx).Select(
&validityInfo,
fmt.Sprintf("SELECT status, expires FROM authz2 WHERE id IN (%s)", strings.Join(qmarks, ",")),
params...,
)
if err != nil {
return nil, err
}
allAuthzValidity := make([]authzValidity, len(validityInfo))
for i, info := range validityInfo {
allAuthzValidity[i] = authzValidity{
Status: uintToStatus[info.Status],
Expires: info.Expires,
}
}
return allAuthzValidity, nil
}
func (ssa *SQLStorageAuthority) getAllOrderAuthorizationStatuses(
ctx context.Context,
orderID, acctID int64,
v2AuthzIDs []int64) ([]authzValidity, error) {
var allAuthzValidity []authzValidity
if len(v2AuthzIDs) > 0 {
validityInfo, err := ssa.getAuthorizationStatuses(ctx, v2AuthzIDs)
if err != nil {
return nil, err
}
allAuthzValidity = append(allAuthzValidity, validityInfo...)
}
for _, table := range authorizationTables {
var validityInfo []authzValidity
_, err := ssa.dbMap.WithContext(ctx).Select(
&validityInfo,
fmt.Sprintf(`SELECT status, expires from %s AS authz
INNER JOIN orderToAuthz
ON authz.ID = orderToAuthz.authzID
WHERE authz.registrationID = ? AND
orderToAuthz.orderID = ?`, table),
acctID,
orderID,
)
if err != nil {
return nil, err
}
allAuthzValidity = append(allAuthzValidity, validityInfo...)
}
return allAuthzValidity, nil
}
// GetValidOrderAuthorizations is used to find the valid, unexpired authorizations
// associated with a specific order and account ID.
func (ssa *SQLStorageAuthority) GetValidOrderAuthorizations(
ctx context.Context,
req *sapb.GetValidOrderAuthorizationsRequest) (map[string]*core.Authorization, error) {
now := ssa.clk.Now()
// Select the full authorization data for all *valid, unexpired*
// authorizations that are owned by the correct account ID and associated with
// the given order ID
var auths []*core.Authorization
_, err := ssa.dbMap.WithContext(ctx).Select(
&auths,
fmt.Sprintf(`SELECT %s FROM %s AS authz
LEFT JOIN orderToAuthz
ON authz.ID = orderToAuthz.authzID
WHERE authz.registrationID = ? AND
authz.expires > ? AND
authz.status = ? AND
orderToAuthz.orderID = ?`, authzFields, authorizationTable),
*req.AcctID,
now,
string(core.StatusValid),
*req.Id)
if err != nil {
return nil, err
}
// Collapse & dedupe the returned authorizations into a mapping from name to
// authorization
byName := make(map[string]*core.Authorization)
for _, auth := range auths {
// We only expect to get back DNS identifiers
if auth.Identifier.Type != identifier.DNS {
return nil, fmt.Errorf("unknown identifier type: %q on authz id %q", auth.Identifier.Type, auth.ID)
}
existing, present := byName[auth.Identifier.Value]
if !present || auth.Expires.After(*existing.Expires) {
// Retrieve challenges for the authz
auth.Challenges, err = ssa.getChallenges(ssa.dbMap.WithContext(ctx), auth.ID)
if err != nil {
return nil, err
}
byName[auth.Identifier.Value] = auth
}
}
return byName, nil
}
// GetOrderForNames tries to find a **pending** or **ready** order with the
// exact set of names requested, associated with the given accountID. Only
// unexpired orders are considered. If no order meeting these requirements is
// found a nil corepb.Order pointer is returned.
func (ssa *SQLStorageAuthority) GetOrderForNames(
ctx context.Context,
req *sapb.GetOrderForNamesRequest) (*corepb.Order, error) {
// Hash the names requested for lookup in the orderFqdnSets table
fqdnHash := hashNames(req.Names)
// Find a possibly-suitable order. We don't include the account ID or order
// status in this query because there's no index that includes those, so
// including them could require the DB to scan extra rows.
// Instead, we select one unexpired order that matches the fqdnSet. If
// that order doesn't match the account ID or status we need, just return
// nothing. We use `ORDER BY expires ASC` because the index on
// (setHash, expires) is in ASC order. DESC would be slightly nicer from a
// user experience perspective but would be slow when there are many entries
// to sort.
// This approach works fine because in most cases there's only one account
// issuing for a given name. If there are other accounts issuing for the same
// name, it just means order reuse happens less often.
var result struct {
OrderID int64
RegistrationID int64
}
var err error
if features.Enabled(features.FasterGetOrderForNames) {
err = ssa.dbMap.WithContext(ctx).SelectOne(&result, `
SELECT orderID, registrationID
FROM orderFqdnSets
WHERE setHash = ?
AND expires > ?
ORDER BY expires ASC
LIMIT 1`,
fqdnHash, ssa.clk.Now())
} else {
err = ssa.dbMap.WithContext(ctx).SelectOne(&result, `
SELECT orderID, registrationID
FROM orderFqdnSets
WHERE setHash = ?
AND registrationID = ?
AND expires > ?
LIMIT 1`,
fqdnHash, *req.AcctID, ssa.clk.Now())
}
if err == sql.ErrNoRows {
return nil, berrors.NotFoundError("no order matching request found")
} else if err != nil {
return nil, err
}
if result.RegistrationID != *req.AcctID {
return nil, berrors.NotFoundError("no order matching request found")
}
// Get the order
order, err := ssa.GetOrder(ctx, &sapb.OrderRequest{Id: &result.OrderID, UseV2Authorizations: req.UseV2Authorizations})
if err != nil {
return nil, err
}
// Only return a pending or ready order
if *order.Status != string(core.StatusPending) &&
*order.Status != string(core.StatusReady) {
return nil, berrors.NotFoundError("no order matching request found")
}
return order, nil
}
func (ssa *SQLStorageAuthority) getAuthorizations(
ctx context.Context,
table string,
status string,
registrationID int64,
names []string,
now time.Time,
requireV2Authzs bool) (map[string]*core.Authorization, error) {
if len(names) == 0 {
return nil, berrors.InternalServerError("no names received")
}
params := make([]interface{}, len(names))
qmarks := make([]string, len(names))
for i, name := range names {
id := identifier.ACMEIdentifier{Type: identifier.DNS, Value: name}
idJSON, err := json.Marshal(id)
if err != nil {
return nil, err
}
params[i] = string(idJSON)
qmarks[i] = "?"
}
// If requested, filter out V1 authorizations by doing a JOIN on the
// orderToAuthz table, ensuring that all authorization IDs returned correspond
// to a V2 order.
queryPrefix := fmt.Sprintf(`SELECT %s FROM %s`, authzFields, table)
if requireV2Authzs {
queryPrefix = queryPrefix + `
JOIN orderToAuthz
ON ID = authzID`
}
var auths []*core.Authorization
_, err := ssa.dbMap.WithContext(ctx).Select(
&auths,
fmt.Sprintf(`%s
WHERE registrationID = ? AND
expires > ? AND
status = ? AND
identifier IN (%s)`,
queryPrefix, strings.Join(qmarks, ",")),
append([]interface{}{registrationID, now, status}, params...)...)
if err != nil {
return nil, err
}
byName := make(map[string]*core.Authorization)
for _, auth := range auths {
// No real life authorizations should have a nil expires. If we find them,
// don't consider them valid.
if auth.Expires == nil {
continue
}
if auth.Identifier.Type != identifier.DNS {
return nil, fmt.Errorf("unknown identifier type: %q on authz id %q", auth.Identifier.Type, auth.ID)
}
existing, present := byName[auth.Identifier.Value]
if !present || auth.Expires.After(*existing.Expires) {
byName[auth.Identifier.Value] = auth
}
}
for _, auth := range byName {
// Retrieve challenges for the authz
if auth.Challenges, err = ssa.getChallenges(ssa.dbMap.WithContext(ctx), auth.ID); err != nil {
return nil, err
}
}
return byName, nil
}
func (ssa *SQLStorageAuthority) getPendingAuthorizations(
ctx context.Context,
registrationID int64,
names []string,
now time.Time,
requireV2Authzs bool) (map[string]*core.Authorization, error) {
return ssa.getAuthorizations(
ctx,
pendingAuthorizationTable,
string(core.StatusPending),
registrationID,
names,
now,
requireV2Authzs)
}
func AuthzMapToPB(m map[string]*core.Authorization) (*sapb.Authorizations, error) {
resp := &sapb.Authorizations{}
for k, v := range m {
authzPB, err := bgrpc.AuthzToPB(*v)
if err != nil {
return nil, err
}
// Make a copy of k because it will be reassigned with each loop.
kCopy := k
resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{Domain: &kCopy, Authz: authzPB})
}
return resp, nil
}
// GetAuthorizations returns a map of valid or pending authorizations for as many names as possible
func (ssa *SQLStorageAuthority) GetAuthorizations(
ctx context.Context,
req *sapb.GetAuthorizationsRequest) (*sapb.Authorizations, error) {
authzMap, err := ssa.getAuthorizations(
ctx,
authorizationTable,
string(core.StatusValid),
*req.RegistrationID,
req.Domains,
time.Unix(0, *req.Now),
*req.RequireV2Authzs,
)
if err != nil {
return nil, err
}
if len(authzMap) == len(req.Domains) {
return AuthzMapToPB(authzMap)
}
// remove names we already have authz for
remainingNames := []string{}
for _, name := range req.Domains {
if _, present := authzMap[name]; !present {
remainingNames = append(remainingNames, name)
}
}
pendingAuthz, err := ssa.getPendingAuthorizations(
ctx,
*req.RegistrationID,
remainingNames,
time.Unix(0, *req.Now),
*req.RequireV2Authzs)
if err != nil {
return nil, err
}
// merge pending into valid
for name, a := range pendingAuthz {
authzMap[name] = a
}
// Wildcard domain issuance requires that the authorizations returned by this
// RPC also include populated challenges such that the caller can know if the
// challenges meet the wildcard issuance policy (e.g. only 1 DNS-01
// challenge).
// Fetch each of the authorizations' associated challenges
for _, authz := range authzMap {
authz.Challenges, err = ssa.getChallenges(ssa.dbMap.WithContext(ctx), authz.ID)
if err != nil {
return nil, err
}
}
return AuthzMapToPB(authzMap)
}
// AddPendingAuthorizations creates a batch of pending authorizations and returns their IDs
func (ssa *SQLStorageAuthority) AddPendingAuthorizations(ctx context.Context, req *sapb.AddPendingAuthorizationsRequest) (*sapb.AuthorizationIDs, error) {
ids := []string{}
for _, authPB := range req.Authz {
authz, err := bgrpc.PBToAuthz(authPB)
if err != nil {
return nil, err
}
result, err := ssa.NewPendingAuthorization(ctx, authz)
if err != nil {
return nil, err
}
ids = append(ids, result.ID)
}
return &sapb.AuthorizationIDs{Ids: ids}, nil
}
func (ssa *SQLStorageAuthority) getChallengesImpl(db dbSelector, authID string) ([]core.Challenge, error) {
var challObjs []challModel
_, err := db.Select(
&challObjs,
getChallengesQuery,
map[string]interface{}{"authID": authID},
)
if err != nil {
return nil, err
}
var challs []core.Challenge
for _, c := range challObjs {
chall, err := modelToChallenge(&c)
if err != nil {
return nil, err
}
challs = append(challs, chall)
}
return challs, nil
}
// NewAuthorizations2 adds a set of new style authorizations to the database and returns
// either the IDs of the authorizations or an error. It will only process corepb.Authorization
// objects if the V2 field is set. This method is intended to deprecate AddPendingAuthorizations
func (ssa *SQLStorageAuthority) NewAuthorizations2(ctx context.Context, req *sapb.AddPendingAuthorizationsRequest) (*sapb.Authorization2IDs, error) {
ids := &sapb.Authorization2IDs{}
for _, authz := range req.Authz {
if *authz.Status != string(core.StatusPending) {
return nil, berrors.InternalServerError("authorization must be pending")
}
am, err := authzPBToModel(authz)
if err != nil {
return nil, err
}
err = ssa.dbMap.Insert(am)
if err != nil {
return nil, err
}
ids.Ids = append(ids.Ids, am.ID)
}
return ids, nil
}
// GetAuthorization2 returns the authz2 style authorization identified by the provided ID or an error.
// If no authorization is found matching the ID a berrors.NotFound type error is returned. This method
// is intended to deprecate GetAuthorization.
func (ssa *SQLStorageAuthority) GetAuthorization2(ctx context.Context, id *sapb.AuthorizationID2) (*corepb.Authorization, error) {
obj, err := ssa.dbMap.Get(authz2Model{}, *id.Id)
if err != nil {
return nil, err
}
if obj == nil {
return nil, berrors.NotFoundError("authorization %d not found", *id.Id)
}
return modelToAuthzPB(obj.(*authz2Model))
}
// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a
// protobuf authorizations map
func authz2ModelMapToPB(m map[string]authz2Model) (*sapb.Authorizations, error) {
resp := &sapb.Authorizations{}
for k, v := range m {
// Make a copy of k because it will be reassigned with each loop.
kCopy := k
authzPB, err := modelToAuthzPB(&v)
if err != nil {
return nil, err
}
resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{Domain: &kCopy, Authz: authzPB})
}
return resp, nil
}
// GetAuthorizations2 returns any valid or pending authorizations that exist for the list of domains
// provided. If both a valid and pending authorization exist only the valid one will be returned.
// This method will look in both the v2 and v1 authorizations tables for authorizations but will
// always prefer v2 authorizations. This method will only return authorizations created using the
// WFE v2 API (in GetAuthorizations this feature was, now somewhat confusingly, called RequireV2Authzs).
// This method is intended to deprecate GetAuthorizations. This method only supports DNS identifier types.
func (ssa *SQLStorageAuthority) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizationsRequest) (*sapb.Authorizations, error) {
var authzModels []authz2Model
params := []interface{}{
*req.RegistrationID,
statusUint(core.StatusValid),
statusUint(core.StatusPending),
time.Unix(0, *req.Now),
identifierTypeToUint[string(identifier.DNS)],
}
qmarks := make([]string, len(req.Domains))
for i, n := range req.Domains {
qmarks[i] = "?"
params = append(params, n)
}
query := fmt.Sprintf(
`SELECT %s FROM authz2
JOIN orderToAuthz2
ON id = authzID
WHERE registrationID = ? AND
status IN (?,?) AND
expires > ? AND
identifierType = ? AND
identifierValue IN (%s)`,
authz2Fields,
strings.Join(qmarks, ","),
)
_, err := ssa.dbMap.Select(
&authzModels,
query,
params...,
)
if err != nil {
return nil, err
}
authzModelMap := make(map[string]authz2Model)
for _, am := range authzModels {
if existing, present := authzModelMap[am.IdentifierValue]; !present ||
uintToStatus[existing.Status] == string(core.StatusPending) && uintToStatus[am.Status] == string(core.StatusValid) {
authzModelMap[am.IdentifierValue] = am
}
}
authzsPB, err := authz2ModelMapToPB(authzModelMap)
if err != nil {
return nil, err
}
// If we still need more authorizations look for them in the old
// style authorization tables.
if len(authzModelMap) != len(req.Domains) {
// Get the authorizations for names we don't already have
var remaining []string
for _, name := range req.Domains {
if _, present := authzModelMap[name]; !present {
remaining = append(remaining, name)
}
}
reqV2 := true
authz, err := ssa.GetAuthorizations(ctx, &sapb.GetAuthorizationsRequest{
RegistrationID: req.RegistrationID,
Domains: remaining,
Now: req.Now,
RequireV2Authzs: &reqV2,
})
if err != nil {
return nil, err
}
for _, authzPB := range authz.Authz {
authzsPB.Authz = append(authzsPB.Authz, authzPB)
}
}
return authzsPB, nil
}
// FinalizeAuthorization2 moves a pending authorization to either the valid or invalid status. If
// the authorization is being moved to invalid the validationError field must be set. If the
// authorization is being moved to valid the validationRecord and expires fields must be set.
// This method is intended to deprecate the FinalizeAuthorization method.
func (ssa *SQLStorageAuthority) FinalizeAuthorization2(ctx context.Context, req *sapb.FinalizeAuthorizationRequest) error {
if *req.Status != string(core.StatusValid) && *req.Status != string(core.StatusInvalid) {
return berrors.InternalServerError("authorization must have status valid or invalid")
}
query := `UPDATE authz2 SET
status = :status,
attempted = :attempted,
validationRecord = :validationRecord,
validationError = :validationError,
expires = :expires
WHERE id = :id AND status = :pending`
var validationRecords []core.ValidationRecord
for _, recordPB := range req.ValidationRecords {
record, err := bgrpc.PBToValidationRecord(recordPB)
if err != nil {
return err
}
validationRecords = append(validationRecords, record)
}
vrJSON, err := json.Marshal(validationRecords)
if err != nil {
return err
}
var veJSON []byte
if req.ValidationError != nil {
validationError, err := bgrpc.PBToProblemDetails(req.ValidationError)
if err != nil {
return err
}
j, err := json.Marshal(validationError)
if err != nil {
return err
}
veJSON = j
}
params := map[string]interface{}{
"status": statusToUint[*req.Status],
"attempted": challTypeToUint[*req.Attempted],
"validationRecord": vrJSON,
"id": *req.Id,
"pending": statusUint(core.StatusPending),
"expires": time.Unix(0, *req.Expires).UTC(),
// if req.ValidationError is nil veJSON should also be nil
// which should result in a NULL field
"validationError": veJSON,
}
res, err := ssa.dbMap.Exec(query, params)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return berrors.NotFoundError("authorization with id %d not found", *req.Id)
} else if rows > 1 {
return berrors.InternalServerError("multiple rows updated for authorization id %d", *req.Id)
}
return nil
}
// RevokeCertificate stores revocation information about a certificate. It will only store this
// information if the certificate is not already marked as revoked.
func (ssa *SQLStorageAuthority) RevokeCertificate(ctx context.Context, req *sapb.RevokeCertificateRequest) error {
_, overallError := withTransaction(ctx, ssa.dbMap, func(txWithCtx transaction) (interface{}, error) {
status, err := SelectCertificateStatus(
txWithCtx,
"WHERE serial = ? AND status != ?",
*req.Serial,
string(core.OCSPStatusRevoked),
)
if err != nil {
if err == sql.ErrNoRows {
// InternalServerError because we expected this certificate status to exist and
// not be revoked.
return nil, berrors.InternalServerError("no certificate with serial %s and status %s", *req.Serial, string(core.OCSPStatusRevoked))
}
return nil, err
}
revokedDate := time.Unix(0, *req.Date)
status.Status = core.OCSPStatusRevoked
status.RevokedReason = revocation.Reason(*req.Reason)
status.RevokedDate = revokedDate
status.OCSPLastUpdated = revokedDate
status.OCSPResponse = req.Response
n, err := txWithCtx.Update(&status)
if err != nil {
return nil, err
}
if n == 0 {
return nil, berrors.InternalServerError("no certificate updated")
}
return nil, nil
})
return overallError
}
// GetPendingAuthorization2 returns the most recent Pending authorization with
// the given identifier, if available. This method is intended to deprecate
// GetPendingAuthorization. This method only supports DNS identifier types.
func (ssa *SQLStorageAuthority) GetPendingAuthorization2(ctx context.Context, req *sapb.GetPendingAuthorizationRequest) (*corepb.Authorization, error) {
var am authz2Model
err := ssa.dbMap.WithContext(ctx).SelectOne(
&am,
fmt.Sprintf(`SELECT %s FROM authz2 WHERE
registrationID = :regID AND
status = :status AND
expires > :validUntil AND
identifierType = :dnsType AND
identifierValue = :ident
ORDER BY expires ASC
LIMIT 1 `, authz2Fields),
map[string]interface{}{
"regID": *req.RegistrationID,
"status": statusUint(core.StatusPending),
"validUntil": time.Unix(0, *req.ValidUntil),
"dnsType": identifierTypeToUint[string(identifier.DNS)],
"ident": *req.IdentifierValue,
},
)
if err != nil {
if err == sql.ErrNoRows {
// there may be an old style pending authorization so look for that
authz, err := ssa.GetPendingAuthorization(ctx, req)
if err != nil {
return nil, err
}
return bgrpc.AuthzToPB(*authz)
}
return nil, err
}
return modelToAuthzPB(&am)
}
// CountPendingAuthorizations2 returns the number of pending, unexpired authorizations
// for the given registration. This method is intended to deprecate CountPendingAuthorizations.
func (ssa *SQLStorageAuthority) CountPendingAuthorizations2(ctx context.Context, req *sapb.RegistrationID) (*sapb.Count, error) {
var count int64
err := ssa.dbMap.WithContext(ctx).SelectOne(&count,
`SELECT COUNT(1) FROM authz2 WHERE
registrationID = :regID AND
expires > :expires AND
status = :status`,
map[string]interface{}{
"regID": *req.Id,
"expires": ssa.clk.Now(),
"status": statusUint(core.StatusPending),
},
)
if err != nil {
return nil, err
}
// also count old style authorizations and add those to the count of
// new authorizations
oldCount, err := ssa.CountPendingAuthorizations(ctx, *req.Id)
if err != nil {
return nil, err
}
count += int64(oldCount)
return &sapb.Count{Count: &count}, nil
}
// GetValidOrderAuthorizations2 is used to find the valid, unexpired authorizations
// associated with a specific order and account ID. This method is intended to
// deprecate GetValidOrderAuthorizations.
func (ssa *SQLStorageAuthority) GetValidOrderAuthorizations2(ctx context.Context, req *sapb.GetValidOrderAuthorizationsRequest) (*sapb.Authorizations, error) {
var ams []authz2Model
_, err := ssa.dbMap.WithContext(ctx).Select(
&ams,
fmt.Sprintf(`SELECT %s FROM authz2
LEFT JOIN orderToAuthz2 ON authz2.ID = orderToAuthz2.authzID
WHERE authz2.registrationID = :regID AND
authz2.expires > :expires AND
authz2.status = :status AND
orderToAuthz2.orderID = :orderID`,
authz2Fields,
),
map[string]interface{}{
"regID": *req.AcctID,
"expires": ssa.clk.Now(),
"status": statusUint(core.StatusValid),
"orderID": *req.Id,
},
)
if err != nil {
return nil, err
}
byName := make(map[string]authz2Model)
for _, am := range ams {
if uintToIdentifierType[am.IdentifierType] != string(identifier.DNS) {
return nil, fmt.Errorf("unknown identifier type: %q on authz id %d", am.IdentifierType, am.ID)
}
existing, present := byName[am.IdentifierValue]
if !present || am.Expires.After(existing.Expires) {
byName[am.IdentifierValue] = am
}
}
authzsPB, err := authz2ModelMapToPB(byName)
if err != nil {
return nil, err
}
// also get any older style authorizations, as far as I can tell
// there is no easy way to tell if this is needed or not as
// an order may be all one style, all the other, or a mix
oldAuthzMap, err := ssa.GetValidOrderAuthorizations(ctx, req)
if err != nil {
return nil, err
}
if len(oldAuthzMap) > 0 {
oldAuthzsPB, err := AuthzMapToPB(oldAuthzMap)
if err != nil {
return nil, err
}
for _, authzPB := range oldAuthzsPB.Authz {
authzsPB.Authz = append(authzsPB.Authz, authzPB)
}
}
return authzsPB, nil
}
// CountInvalidAuthorizations2 counts invalid authorizations for a user expiring
// in a given time range. This method is intended to deprecate CountInvalidAuthorizations.
// This method only supports DNS identifier types.
func (ssa *SQLStorageAuthority) CountInvalidAuthorizations2(ctx context.Context, req *sapb.CountInvalidAuthorizationsRequest) (*sapb.Count, error) {
var count int64
err := ssa.dbMap.WithContext(ctx).SelectOne(
&count,
`SELECT COUNT(1) FROM authz2 WHERE
registrationID = :regID AND
status = :status AND
expires > :expiresEarliest AND
expires <= :expiresLatest AND
identifierType = :dnsType AND
identifierValue = :ident`,
map[string]interface{}{
"regID": *req.RegistrationID,
"dnsType": identifierTypeToUint[string(identifier.DNS)],
"ident": *req.Hostname,
"expiresEarliest": time.Unix(0, *req.Range.Earliest),
"expiresLatest": time.Unix(0, *req.Range.Latest),
"status": statusUint(core.StatusInvalid),
},
)
if err != nil {
return nil, err
}
// Also count old authorizations and add those to the new style
// count
oldCount, err := ssa.CountInvalidAuthorizations(ctx, req)
if err != nil {
return nil, err
}
count += *oldCount.Count
return &sapb.Count{Count: &count}, nil
}
// GetValidAuthorizations2 returns the latest authorization for all
// domain names that the account has authorizations for. This method is
// intended to deprecate GetValidAuthorizations. This method only supports
// DNS identifier types.
func (ssa *SQLStorageAuthority) GetValidAuthorizations2(ctx context.Context, req *sapb.GetValidAuthorizationsRequest) (*sapb.Authorizations, error) {
var authzModels []authz2Model
params := []interface{}{
*req.RegistrationID,
statusUint(core.StatusValid),
time.Unix(0, *req.Now),
identifierTypeToUint[string(identifier.DNS)],
}
qmarks := make([]string, len(req.Domains))
for i, n := range req.Domains {
qmarks[i] = "?"
params = append(params, n)
}
_, err := ssa.dbMap.Select(
&authzModels,
fmt.Sprintf(
`SELECT %s FROM authz2 WHERE
registrationID = ? AND
status = ? AND
expires > ? AND
identifierType = ? AND
identifierValue IN (%s)`,
authz2Fields,
strings.Join(qmarks, ","),
),
params...,
)
if err != nil {
return nil, err
}
authzMap := make(map[string]authz2Model, len(authzModels))
for _, am := range authzModels {
// Only allow DNS identifiers
if uintToIdentifierType[am.IdentifierType] != string(identifier.DNS) {
continue
}
// If there is an existing authorization in the map only replace it with one
// which has a later expiry.
if existing, present := authzMap[am.IdentifierValue]; present && am.Expires.Before(existing.Expires) {
continue
}
authzMap[am.IdentifierValue] = am
}
authzsPB, err := authz2ModelMapToPB(authzMap)
if err != nil {
return nil, err
}
if len(authzsPB.Authz) != len(req.Domains) {
// We may still have valid old style authorizations
// we want for names in the list, so we have to look
// for them.
var remaining []string
for _, name := range req.Domains {
if _, present := authzMap[name]; !present {
remaining = append(remaining, name)
}
}
now := time.Unix(0, *req.Now)
oldAuthzs, err := ssa.GetValidAuthorizations(
ctx,
*req.RegistrationID,
remaining,
now,
)
if err != nil {
return nil, err
}
if len(oldAuthzs) > 0 {
oldAuthzsPB, err := AuthzMapToPB(oldAuthzs)
if err != nil {
return nil, err
}
for _, authzPB := range oldAuthzsPB.Authz {
authzsPB.Authz = append(authzsPB.Authz, authzPB)
}
}
}
return authzsPB, nil
}