Merge pull request #2575 from letsencrypt/master

Merge master to staging
This commit is contained in:
Daniel McCarney 2017-02-20 13:16:55 -05:00 committed by GitHub
commit f445cf3b32
18 changed files with 146 additions and 556 deletions

View File

@ -46,7 +46,6 @@ env:
install:
- ./test/travis-before-install.sh
- docker-compose pull
- docker pull letsencrypt/boulder-tools
- docker-compose build
script:

View File

@ -41,6 +41,9 @@ container for service boulder" you should double check that your `$GOPATH`
exists and doesn't contain any characters other than letters, numbers, `-`
and `_`.
If you have problems with Docker, you may want to try [removing all containers
and volumes](https://www.digitalocean.com/community/tutorials/how-to-remove-docker-images-containers-and-volumes).
By default, Boulder uses a fake DNS resolver that resolves all hostnames to
127.0.0.1. This is suitable for running integration tests inside the Docker
container. If you want Boulder to be able to communicate with a client running

View File

@ -40,7 +40,7 @@ func TestParseAnswer(t *testing.T) {
func TestQueryCAA(t *testing.T) {
testServ := httptest.NewServer(http.HandlerFunc(mocks.GPDNSHandler))
// TODO(#1989): Close testServ
defer testServ.Close()
req, err := http.NewRequest("GET", testServ.URL, nil)
test.AssertNotError(t, err, "Failed to create request")
@ -63,7 +63,7 @@ func TestQueryCAA(t *testing.T) {
func TestLookupCAA(t *testing.T) {
testSrv := httptest.NewServer(http.HandlerFunc(mocks.GPDNSHandler))
// TODO(#1989): Close testServ
defer testSrv.Close()
cpr := CAADistributedResolver{
logger: log,
@ -119,7 +119,7 @@ func (sbh *slightlyBrokenHandler) Handler(w http.ResponseWriter, r *http.Request
func TestHTTPQuorum(t *testing.T) {
sbh := &slightlyBrokenHandler{}
testSrv := httptest.NewServer(http.HandlerFunc(sbh.Handler))
// TODO(#1989): Close testServ
defer testSrv.Close()
cpr := CAADistributedResolver{
logger: log,

View File

@ -260,11 +260,9 @@ func (m *mailer) findExpiringCertificates() error {
// sequentially fetch the certificate details. This avoids an expensive
// JOIN.
var serials []string
var err error
if features.Enabled(features.CertStatusOptimizationsMigrated) {
_, err = m.dbMap.Select(
&serials,
`SELECT
_, err := m.dbMap.Select(
&serials,
`SELECT
cs.serial
FROM certificateStatus AS cs
WHERE cs.notAfter > :cutoffA
@ -273,35 +271,13 @@ func (m *mailer) findExpiringCertificates() error {
AND COALESCE(TIMESTAMPDIFF(SECOND, cs.lastExpirationNagSent, cs.notAfter) > :nagCutoff, 1)
ORDER BY cs.notAfter ASC
LIMIT :limit`,
map[string]interface{}{
"cutoffA": left,
"cutoffB": right,
"nagCutoff": expiresIn.Seconds(),
"limit": m.limit,
},
)
} else {
_, err = m.dbMap.Select(
&serials,
`SELECT
cert.serial
FROM certificates AS cert
JOIN certificateStatus AS cs
ON cs.serial = cert.serial
AND cert.expires > :cutoffA
AND cert.expires <= :cutoffB
AND cs.status != "revoked"
AND COALESCE(TIMESTAMPDIFF(SECOND, cs.lastExpirationNagSent, cert.expires) > :nagCutoff, 1)
ORDER BY cert.expires ASC
LIMIT :limit`,
map[string]interface{}{
"cutoffA": left,
"cutoffB": right,
"nagCutoff": expiresIn.Seconds(),
"limit": m.limit,
},
)
}
map[string]interface{}{
"cutoffA": left,
"cutoffB": right,
"nagCutoff": expiresIn.Seconds(),
"limit": m.limit,
},
)
if err != nil {
m.log.AuditErr(fmt.Sprintf("expiration-mailer: Error loading certificate serials: %s", err))
return err

View File

@ -313,6 +313,7 @@ func addExpiringCerts(t *testing.T, ctx *testCtx) []core.Certificate {
Serial: serial1String,
LastExpirationNagSent: ctx.fc.Now().AddDate(0, 0, -3),
Status: core.OCSPStatusGood,
NotAfter: rawCertA.NotAfter,
}
// Expires in 3d, already sent 4d nag at 4.5d
@ -335,6 +336,7 @@ func addExpiringCerts(t *testing.T, ctx *testCtx) []core.Certificate {
Serial: serial2String,
LastExpirationNagSent: ctx.fc.Now().Add(-36 * time.Hour),
Status: core.OCSPStatusGood,
NotAfter: rawCertB.NotAfter,
}
// Expires in 7d and change, no nag sent at all yet
@ -354,8 +356,9 @@ func addExpiringCerts(t *testing.T, ctx *testCtx) []core.Certificate {
DER: certDerC,
}
certStatusC := &core.CertificateStatus{
Serial: serial3String,
Status: core.OCSPStatusGood,
Serial: serial3String,
Status: core.OCSPStatusGood,
NotAfter: rawCertC.NotAfter,
}
// Expires in 3d, renewed
@ -375,8 +378,9 @@ func addExpiringCerts(t *testing.T, ctx *testCtx) []core.Certificate {
DER: certDerD,
}
certStatusD := &core.CertificateStatus{
Serial: serial4String,
Status: core.OCSPStatusGood,
Serial: serial4String,
Status: core.OCSPStatusGood,
NotAfter: rawCertD.NotAfter,
}
fqdnStatusD := &core.FQDNSet{
SetHash: []byte("hash of D"),
@ -628,8 +632,9 @@ func TestLifetimeOfACert(t *testing.T) {
}
certStatusA := &core.CertificateStatus{
Serial: serial1String,
Status: core.OCSPStatusGood,
Serial: serial1String,
Status: core.OCSPStatusGood,
NotAfter: rawCertA.NotAfter,
}
setupDBMap, err := sa.NewDbMap(vars.DBConnSAFullPerms, 0)
@ -788,6 +793,7 @@ func TestDedupOnRegistration(t *testing.T) {
Serial: serial1String,
LastExpirationNagSent: time.Unix(0, 0),
Status: core.OCSPStatusGood,
NotAfter: rawCertA.NotAfter,
}
rawCertB := newX509Cert("happy B",
@ -806,6 +812,7 @@ func TestDedupOnRegistration(t *testing.T) {
Serial: serial2String,
LastExpirationNagSent: time.Unix(0, 0),
Status: core.OCSPStatusGood,
NotAfter: rawCertB.NotAfter,
}
setupDBMap, err := sa.NewDbMap(vars.DBConnSAFullPerms, 0)

View File

@ -1,247 +0,0 @@
package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"time"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/metrics"
"github.com/letsencrypt/boulder/sa"
)
type dbAccess interface {
SelectOne(holder interface{}, query string, args ...interface{}) error
Select(holder interface{}, query string, args ...interface{}) ([]interface{}, error)
Exec(query string, args ...interface{}) (sql.Result, error)
}
type backfiller struct {
dbMap dbAccess
log blog.Logger
clk clock.Clock
dryRun bool
batchSize uint
numBatches uint
sleep time.Duration
}
func (b backfiller) printStatus(
serial string, notAfter time.Time, cur, total int, start time.Time) {
// Should never happen
if total <= 0 || cur < 0 || cur > total {
b.log.AuditErr(fmt.Sprintf(
"invalid cur (%d) or total (%d)\n", cur, total))
}
completion := (float32(cur) / float32(total)) * 100
now := b.clk.Now()
elapsed := now.Sub(start)
b.log.Info(
fmt.Sprintf("Updating %q notAfter to %q. Cert. %d of %d [%.2f%%]. Elapsed: %s",
serial, notAfter, cur+1, total, completion, elapsed.String()))
}
func (b backfiller) backfill(certStatus *core.CertificateStatus) error {
// We explicitly use `Exec` over `Update` to avoid contention on the
// `LockCol` field that Gorp uses for optimistic locking. With an
// `ocsp-updater` running at the same time as a backfill there is a pretty
// good chance they would clobber each others `LockCol` values if we used
// `Update()` instead of a raw `Exec()`.
_, err := b.dbMap.Exec(
`UPDATE certificateStatus
SET notAfter=?
WHERE serial=?`,
certStatus.NotAfter,
certStatus.Serial,
)
return err
}
func (b backfiller) findEmpty() ([]*core.CertificateStatus, error) {
var certs []*core.CertificateStatus
_, err := b.dbMap.Select(&certs,
`SELECT
serial
FROM certificateStatus
WHERE notAfter IS NULL
LIMIT :batchSize`,
map[string]interface{}{
"batchSize": b.batchSize,
},
)
return certs, err
}
func (b backfiller) populateNotAfter(certs []*core.CertificateStatus) error {
for _, cs := range certs {
var c core.Certificate
err := b.dbMap.SelectOne(&c,
`SELECT expires
FROM certificates
WHERE serial = :serial`,
map[string]interface{}{
"serial": cs.Serial,
})
if err != nil {
return err
}
cs.NotAfter = c.Expires
}
return nil
}
func (b backfiller) processBatch() (int, error) {
certs, err := b.findEmpty()
if err != nil {
return 0, err
}
b.log.Info(fmt.Sprintf("Found %d certificates for this batch", len(certs)))
if len(certs) == 0 {
return 0, nil // Nothing to backfill!
}
err = b.populateNotAfter(certs)
if err != nil {
return 0, err
}
startTime := b.clk.Now()
for i, c := range certs {
b.printStatus(c.Serial, c.NotAfter, i, len(certs), startTime)
if !b.dryRun {
err := b.backfill(c)
if err != nil {
return i, err
}
}
}
return len(certs), nil
}
func (b backfiller) processForever() error {
var batchNum uint
for {
start := b.clk.Now()
b.log.Info(fmt.Sprintf("Starting to process batch %d", batchNum+1))
processed, err := b.processBatch()
now := b.clk.Now()
elapsed := now.Sub(start)
if err != nil {
return err
}
b.log.Info(fmt.Sprintf("Batch %d finished. Processed %d certificates in %s",
batchNum+1, processed, elapsed))
if processed == 0 {
b.log.Info("No more certificates to process. Terminating.")
break
}
batchNum++
if batchNum >= b.numBatches {
b.log.Info(fmt.Sprintf("Reached numBatches (%d). Terminating.", b.numBatches))
break
}
b.log.Info(fmt.Sprintf("Sleeping for %s before next batch", b.sleep))
b.clk.Sleep(b.sleep)
}
return nil
}
const usageIntro = `
Introduction:
The "20160817143417_AddCertStatusNotAfter.sql" db migration adds a "notAfter"
column to the certificateStatus database table. This field duplicates the
contents of the certificates table "expires" column. This enables performance
improvements[0] for both the ocsp-updater and the expiration-mailer utilities.
Since existing rows will have a NULL value in the new field the
notafter-backfill utility exists to perform a one-time update of the existing
certificateStatus rows to set their notAfter column based on the data that
exists in the certificates table.
[0] https://github.com/letsencrypt/boulder/issues/1864
Examples:
Process 50 certificates at a time, printing the updates but not performing
them:
notafter-backfill -config test/config/notafter-backfiller.json -batchSize=50
-dryRun=true
Process 1000 certificates at a time, quitting after 5 batches (5000
certificates) and sleeping 10 minutes between batches:
notafter-backfill -config test/config/notafter-backfiller.json -batchSize=1000
-numBatches=5 -sleep=5m -dryRun=false
Required arguments:
- config
`
func main() {
dryRun := flag.Bool("dryRun", true, "Whether to do a dry run.")
sleep := flag.Duration("sleep", 60*time.Second, "How long to sleep between batches.")
batchSize := flag.Uint("batchSize", 1000, "Number of certificates to process between sleeps.")
numBatches := flag.Uint("numBatches", 999999, "Stop processing after N batches.")
type config struct {
NotAfterBackFiller struct {
cmd.DBConfig
}
Statsd cmd.StatsdConfig
Syslog cmd.SyslogConfig
}
configFile := flag.String("config", "", "File containing a JSON config.")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "%s\n\n", usageIntro)
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if *configFile == "" {
flag.Usage()
os.Exit(1)
}
configData, err := ioutil.ReadFile(*configFile)
cmd.FailOnError(err, fmt.Sprintf("Reading %q", *configFile))
var cfg config
err = json.Unmarshal(configData, &cfg)
cmd.FailOnError(err, "Unmarshaling config")
stats, log := cmd.StatsAndLogging(cfg.Statsd, cfg.Syslog)
defer log.AuditPanic()
dbURL, err := cfg.NotAfterBackFiller.DBConfig.URL()
cmd.FailOnError(err, "Couldn't load DB URL")
dbMap, err := sa.NewDbMap(dbURL, 10)
cmd.FailOnError(err, "Could not connect to database")
go sa.ReportDbConnCount(dbMap, metrics.NewStatsdScope(stats, "NotAfterBackfiller"))
b := backfiller{
dbMap: dbMap,
log: log,
clk: cmd.Clock(),
dryRun: *dryRun,
batchSize: *batchSize,
numBatches: *numBatches,
sleep: *sleep,
}
err = b.processForever()
cmd.FailOnError(err, "Could not process certificate batches")
}

View File

@ -269,14 +269,9 @@ func (updater *OCSPUpdater) findStaleOCSPResponses(oldestLastUpdatedTime time.Ti
now := updater.clk.Now()
maxAgeCutoff := now.Add(-updater.ocspStaleMaxAge)
// If CertStatusOptimizationsMigrated is enabled then we can do this query
// using only the `certificateStatus` table, saving an expensive JOIN and
// improving performance substantially
var err error
if features.Enabled(features.CertStatusOptimizationsMigrated) {
_, err = updater.dbMap.Select(
&statuses,
`SELECT
_, err := updater.dbMap.Select(
&statuses,
`SELECT
cs.serial,
cs.status,
cs.revokedDate,
@ -287,37 +282,12 @@ func (updater *OCSPUpdater) findStaleOCSPResponses(oldestLastUpdatedTime time.Ti
AND NOT cs.isExpired
ORDER BY cs.ocspLastUpdated ASC
LIMIT :limit`,
map[string]interface{}{
"lastUpdate": oldestLastUpdatedTime,
"maxAge": maxAgeCutoff,
"limit": batchSize,
},
)
// If the migration hasn't been applied we don't have the `isExpired` or
// `notAfter` fields on the certificate status table to use and must do the
// expensive JOIN on `certificates`
} else {
_, err = updater.dbMap.Select(
&statuses,
`SELECT
cs.serial,
cs.status,
cs.revokedDate
FROM certificateStatus AS cs
JOIN certificates AS cert
ON cs.serial = cert.serial
WHERE cs.ocspLastUpdated > :maxAge
AND cs.ocspLastUpdated < :lastUpdate
AND cert.expires > now()
ORDER BY cs.ocspLastUpdated ASC
LIMIT :limit`,
map[string]interface{}{
"lastUpdate": oldestLastUpdatedTime,
"maxAge": maxAgeCutoff,
"limit": batchSize,
},
)
}
map[string]interface{}{
"lastUpdate": oldestLastUpdatedTime,
"maxAge": maxAgeCutoff,
"limit": batchSize,
},
)
if err == sql.ErrNoRows {
return statuses, nil
}
@ -326,21 +296,11 @@ func (updater *OCSPUpdater) findStaleOCSPResponses(oldestLastUpdatedTime time.Ti
func (updater *OCSPUpdater) getCertificatesWithMissingResponses(batchSize int) ([]core.CertificateStatus, error) {
const query = "WHERE ocspLastUpdated = 0 LIMIT ?"
var statuses []core.CertificateStatus
var err error
if features.Enabled(features.CertStatusOptimizationsMigrated) {
statuses, err = sa.SelectCertificateStatusesv2(
updater.dbMap,
query,
batchSize,
)
} else {
statuses, err = sa.SelectCertificateStatuses(
updater.dbMap,
query,
batchSize,
)
}
statuses, err := sa.SelectCertificateStatuses(
updater.dbMap,
query,
batchSize,
)
if err == sql.ErrNoRows {
return statuses, nil
}
@ -456,23 +416,12 @@ func (updater *OCSPUpdater) newCertificateTick(ctx context.Context, batchSize in
func (updater *OCSPUpdater) findRevokedCertificatesToUpdate(batchSize int) ([]core.CertificateStatus, error) {
const query = "WHERE status = ? AND ocspLastUpdated <= revokedDate LIMIT ?"
var statuses []core.CertificateStatus
var err error
if features.Enabled(features.CertStatusOptimizationsMigrated) {
statuses, err = sa.SelectCertificateStatusesv2(
updater.dbMap,
query,
string(core.OCSPStatusRevoked),
batchSize,
)
} else {
statuses, err = sa.SelectCertificateStatuses(
updater.dbMap,
query,
string(core.OCSPStatusRevoked),
batchSize,
)
}
statuses, err := sa.SelectCertificateStatuses(
updater.dbMap,
query,
string(core.OCSPStatusRevoked),
batchSize,
)
return statuses, err
}
@ -559,16 +508,11 @@ func (updater *OCSPUpdater) oldOCSPResponsesTick(ctx context.Context, batchSize
tickEnd := updater.clk.Now()
updater.stats.TimingDuration("oldOCSPResponsesTick.QueryTime", tickEnd.Sub(tickStart))
// If the CertStatusOptimizationsMigrated flag is set then we need to
// opportunistically update the certificateStatus `isExpired` column for expired
// certificates we come across
if features.Enabled(features.CertStatusOptimizationsMigrated) {
for _, s := range statuses {
if !s.IsExpired && tickStart.After(s.NotAfter) {
err := updater.markExpired(s)
if err != nil {
return err
}
for _, s := range statuses {
if !s.IsExpired && tickStart.After(s.NotAfter) {
err := updater.markExpired(s)
if err != nil {
return err
}
}
}

View File

@ -4,21 +4,25 @@ While Boulder attempts to implement the ACME specification as strictly as possib
This document details these differences, since ACME is not yet finalized it will be updated as numbered drafts are published.
Current draft: [`draft-ietf-acme-acme-04`](https://tools.ietf.org/html/draft-ietf-acme-acme-04).
Current draft: [`draft-ietf-acme-acme-05`](https://tools.ietf.org/html/draft-ietf-acme-acme-05).
## [Section 5.2](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-5.2)
## [Section 5](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5)
Boulder does not implement the [general JWS syntax](https://tools.ietf.org/html/rfc7515#page-20), but only accepts the [flattened syntax](https://tools.ietf.org/html/rfc7515#page-21).
## [Section 5.2](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.2)
Boulder enforces the presence of the `jwk` field in JWS objects, and does not support the `kid` field.
## [Section 5.4.1](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-5.4.1)
## [Section 5.4.1](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.4.1)
Boulder does not use the `url` field from the JWS protected resource. Instead Boulder will validate the `resource` field from the JWS payload matches the resource being requested. Boulder implements the resource types described in [draft-ietf-acme-02 Section 6.1](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1) plus the additional "KeyChange" resource. Boulder verifies the `resource` field contains the `/directory` URI for the requested resource.
## [Section 5.6.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-5.6)
## [Section 5.6.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.6)
Boulder does not provide a `Retry-After` header when a user hits a rate-limit, nor does it provide `Link` headers to further documentation on rate-limiting.
## [Section 5.7.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-5.7)
## [Section 5.7.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.7)
Boulder doesn't return errors under the `urn:ietf:params:acme:error:` namespace but instead uses the `urn:acme:error:` namespace from [draft-ietf-acme-01 Section 5.4](https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-5.4).
@ -26,52 +30,64 @@ Boulder uses `invalidEmail` in place of the error `invalidContact` defined in [d
Boulder does not implement the `caa` and `dnssec` errors.
## [Section 6.1.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.1)
## [Section 6.1.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.1)
Boulder does not implement the `new-application` resource. Instead of `new-application` Boulder implements the `new-cert` resource that is defined in [draft-ietf-acme-02 Section 6.5](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5). Boulder also doesn't implement the `new-nonce` endpoint.
Boulder does not implement the `new-order` resource. Instead of `new-order` Boulder implements the `new-cert` resource that is defined in [draft-ietf-acme-02 Section 6.5](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5).
## [Section 6.1.1.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.1.1)
Boulder also doesn't implement the `new-nonce` endpoint.
Boulder implements the `new-account` ressource only under the `new-reg` key.
## [Section 6.1.1.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.1.1)
Boulder does not implement the `meta` field returned by the `directory` endpoint.
## [Section 6.1.2.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.1.2)
## [Section 6.1.2.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.1.2)
Boulder does not implement the `terms-of-service-agreed` or `applications` fields in the registration object (nor the endpoints the latter links to).
Boulder does not implement the `terms-of-service-agreed` or `orders` fields in the registration object (nor the endpoints the latter links to).
## [Section 6.1.3.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.1.3)
## [Section 6.1.3.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.1.3)
Boulder does not implement applications, instead it implements the `new-cert` flow from [draft-ietf-acme-02 Section 6.5](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5). Instead of application requirements Boulder currently uses authorizations that are created using the `new-authz` flow from [draft-ietf-acme-02 Section 6.4](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4).
Boulder does not implement orders, instead it implements the `new-cert` flow from [draft-ietf-acme-02 Section 6.5](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5). Instead of authorizations in the order response, Boulder currently uses authorizations that are created using the `new-authz` flow from [draft-ietf-acme-02 Section 6.4](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4).
## [Section 6.1.4.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.1.4)
## [Section 6.1.4.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.1.4)
Boulder does not implement the `scope` field in authorization objects.
## [Section 6.2.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.2)
## [Section 6.2.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.2)
Boulder doesn't implement the `new-nonce` endpoint, instead it responds to `HEAD` requests with a valid `Replay-Nonce` header per [draft-ietf-acme-03 Section 5.4](https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-5.4).
## [Section 6.3.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.3)
## [Section 6.3.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.3)
Boulder only allows `mailto` URIs in the registrations `contact` list.
Boulder uses a HTTP status code 409 (Conflict) response for an already existing registration instead of 200 (OK). Boulder returns the URI of the already existing registration in a `Location` header field instead of a `Content-Location` header field.
## [Section 6.3.2.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.3.2)
## [Section 6.3.3.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.3.3)
Boulder implements draft-04 style key roll-over with a few divergences. Since Boulder doesn't currently use the registration URL to identify users we do not check for that field in the JWS protected headers but do check for it in the inner payload. Boulder also requires the outer JWS payload contains the `"resource": "key-change"` field.
Boulder implements draft-05 style key roll-over with a few divergences. Since Boulder doesn't currently use the registration URL to identify users we do not check for that field in the JWS protected headers but do check for it in the inner payload. Boulder also requires the outer JWS payload contains the `"resource": "key-change"` field.
## [Section 6.4.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-6.4)
## [Section 6.4.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.4)
Boulder does not implement applications, instead it implements the `new-cert` flow from [draft-ietf-acme-02 Section 6.5](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5). Instead of application requirements Boulder currently uses authorizations that are created using the `new-authz` flow from [draft-ietf-acme-02 Section 6.4](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4). Certificates are not proactively issued, a user must request issuance via the `new-cert` endpoint instead of assuming a certificate will be created once all required authorizations are validated.
Boulder does not implement orders, instead it implements the `new-cert` flow from [draft-ietf-acme-02 Section 6.5](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5). Instead of authorizations in the order response, Boulder currently uses authorizations that are created using the `new-authz` flow from [draft-ietf-acme-02 Section 6.4](https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4). Certificates are not proactively issued, a user must request issuance via the `new-cert` endpoint instead of assuming a certificate will be created once all required authorizations are validated.
## [Section 7.3.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-7.3)
## [Section 6.4.1.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-6.4.1)
Boulder ignores the `existing` field in authorization request objects.
## [Section 7.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-7)
Boulder returns an `uri` instead of an `url` field in challenge objects.
## [Section 7.3.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-7.3)
Boulder implements `tls-sni-01` from [draft-ietf-acme-01 Section 7.3](https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3) instead of the `tls-sni-02` validation method.
## [Section 7.5.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-7.5)
## [Section 7.5.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-7.5)
Boulder does not implement the `oob-01` validation method.
## [Section 8.5.](https://tools.ietf.org/html/draft-ietf-acme-acme-04#section-8.5)
## [Section 8.5.](https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-8.5)
Boulder uses the `urn:acme:` namespace from [draft-ietf-acme-01 Section 5.4](https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-5.4) for errors instead of `urn:ietf:params:acme:`.

View File

@ -4,9 +4,9 @@ package features
import "fmt"
const _FeatureFlag_name = "unusedIDNASupportAllowAccountDeactivationCertStatusOptimizationsMigratedAllowKeyRolloverResubmitMissingSCTsOnlyGoogleSafeBrowsingV4UseAIAIssuerURL"
const _FeatureFlag_name = "unusedIDNASupportAllowAccountDeactivationAllowKeyRolloverResubmitMissingSCTsOnlyGoogleSafeBrowsingV4UseAIAIssuerURL"
var _FeatureFlag_index = [...]uint8{0, 6, 17, 41, 72, 88, 111, 131, 146}
var _FeatureFlag_index = [...]uint8{0, 6, 17, 41, 57, 80, 100, 115}
func (i FeatureFlag) String() string {
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {

View File

@ -14,7 +14,6 @@ const (
unused FeatureFlag = iota // unused is used for testing
IDNASupport
AllowAccountDeactivation
CertStatusOptimizationsMigrated
AllowKeyRollover
ResubmitMissingSCTsOnly
GoogleSafeBrowsingV4
@ -23,14 +22,13 @@ const (
// List of features and their default value, protected by fMu
var features = map[FeatureFlag]bool{
unused: false,
IDNASupport: false,
AllowAccountDeactivation: false,
CertStatusOptimizationsMigrated: false,
AllowKeyRollover: false,
ResubmitMissingSCTsOnly: false,
GoogleSafeBrowsingV4: false,
UseAIAIssuerURL: false,
unused: false,
IDNASupport: false,
AllowAccountDeactivation: false,
AllowKeyRollover: false,
ResubmitMissingSCTsOnly: false,
GoogleSafeBrowsingV4: false,
UseAIAIssuerURL: false,
}
var fMu = new(sync.RWMutex)

View File

@ -219,11 +219,5 @@ func initTables(dbMap *gorp.DbMap) {
dbMap.AddTableWithName(core.CRL{}, "crls").SetKeys(false, "Serial")
dbMap.AddTableWithName(core.SignedCertificateTimestamp{}, "sctReceipts").SetKeys(true, "ID").SetVersionCol("LockCol")
dbMap.AddTableWithName(core.FQDNSet{}, "fqdnSets").SetKeys(true, "ID")
// TODO(@cpu): Delete these table maps when the `CertStatusOptimizationsMigrated` feature flag is removed
if features.Enabled(features.CertStatusOptimizationsMigrated) {
dbMap.AddTableWithName(certStatusModelv2{}, "certificateStatus").SetKeys(false, "Serial").SetVersionCol("LockCol")
} else {
dbMap.AddTableWithName(certStatusModelv1{}, "certificateStatus").SetKeys(false, "Serial").SetVersionCol("LockCol")
}
dbMap.AddTableWithName(certStatusModel{}, "certificateStatus").SetKeys(false, "Serial").SetVersionCol("LockCol")
}

View File

@ -116,12 +116,11 @@ func SelectCertificates(s dbSelector, q string, args map[string]interface{}) ([]
return models, err
}
const certStatusFields = "serial, subscriberApproved, status, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent, ocspResponse, LockCol"
const certStatusFieldsv2 = certStatusFields + ", notAfter, isExpired"
const certStatusFields = "serial, subscriberApproved, status, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent, ocspResponse, LockCol, notAfter, isExpired"
// SelectCertificateStatus selects all fields of one certificate status model
func SelectCertificateStatus(s dbOneSelector, q string, args ...interface{}) (certStatusModelv1, error) {
var model certStatusModelv1
func SelectCertificateStatus(s dbOneSelector, q string, args ...interface{}) (certStatusModel, error) {
var model certStatusModel
err := s.SelectOne(
&model,
"SELECT "+certStatusFields+" FROM certificateStatus "+q,
@ -130,17 +129,6 @@ func SelectCertificateStatus(s dbOneSelector, q string, args ...interface{}) (ce
return model, err
}
// SelectCertificateStatusv2 selects all fields (including the v2 migrated fields) of one certificate status model
func SelectCertificateStatusv2(s dbOneSelector, q string, args ...interface{}) (certStatusModelv2, error) {
var model certStatusModelv2
err := s.SelectOne(
&model,
"SELECT "+certStatusFieldsv2+" FROM certificateStatus "+q,
args...,
)
return model, err
}
// SelectCertificateStatuses selects all fields of multiple certificate status objects
func SelectCertificateStatuses(s dbSelector, q string, args ...interface{}) ([]core.CertificateStatus, error) {
var models []core.CertificateStatus
@ -152,17 +140,6 @@ func SelectCertificateStatuses(s dbSelector, q string, args ...interface{}) ([]c
return models, err
}
// SelectCertificateStatusesv2 selects all fields (including the v2 migrated fields) of multiple certificate status objects
func SelectCertificateStatusesv2(s dbSelector, q string, args ...interface{}) ([]core.CertificateStatus, error) {
var models []core.CertificateStatus
_, err := s.Select(
&models,
"SELECT "+certStatusFieldsv2+" FROM certificateStatus "+q,
args...,
)
return models, err
}
var mediumBlobSize = int(math.Pow(2, 24))
type issuedNameModel struct {
@ -194,13 +171,7 @@ type regModelv2 struct {
Status string `db:"status"`
}
// We need two certStatus model structs, one for when boulder does *not* have
// the 20160817143417_CertStatusOptimizations.sql migration applied
// (certStatusModelv1) and one for when it does (certStatusModelv2)
//
// TODO(@cpu): Collapse into one struct once the migration has been applied
// & feature flag set.
type certStatusModelv1 struct {
type certStatusModel struct {
Serial string `db:"serial"`
SubscriberApproved bool `db:"subscriberApproved"`
Status core.OCSPStatus `db:"status"`
@ -210,12 +181,8 @@ type certStatusModelv1 struct {
LastExpirationNagSent time.Time `db:"lastExpirationNagSent"`
OCSPResponse []byte `db:"ocspResponse"`
LockCol int64 `json:"-"`
}
type certStatusModelv2 struct {
certStatusModelv1
NotAfter time.Time `db:"notAfter"`
IsExpired bool `db:"isExpired"`
NotAfter time.Time `db:"notAfter"`
IsExpired bool `db:"isExpired"`
}
// challModel is the description of a core.Challenge in the database

123
sa/sa.go
View File

@ -439,48 +439,26 @@ func (ssa *SQLStorageAuthority) GetCertificateStatus(ctx context.Context, serial
}
var status core.CertificateStatus
if features.Enabled(features.CertStatusOptimizationsMigrated) {
statusObj, err := ssa.dbMap.Get(certStatusModelv2{}, serial)
if err != nil {
return status, err
}
if statusObj == nil {
return status, nil
}
statusModel := statusObj.(*certStatusModelv2)
status = core.CertificateStatus{
Serial: statusModel.Serial,
SubscriberApproved: statusModel.SubscriberApproved,
Status: statusModel.Status,
OCSPLastUpdated: statusModel.OCSPLastUpdated,
RevokedDate: statusModel.RevokedDate,
RevokedReason: statusModel.RevokedReason,
LastExpirationNagSent: statusModel.LastExpirationNagSent,
OCSPResponse: statusModel.OCSPResponse,
NotAfter: statusModel.NotAfter,
IsExpired: statusModel.IsExpired,
LockCol: statusModel.LockCol,
}
} else {
statusObj, err := ssa.dbMap.Get(certStatusModelv1{}, serial)
if err != nil {
return status, err
}
if statusObj == nil {
return status, nil
}
statusModel := statusObj.(*certStatusModelv1)
status = core.CertificateStatus{
Serial: statusModel.Serial,
SubscriberApproved: statusModel.SubscriberApproved,
Status: statusModel.Status,
OCSPLastUpdated: statusModel.OCSPLastUpdated,
RevokedDate: statusModel.RevokedDate,
RevokedReason: statusModel.RevokedReason,
LastExpirationNagSent: statusModel.LastExpirationNagSent,
OCSPResponse: statusModel.OCSPResponse,
LockCol: statusModel.LockCol,
}
statusObj, err := ssa.dbMap.Get(certStatusModel{}, serial)
if err != nil {
return status, err
}
if statusObj == nil {
return status, nil
}
statusModel := statusObj.(*certStatusModel)
status = core.CertificateStatus{
Serial: statusModel.Serial,
SubscriberApproved: statusModel.SubscriberApproved,
Status: statusModel.Status,
OCSPLastUpdated: statusModel.OCSPLastUpdated,
RevokedDate: statusModel.RevokedDate,
RevokedReason: statusModel.RevokedReason,
LastExpirationNagSent: statusModel.LastExpirationNagSent,
OCSPResponse: statusModel.OCSPResponse,
NotAfter: statusModel.NotAfter,
IsExpired: statusModel.IsExpired,
LockCol: statusModel.LockCol,
}
return status, nil
@ -520,13 +498,7 @@ func (ssa *SQLStorageAuthority) MarkCertificateRevoked(ctx context.Context, seri
}
const statusQuery = "WHERE serial = ?"
var statusObj interface{}
if features.Enabled(features.CertStatusOptimizationsMigrated) {
statusObj, err = SelectCertificateStatusv2(tx, statusQuery, serial)
} else {
statusObj, err = SelectCertificateStatus(tx, statusQuery, serial)
}
statusObj, err := SelectCertificateStatus(tx, statusQuery, serial)
if err == sql.ErrNoRows {
err = fmt.Errorf("No certificate with serial %s", serial)
err = Rollback(tx, err)
@ -539,19 +511,10 @@ func (ssa *SQLStorageAuthority) MarkCertificateRevoked(ctx context.Context, seri
var n int64
now := ssa.clk.Now()
if features.Enabled(features.CertStatusOptimizationsMigrated) {
status := statusObj.(certStatusModelv2)
status.Status = core.OCSPStatusRevoked
status.RevokedDate = now
status.RevokedReason = reasonCode
n, err = tx.Update(&status)
} else {
status := statusObj.(certStatusModelv1)
status.Status = core.OCSPStatusRevoked
status.RevokedDate = now
status.RevokedReason = reasonCode
n, err = tx.Update(&status)
}
statusObj.Status = core.OCSPStatusRevoked
statusObj.RevokedDate = now
statusObj.RevokedReason = reasonCode
n, err = tx.Update(&statusObj)
if err != nil {
err = Rollback(tx, err)
return err
@ -807,32 +770,16 @@ func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, certDER []by
Expires: parsedCertificate.NotAfter,
}
var certStatusOb interface{}
if features.Enabled(features.CertStatusOptimizationsMigrated) {
certStatusOb = &certStatusModelv2{
certStatusModelv1: certStatusModelv1{
SubscriberApproved: false,
Status: core.OCSPStatus("good"),
OCSPLastUpdated: time.Time{},
OCSPResponse: []byte{},
Serial: serial,
RevokedDate: time.Time{},
RevokedReason: 0,
LockCol: 0,
},
NotAfter: parsedCertificate.NotAfter,
}
} else {
certStatusOb = &certStatusModelv1{
SubscriberApproved: false,
Status: core.OCSPStatus("good"),
OCSPLastUpdated: time.Time{},
OCSPResponse: []byte{},
Serial: serial,
RevokedDate: time.Time{},
RevokedReason: 0,
LockCol: 0,
}
certStatusOb := &certStatusModel{
SubscriberApproved: false,
Status: core.OCSPStatus("good"),
OCSPLastUpdated: time.Time{},
OCSPResponse: []byte{},
Serial: serial,
RevokedDate: time.Time{},
RevokedReason: 0,
LockCol: 0,
NotAfter: parsedCertificate.NotAfter,
}
tx, err := ssa.dbMap.Begin()

View File

@ -426,15 +426,6 @@ func TestGetValidAuthorizationsMultiple(t *testing.T) {
}
func TestAddCertificate(t *testing.T) {
// Enable the feature for the `CertStatusOptimizationsMigrated` flag so that
// adding a new certificate will populate the `certificateStatus.NotAfter`
// field correctly. This will let the unit test assertion for `NotAfter`
// pass provided everything is working as intended. Note: this must be done
// **before** the DbMap is created in `initSA()` or the feature flag won't be
// set correctly at the time the table maps are set up.
_ = features.Set(map[string]bool{"CertStatusOptimizationsMigrated": true})
defer features.Reset()
sa, _, cleanUp := initSA(t)
defer cleanUp()

View File

@ -20,9 +20,6 @@
"saService": {
"serverAddresses": ["sa.boulder:9095"],
"timeout": "15s"
},
"features": {
"CertStatusOptimizationsMigrated": true
}
},

View File

@ -35,8 +35,7 @@
"timeout": "15s"
},
"features": {
"ResubmitMissingSCTsOnly": true,
"CertStatusOptimizationsMigrated": true
"ResubmitMissingSCTsOnly": true
}
},

View File

@ -29,8 +29,7 @@
"serviceQueue": "SA.server"
},
"features": {
"AllowAccountDeactivation": true,
"CertStatusOptimizationsMigrated": true
"AllowAccountDeactivation": true
}
},

View File

@ -1841,7 +1841,7 @@ func TestGetCertificateHEADHasCorrectBodyLength(t *testing.T) {
mux := wfe.Handler()
s := httptest.NewServer(mux)
// TODO(#1989): Close s
defer s.Close()
req, _ := http.NewRequest("HEAD", s.URL+"/acme/cert/0000000000000000000000000000000000b2", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {