Merge branch 'master' into more-revoker

This commit is contained in:
Roland Shoemaker 2016-02-01 15:38:05 -08:00
commit c3a6a837a4
10 changed files with 258 additions and 58 deletions

View File

@ -1,9 +1,5 @@
# Contributing to Boulder
> **Note:** We are currently in a *General Availability* only merge window, meaning
> we will only be reviewing & merging patches which close an issue tagged with the *General
> Availability* milestone.
Thanks for helping us build Boulder, if you haven't already had a chance to look
over our patch submission guidelines take a minute to do so now.

View File

@ -99,6 +99,14 @@ func (mock *MockDNSResolver) LookupCAA(_ context.Context, domain string) ([]*dns
fallthrough
case "servfail.com":
return results, fmt.Errorf("SERVFAIL")
case "multi-crit-present.com":
record.Flag = 1
record.Tag = "issue"
record.Value = "symantec.com"
results = append(results, &record)
secondRecord := record
secondRecord.Value = "letsencrypt.org"
results = append(results, &secondRecord)
}
return results, nil
}

View File

@ -8,8 +8,10 @@ package main
import (
"bytes"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"math"
netmail "net/mail"
"sort"
"strings"
@ -32,7 +34,7 @@ import (
const defaultNagCheckInterval = 24 * time.Hour
type emailContent struct {
ExpirationDate time.Time
ExpirationDate string
DaysToExpiration int
DNSNames string
}
@ -54,35 +56,58 @@ type mailer struct {
clk clock.Clock
}
func (m *mailer) sendNags(parsedCert *x509.Certificate, contacts []*core.AcmeURL) error {
expiresIn := int(parsedCert.NotAfter.Sub(m.clk.Now()).Hours() / 24)
func (m *mailer) sendNags(contacts []*core.AcmeURL, certs []*x509.Certificate) error {
if len(contacts) == 0 {
return nil
}
if len(certs) == 0 {
return errors.New("no certs given to send nags for")
}
emails := []string{}
for _, contact := range contacts {
if contact.Scheme == "mailto" {
emails = append(emails, contact.Opaque)
}
}
if len(emails) > 0 {
email := emailContent{
ExpirationDate: parsedCert.NotAfter,
DaysToExpiration: expiresIn,
DNSNames: strings.Join(parsedCert.DNSNames, ", "),
}
msgBuf := new(bytes.Buffer)
err := m.emailTemplate.Execute(msgBuf, email)
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.TemplateFailure", 1, 1.0)
return err
}
startSending := m.clk.Now()
err = m.mailer.SendMail(emails, m.subject, msgBuf.String())
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.SendFailure", 1, 1.0)
return err
}
m.stats.TimingDuration("Mailer.Expiration.SendLatency", time.Since(startSending), 1.0)
m.stats.Inc("Mailer.Expiration.Sent", int64(len(emails)), 1.0)
if len(emails) == 0 {
return nil
}
expiresIn := time.Duration(math.MaxInt64)
expDate := m.clk.Now()
domains := []string{}
// Pick out the expiration date that is closest to being hit.
for _, cert := range certs {
domains = append(domains, cert.DNSNames...)
possible := cert.NotAfter.Sub(m.clk.Now())
if possible < expiresIn {
expiresIn = possible
expDate = cert.NotAfter
}
}
domains = core.UniqueLowerNames(domains)
sort.Strings(domains)
email := emailContent{
ExpirationDate: expDate.UTC().Format(time.RFC822Z),
DaysToExpiration: int(expiresIn.Hours() / 24),
DNSNames: strings.Join(domains, "\n"),
}
msgBuf := new(bytes.Buffer)
err := m.emailTemplate.Execute(msgBuf, email)
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.TemplateFailure", 1, 1.0)
return err
}
startSending := m.clk.Now()
err = m.mailer.SendMail(emails, m.subject, msgBuf.String())
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.SendFailure", 1, 1.0)
return err
}
m.stats.TimingDuration("Mailer.Expiration.SendLatency", time.Since(startSending), 1.0)
m.stats.Inc("Mailer.Expiration.Sent", int64(len(emails)), 1.0)
return nil
}
@ -121,32 +146,49 @@ func (m *mailer) updateCertStatus(serial string) error {
return nil
}
func (m *mailer) processCerts(certs []core.Certificate) {
m.log.Info(fmt.Sprintf("expiration-mailer: Found %d certificates, starting sending messages", len(certs)))
func (m *mailer) processCerts(allCerts []core.Certificate) {
m.log.Info(fmt.Sprintf("expiration-mailer: Found %d certificates, starting sending messages", len(allCerts)))
for _, cert := range certs {
reg, err := m.rs.GetRegistration(cert.RegistrationID)
regIDToCerts := make(map[int64][]core.Certificate)
for _, cert := range allCerts {
cs := regIDToCerts[cert.RegistrationID]
cs = append(cs, cert)
regIDToCerts[cert.RegistrationID] = cs
}
for regID, certs := range regIDToCerts {
reg, err := m.rs.GetRegistration(regID)
if err != nil {
m.log.Err(fmt.Sprintf("Error fetching registration %d: %s", cert.RegistrationID, err))
m.log.Err(fmt.Sprintf("Error fetching registration %d: %s", regID, err))
m.stats.Inc("Mailer.Expiration.Errors.GetRegistration", 1, 1.0)
continue
}
parsedCert, err := x509.ParseCertificate(cert.DER)
if err != nil {
m.log.Err(fmt.Sprintf("Error parsing certificate %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.ParseCertificate", 1, 1.0)
continue
parsedCerts := []*x509.Certificate{}
for _, cert := range certs {
parsedCert, err := x509.ParseCertificate(cert.DER)
if err != nil {
// TODO(#1420): tell registration about this error
m.log.Err(fmt.Sprintf("Error parsing certificate %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.ParseCertificate", 1, 1.0)
continue
}
parsedCerts = append(parsedCerts, parsedCert)
}
err = m.sendNags(parsedCert, reg.Contact)
err = m.sendNags(reg.Contact, parsedCerts)
if err != nil {
m.log.Err(fmt.Sprintf("Error sending nag emails: %s", err))
continue
}
err = m.updateCertStatus(cert.Serial)
if err != nil {
m.log.Err(fmt.Sprintf("Error updating certificate status for %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.UpdateCertificateStatus", 1, 1.0)
continue
for _, cert := range certs {
err = m.updateCertStatus(cert.Serial)
if err != nil {
m.log.Err(fmt.Sprintf("Error updating certificate status for %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.UpdateCertificateStatus", 1, 1.0)
continue
}
}
}
m.log.Info("expiration-mailer: Finished sending messages")

View File

@ -113,20 +113,20 @@ func TestSendNags(t *testing.T) {
email, _ := core.ParseAcmeURL("mailto:rolandshoemaker@gmail.com")
emailB, _ := core.ParseAcmeURL("mailto:test@gmail.com")
err := m.sendNags(cert, []*core.AcmeURL{email})
err := m.sendNags([]*core.AcmeURL{email}, []*x509.Certificate{cert})
test.AssertNotError(t, err, "Failed to send warning messages")
test.AssertEquals(t, len(mc.Messages), 1)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.RFC822Z)), mc.Messages[0])
mc.Clear()
err = m.sendNags(cert, []*core.AcmeURL{email, emailB})
err = m.sendNags([]*core.AcmeURL{email, emailB}, []*x509.Certificate{cert})
test.AssertNotError(t, err, "Failed to send warning messages")
test.AssertEquals(t, len(mc.Messages), 2)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[1])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.RFC822Z)), mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.RFC822Z)), mc.Messages[1])
mc.Clear()
err = m.sendNags(cert, []*core.AcmeURL{})
err = m.sendNags([]*core.AcmeURL{}, []*x509.Certificate{cert})
test.AssertNotError(t, err, "Not an error to pass no email contacts")
test.AssertEquals(t, len(mc.Messages), 0)
@ -134,7 +134,7 @@ func TestSendNags(t *testing.T) {
test.AssertNotError(t, err, "Failed to parse templates")
for _, template := range templates.Templates() {
m.emailTemplate = template
err = m.sendNags(cert, []*core.AcmeURL{})
err = m.sendNags(nil, []*x509.Certificate{cert})
test.AssertNotError(t, err, "failed to send nag")
}
}
@ -278,8 +278,8 @@ func TestFindExpiringCertificates(t *testing.T) {
// Should get 001 and 003
test.AssertEquals(t, len(ctx.mc.Messages), 2)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-a.com is going to expire in 0 days (%s)`, rawCertA.NotAfter.UTC().Format("2006-01-02 15:04:05 -0700 MST")), ctx.mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-c.com is going to expire in 7 days (%s)`, rawCertC.NotAfter.UTC().Format("2006-01-02 15:04:05 -0700 MST")), ctx.mc.Messages[1])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-a.com is going to expire in 0 days (%s)`, rawCertA.NotAfter.UTC().Format(time.RFC822Z)), ctx.mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-c.com is going to expire in 7 days (%s)`, rawCertC.NotAfter.UTC().Format(time.RFC822Z)), ctx.mc.Messages[1])
// A consecutive run shouldn't find anything
ctx.mc.Clear()
@ -452,6 +452,88 @@ func TestDontFindRevokedCert(t *testing.T) {
}
}
func TestDedupOnRegistration(t *testing.T) {
expiresIn := 96 * time.Hour
ctx := setup(t, []time.Duration{expiresIn})
var keyA jose.JsonWebKey
err := json.Unmarshal(jsonKeyA, &keyA)
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
regA := core.Registration{
ID: 1,
Contact: []*core.AcmeURL{
email1,
},
Key: keyA,
InitialIP: net.ParseIP("6.5.5.6"),
}
regA, err = ctx.ssa.NewRegistration(regA)
if err != nil {
t.Fatalf("Couldn't store regA: %s", err)
}
rawCertA := newX509Cert("happy A",
ctx.fc.Now().Add(72*time.Hour),
[]string{"example-a.com", "shared-example.com"},
1338,
)
certDerA, _ := x509.CreateCertificate(rand.Reader, rawCertA, rawCertA, &testKey.PublicKey, &testKey)
certA := &core.Certificate{
RegistrationID: regA.ID,
Serial: "001",
Expires: rawCertA.NotAfter,
DER: certDerA,
}
certStatusA := &core.CertificateStatus{
Serial: "001",
LastExpirationNagSent: time.Unix(0, 0),
Status: core.OCSPStatusGood,
}
rawCertB := newX509Cert("happy B",
ctx.fc.Now().Add(48*time.Hour),
[]string{"example-b.com", "shared-example.com"},
1337,
)
certDerB, _ := x509.CreateCertificate(rand.Reader, rawCertB, rawCertB, &testKey.PublicKey, &testKey)
certB := &core.Certificate{
RegistrationID: regA.ID,
Serial: "002",
Expires: rawCertB.NotAfter,
DER: certDerB,
}
certStatusB := &core.CertificateStatus{
Serial: "002",
LastExpirationNagSent: time.Unix(0, 0),
Status: core.OCSPStatusGood,
}
setupDBMap, err := sa.NewDbMap("mysql+tcp://test_setup@localhost:3306/boulder_sa_test")
err = setupDBMap.Insert(certA)
test.AssertNotError(t, err, "Couldn't add certA")
err = setupDBMap.Insert(certB)
test.AssertNotError(t, err, "Couldn't add certB")
err = setupDBMap.Insert(certStatusA)
test.AssertNotError(t, err, "Couldn't add certStatusA")
err = setupDBMap.Insert(certStatusB)
test.AssertNotError(t, err, "Couldn't add certStatusB")
err = ctx.m.findExpiringCertificates()
test.AssertNotError(t, err, "error calling findExpiringCertificates")
if len(ctx.mc.Messages) > 1 {
t.Errorf("num of messages, want %d, got %d", 1, len(ctx.mc.Messages))
}
if len(ctx.mc.Messages) == 0 {
t.Fatalf("no messages sent")
}
domains := "example-a.com\nexample-b.com\nshared-example.com"
expected := fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 1 days (%s)`,
domains,
rawCertB.NotAfter.Format(time.RFC822Z))
test.AssertEquals(t, expected, ctx.mc.Messages[0])
}
type testCtx struct {
dbMap *gorp.DbMap
ssa core.StorageAdder

View File

@ -0,0 +1,68 @@
package main
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"testing"
"time"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/test"
)
var (
email1 = mustParseAcmeURL("mailto:one@example.com")
email2 = mustParseAcmeURL("mailto:two@example.com")
)
func mustParseAcmeURL(acmeURL string) *core.AcmeURL {
c, err := core.ParseAcmeURL(acmeURL)
if err != nil {
panic(fmt.Sprintf("unable to parse as AcmeURL %#v: %s", acmeURL, err))
}
return c
}
func TestSendEarliestCertInfo(t *testing.T) {
expiresIn := 24 * time.Hour
ctx := setup(t, []time.Duration{expiresIn})
defer ctx.cleanUp()
rawCertA := newX509Cert("happy A",
ctx.fc.Now().AddDate(0, 0, 5),
[]string{"example-A.com", "SHARED-example.com"},
1337,
)
rawCertB := newX509Cert("happy B",
ctx.fc.Now().AddDate(0, 0, 2),
[]string{"shared-example.com", "example-b.com"},
1337,
)
ctx.m.sendNags([]*core.AcmeURL{email1, email2}, []*x509.Certificate{rawCertA, rawCertB})
if len(ctx.mc.Messages) != 2 {
t.Errorf("num of messages, want %d, got %d", 2, len(ctx.mc.Messages))
}
if len(ctx.mc.Messages) == 0 {
t.Fatalf("no message sent")
}
domains := "example-a.com\nexample-b.com\nshared-example.com"
expected := fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 2 days (%s)`,
domains,
rawCertB.NotAfter.Format(time.RFC822Z))
test.AssertEquals(t, expected, ctx.mc.Messages[0])
}
func newX509Cert(commonName string, notAfter time.Time, dnsNames []string, serial int64) *x509.Certificate {
return &x509.Certificate{
Subject: pkix.Name{
CommonName: commonName,
},
NotAfter: notAfter,
DNSNames: dnsNames,
SerialNumber: big.NewInt(serial),
}
}

View File

@ -363,6 +363,8 @@ func (m *Mailer) Clear() {
// SendMail is a mock
func (m *Mailer) SendMail(to []string, subject, msg string) (err error) {
// TODO(1421): clean up this To: stuff
for range to {
m.Messages = append(m.Messages, msg)
}

View File

@ -173,6 +173,10 @@ func (pa PolicyAuthorityImpl) WillingToIssue(id core.AcmeIdentifier, regID int64
return errInvalidDNSCharacter
}
if label[len(label)-1] == '-' {
return errInvalidDNSCharacter
}
if punycodeRegexp.MatchString(label) {
return errIDNNotSupported
}

View File

@ -66,8 +66,9 @@ func TestWillingToIssue(t *testing.T) {
{`www.abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.com`, errLabelTooLong}, // Label too long (>63 characters)
{`www.-ombo.com`, errInvalidDNSCharacter}, // Label starts with '-'
{`www.zomb-.com`, errInvalidDNSCharacter}, // Label ends with '-'
{`xn--.net`, errInvalidDNSCharacter}, // Label ends with '-'
{`www.xn--hmr.net`, errIDNNotSupported}, // Punycode (disallowed for now)
{`xn--.net`, errIDNNotSupported}, // No punycode for now.
{`0`, errTooFewLabels},
{`1`, errTooFewLabels},
{`*`, errInvalidDNSCharacter},
@ -123,9 +124,7 @@ func TestWillingToIssue(t *testing.T) {
"www.8675309.com",
"8675309.com",
"zom2bo.com",
"zombo-.com",
"www.zom-bo.com",
"www.zombo-.com",
}
pa, cleanup := paImpl(t)

View File

@ -658,9 +658,6 @@ func (va *ValidationAuthorityImpl) checkCAARecords(ctx context.Context, identifi
if caa.Value == va.IssuerDomain {
valid = true
return
} else if caa.Flag > 0 {
valid = false
return
}
}

View File

@ -679,6 +679,8 @@ func TestCAAChecking(t *testing.T) {
CAATest{"example.co.uk", false, true},
// Good (present)
CAATest{"present.com", true, true},
// Good (multiple critical, one matching)
CAATest{"multi-crit-present.com", true, true},
}
stats, _ := statsd.NewNoopClient()