commit
4d8db36b2e
1
Makefile
1
Makefile
|
|
@ -17,6 +17,7 @@ OBJECTS = activity-monitor \
|
|||
boulder-sa \
|
||||
boulder-va \
|
||||
boulder-wfe \
|
||||
expiration-mailer \
|
||||
ocsp-updater \
|
||||
ocsp-responder
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,280 @@
|
|||
// Copyright 2015 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
|
||||
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/mail"
|
||||
"github.com/letsencrypt/boulder/sa"
|
||||
)
|
||||
|
||||
type emailContent struct {
|
||||
ExpirationDate time.Time
|
||||
DaysToExpiration int
|
||||
DNSNames string
|
||||
}
|
||||
|
||||
type mailer struct {
|
||||
stats statsd.Statter
|
||||
log *blog.AuditLogger
|
||||
dbMap *gorp.DbMap
|
||||
mailer mail.Mailer
|
||||
emailTemplate *template.Template
|
||||
nagTimes []time.Duration
|
||||
limit int
|
||||
}
|
||||
|
||||
func (m *mailer) sendNags(parsedCert *x509.Certificate, contacts []core.AcmeURL) error {
|
||||
expiresIn := int(parsedCert.NotAfter.Sub(time.Now()).Hours()/24) + 1
|
||||
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 := time.Now()
|
||||
err = m.mailer.SendMail(emails, msgBuf.String())
|
||||
if err != nil {
|
||||
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.SendFailure", 1, 1.0)
|
||||
return err
|
||||
}
|
||||
m.stats.TimingDuration("Mailer.Expiration.Sending", time.Since(startSending), 1.0)
|
||||
m.stats.Inc("Mailer.Expiration.Sent", int64(len(emails)), 1.0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mailer) updateCertStatus(serial string) error {
|
||||
// Update CertificateStatus object
|
||||
tx, err := m.dbMap.Begin()
|
||||
if err != nil {
|
||||
m.log.Err(fmt.Sprintf("Error opening transaction for certificate %s: %s", serial, err))
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
csObj, err := tx.Get(&core.CertificateStatus{}, serial)
|
||||
if err != nil {
|
||||
m.log.Err(fmt.Sprintf("Error fetching status for certificate %s: %s", serial, err))
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
certStatus := csObj.(*core.CertificateStatus)
|
||||
certStatus.LastExpirationNagSent = time.Now()
|
||||
|
||||
_, err = tx.Update(certStatus)
|
||||
if err != nil {
|
||||
m.log.Err(fmt.Sprintf("Error updating status for certificate %s: %s", serial, err))
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
m.log.Err(fmt.Sprintf("Error commiting transaction for certificate %s: %s", serial, err))
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mailer) processCerts(certs []core.Certificate) {
|
||||
m.log.Info(fmt.Sprintf("expiration-mailer: Found %d certificates, starting sending messages", len(certs)))
|
||||
for _, cert := range certs {
|
||||
regObj, err := m.dbMap.Get(&core.Registration{}, cert.RegistrationID)
|
||||
if err != nil {
|
||||
m.log.Err(fmt.Sprintf("Error fetching registration %d: %s", cert.RegistrationID, err))
|
||||
m.stats.Inc("Mailer.Expiration.Errors.GetRegistration", 1, 1.0)
|
||||
continue
|
||||
}
|
||||
reg := regObj.(*core.Registration)
|
||||
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
|
||||
}
|
||||
err = m.sendNags(parsedCert, reg.Contact)
|
||||
if err != nil {
|
||||
m.log.Err(fmt.Sprintf("Error sending nag emails: %s", err))
|
||||
m.stats.Inc("Mailer.Expiration.Errors.SendingNags", 1, 1.0)
|
||||
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
|
||||
}
|
||||
}
|
||||
m.log.Info("expiration-mailer: Finished sending messages")
|
||||
return
|
||||
}
|
||||
|
||||
func (m *mailer) findExpiringCertificates() error {
|
||||
now := time.Now()
|
||||
// E.g. m.NagTimes = [1, 3, 7, 14] days from expiration
|
||||
for i, expiresIn := range m.nagTimes {
|
||||
left := now
|
||||
if i > 0 {
|
||||
left = left.Add(m.nagTimes[i-1])
|
||||
}
|
||||
right := now.Add(expiresIn)
|
||||
|
||||
m.log.Info(fmt.Sprintf("expiration-mailer: Searching for certificates that expire between %s and %s", left, right))
|
||||
var certs []core.Certificate
|
||||
_, err := m.dbMap.Select(
|
||||
&certs,
|
||||
`SELECT cert.* FROM certificates AS cert
|
||||
JOIN certificateStatus AS cs
|
||||
ON cs.serial = cert.serial
|
||||
JOIN registrations AS reg
|
||||
ON cert.registrationId = reg.id
|
||||
WHERE reg.contact LIKE "%mailto%"
|
||||
AND cert.expires > :cutoffA
|
||||
AND cert.expires < :cutoffB
|
||||
AND cert.status != "revoked"
|
||||
AND cs.lastExpirationNagSent <= :nagCutoff
|
||||
ORDER BY cert.expires ASC
|
||||
LIMIT :limit`,
|
||||
map[string]interface{}{
|
||||
"cutoffA": left,
|
||||
"cutoffB": right,
|
||||
"nagCutoff": time.Now().Add(-expiresIn),
|
||||
"limit": m.limit,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.log.Err(fmt.Sprintf("expiration-mailer: Error loading certificates: %s", err))
|
||||
return err // fatal
|
||||
}
|
||||
if len(certs) > 0 {
|
||||
processingStarted := time.Now()
|
||||
m.processCerts(certs)
|
||||
m.stats.TimingDuration("Mailer.Expiration.ProcessingCertificates", time.Since(processingStarted), 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type durationSlice []time.Duration
|
||||
|
||||
func (ds durationSlice) Len() int {
|
||||
return len(ds)
|
||||
}
|
||||
|
||||
func (ds durationSlice) Less(a, b int) bool {
|
||||
return ds[a] < ds[b]
|
||||
}
|
||||
|
||||
func (ds durationSlice) Swap(a, b int) {
|
||||
ds[a], ds[b] = ds[b], ds[a]
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cmd.NewAppShell("expiration-mailer")
|
||||
|
||||
app.App.Flags = append(app.App.Flags, cli.IntFlag{
|
||||
Name: "cert_limit",
|
||||
Value: 100,
|
||||
EnvVar: "CERT_LIMIT",
|
||||
Usage: "Count of certificates to process per expiration period",
|
||||
})
|
||||
|
||||
app.Config = func(c *cli.Context, config cmd.Config) cmd.Config {
|
||||
if c.GlobalInt("cert_limit") > 0 {
|
||||
config.Mailer.CertLimit = c.GlobalInt("cert_limit")
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
app.Action = func(c cmd.Config) {
|
||||
// Set up logging
|
||||
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
|
||||
cmd.FailOnError(err, "Couldn't connect to statsd")
|
||||
|
||||
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
|
||||
cmd.FailOnError(err, "Could not connect to Syslog")
|
||||
|
||||
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
|
||||
defer auditlogger.AuditPanic()
|
||||
|
||||
blog.SetAuditLogger(auditlogger)
|
||||
|
||||
auditlogger.Info(app.VersionString())
|
||||
|
||||
go cmd.DebugServer(c.Mailer.DebugAddr)
|
||||
|
||||
// Configure DB
|
||||
dbMap, err := sa.NewDbMap(c.Mailer.DBDriver, c.Mailer.DBConnect)
|
||||
cmd.FailOnError(err, "Could not connect to database")
|
||||
|
||||
// Load email template
|
||||
emailTmpl, err := ioutil.ReadFile(c.Mailer.EmailTemplate)
|
||||
cmd.FailOnError(err, fmt.Sprintf("Could not read email template file [%s]", c.Mailer.EmailTemplate))
|
||||
tmpl, err := template.New("expiry-email").Parse(string(emailTmpl))
|
||||
cmd.FailOnError(err, "Could not parse email template")
|
||||
|
||||
mailClient := mail.New(c.Mailer.Server, c.Mailer.Port, c.Mailer.Username, c.Mailer.Password)
|
||||
|
||||
var nags durationSlice
|
||||
for _, nagDuration := range c.Mailer.NagTimes {
|
||||
dur, err := time.ParseDuration(nagDuration)
|
||||
if err != nil {
|
||||
auditlogger.Err(fmt.Sprintf("Failed to parse nag duration string [%s]: %s", nagDuration, err))
|
||||
return
|
||||
}
|
||||
nags = append(nags, dur)
|
||||
}
|
||||
// Make sure durations are sorted in increasing order
|
||||
sort.Sort(nags)
|
||||
|
||||
m := mailer{
|
||||
stats: stats,
|
||||
log: auditlogger,
|
||||
dbMap: dbMap,
|
||||
mailer: &mailClient,
|
||||
emailTemplate: tmpl,
|
||||
nagTimes: nags,
|
||||
limit: c.Mailer.CertLimit,
|
||||
}
|
||||
|
||||
auditlogger.Info("expiration-mailer: Starting")
|
||||
err = m.findExpiringCertificates()
|
||||
cmd.FailOnError(err, "expiration-mailer has failed")
|
||||
}
|
||||
|
||||
app.Run()
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
// Copyright 2015 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
||||
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/mocks"
|
||||
"github.com/letsencrypt/boulder/sa"
|
||||
"github.com/letsencrypt/boulder/test"
|
||||
)
|
||||
|
||||
func bigIntFromB64(b64 string) *big.Int {
|
||||
bytes, _ := base64.URLEncoding.DecodeString(b64)
|
||||
x := big.NewInt(0)
|
||||
x.SetBytes(bytes)
|
||||
return x
|
||||
}
|
||||
|
||||
func intFromB64(b64 string) int {
|
||||
return int(bigIntFromB64(b64).Int64())
|
||||
}
|
||||
|
||||
type mockMail struct {
|
||||
Messages []string
|
||||
}
|
||||
|
||||
func (m *mockMail) Clear() {
|
||||
m.Messages = []string{}
|
||||
}
|
||||
|
||||
func (m *mockMail) SendMail(to []string, msg string) (err error) {
|
||||
for _ = range to {
|
||||
m.Messages = append(m.Messages, msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`
|
||||
|
||||
var jsonKeyA = []byte(`{
|
||||
"kty":"RSA",
|
||||
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
|
||||
"e":"AQAB"
|
||||
}`)
|
||||
var jsonKeyB = []byte(`{
|
||||
"kty":"RSA",
|
||||
"n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w",
|
||||
"e":"AAEAAQ"
|
||||
}`)
|
||||
|
||||
var log = mocks.UseMockLog()
|
||||
|
||||
func TestSendNags(t *testing.T) {
|
||||
tmpl, err := template.New("expiry-email").Parse(testTmpl)
|
||||
test.AssertNotError(t, err, "Couldn't parse test email template")
|
||||
stats, _ := statsd.NewNoopClient(nil)
|
||||
mc := mockMail{}
|
||||
m := mailer{
|
||||
stats: stats,
|
||||
mailer: &mc,
|
||||
emailTemplate: tmpl,
|
||||
}
|
||||
|
||||
cert := &x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "happy",
|
||||
},
|
||||
NotAfter: time.Now().AddDate(0, 0, 2),
|
||||
DNSNames: []string{"example.com"},
|
||||
}
|
||||
|
||||
email, _ := url.Parse("mailto:rolandshoemaker@gmail.com")
|
||||
emailB, _ := url.Parse("mailto:test@gmail.com")
|
||||
|
||||
err = m.sendNags(cert, []core.AcmeURL{core.AcmeURL(*email)})
|
||||
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])
|
||||
|
||||
mc.Clear()
|
||||
err = m.sendNags(cert, []core.AcmeURL{core.AcmeURL(*email), core.AcmeURL(*emailB)})
|
||||
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])
|
||||
|
||||
mc.Clear()
|
||||
err = m.sendNags(cert, []core.AcmeURL{})
|
||||
test.AssertNotError(t, err, "Not an error to pass no email contacts")
|
||||
test.AssertEquals(t, len(mc.Messages), 0)
|
||||
}
|
||||
|
||||
var n = bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==")
|
||||
var e = intFromB64("AQAB")
|
||||
var d = bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==")
|
||||
var p = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
|
||||
var q = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
|
||||
|
||||
var testKey = rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{N: n, E: e},
|
||||
D: d,
|
||||
Primes: []*big.Int{p, q},
|
||||
}
|
||||
|
||||
func TestFindExpiringCertificates(t *testing.T) {
|
||||
dbMap, err := sa.NewDbMap("sqlite3", ":memory:")
|
||||
test.AssertNotError(t, err, "Couldn't connect to SQLite")
|
||||
err = dbMap.CreateTablesIfNotExists()
|
||||
test.AssertNotError(t, err, "Couldn't create tables")
|
||||
tmpl, err := template.New("expiry-email").Parse(testTmpl)
|
||||
test.AssertNotError(t, err, "Couldn't parse test email template")
|
||||
stats, _ := statsd.NewNoopClient(nil)
|
||||
mc := mockMail{}
|
||||
m := mailer{
|
||||
log: blog.GetAuditLogger(),
|
||||
stats: stats,
|
||||
mailer: &mc,
|
||||
emailTemplate: tmpl,
|
||||
dbMap: dbMap,
|
||||
nagTimes: []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7},
|
||||
limit: 100,
|
||||
}
|
||||
|
||||
log.Clear()
|
||||
err = m.findExpiringCertificates()
|
||||
test.AssertNotError(t, err, "Failed on no certificates")
|
||||
test.AssertEquals(t, len(log.GetAllMatching("Searching for certificates that expire between.*")), 3)
|
||||
|
||||
// Add some expiring certificates and registrations
|
||||
emailA, _ := url.Parse("mailto:one@mail.com")
|
||||
emailB, _ := url.Parse("mailto:twp@mail.com")
|
||||
var keyA jose.JsonWebKey
|
||||
var keyB jose.JsonWebKey
|
||||
err = json.Unmarshal(jsonKeyA, &keyA)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
|
||||
err = json.Unmarshal(jsonKeyB, &keyB)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
|
||||
regA := &core.Registration{
|
||||
ID: 1,
|
||||
Contact: []core.AcmeURL{
|
||||
core.AcmeURL(*emailA),
|
||||
},
|
||||
Key: keyA,
|
||||
}
|
||||
regB := &core.Registration{
|
||||
ID: 2,
|
||||
Contact: []core.AcmeURL{
|
||||
core.AcmeURL(*emailB),
|
||||
},
|
||||
Key: keyB,
|
||||
}
|
||||
rawCertA := x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "happy A",
|
||||
},
|
||||
NotAfter: time.Now().AddDate(0, 0, 1),
|
||||
DNSNames: []string{"example-a.com"},
|
||||
SerialNumber: big.NewInt(1337),
|
||||
}
|
||||
certDerA, _ := x509.CreateCertificate(rand.Reader, &rawCertA, &rawCertA, &testKey.PublicKey, &testKey)
|
||||
certA := &core.Certificate{
|
||||
RegistrationID: 1,
|
||||
Status: core.StatusValid,
|
||||
Serial: "001",
|
||||
Expires: time.Now().AddDate(0, 0, 1),
|
||||
DER: certDerA,
|
||||
}
|
||||
// Already sent a nag but too long ago
|
||||
certStatusA := &core.CertificateStatus{Serial: "001", LastExpirationNagSent: time.Now().Add(-time.Hour * 24 * 3)}
|
||||
rawCertB := x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "happy B",
|
||||
},
|
||||
NotAfter: time.Now().AddDate(0, 0, 3),
|
||||
DNSNames: []string{"example-b.com"},
|
||||
SerialNumber: big.NewInt(1337),
|
||||
}
|
||||
certDerB, _ := x509.CreateCertificate(rand.Reader, &rawCertB, &rawCertB, &testKey.PublicKey, &testKey)
|
||||
certB := &core.Certificate{
|
||||
RegistrationID: 1,
|
||||
Status: core.StatusValid,
|
||||
Serial: "002",
|
||||
Expires: time.Now().AddDate(0, 0, 3),
|
||||
DER: certDerB,
|
||||
}
|
||||
// Already sent a nag for this period
|
||||
certStatusB := &core.CertificateStatus{Serial: "002", LastExpirationNagSent: time.Now().Add(-time.Hour * 24 * 3)}
|
||||
rawCertC := x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "happy C",
|
||||
},
|
||||
NotAfter: time.Now().AddDate(0, 0, 7),
|
||||
DNSNames: []string{"example-c.com"},
|
||||
SerialNumber: big.NewInt(1337),
|
||||
}
|
||||
certDerC, _ := x509.CreateCertificate(rand.Reader, &rawCertC, &rawCertC, &testKey.PublicKey, &testKey)
|
||||
certC := &core.Certificate{
|
||||
RegistrationID: 2,
|
||||
Status: core.StatusValid,
|
||||
Serial: "003",
|
||||
Expires: time.Now().AddDate(0, 0, 7),
|
||||
DER: certDerC,
|
||||
}
|
||||
certStatusC := &core.CertificateStatus{Serial: "003"}
|
||||
err = dbMap.Insert(regA)
|
||||
test.AssertNotError(t, err, "Couldn't add regA")
|
||||
err = dbMap.Insert(regB)
|
||||
test.AssertNotError(t, err, "Couldn't add regB")
|
||||
err = dbMap.Insert(certA)
|
||||
test.AssertNotError(t, err, "Couldn't add certA")
|
||||
err = dbMap.Insert(certB)
|
||||
test.AssertNotError(t, err, "Couldn't add certB")
|
||||
err = dbMap.Insert(certC)
|
||||
test.AssertNotError(t, err, "Couldn't add certC")
|
||||
err = dbMap.Insert(certStatusA)
|
||||
test.AssertNotError(t, err, "Couldn't add certStatusA")
|
||||
err = dbMap.Insert(certStatusB)
|
||||
test.AssertNotError(t, err, "Couldn't add certStatusB")
|
||||
err = dbMap.Insert(certStatusC)
|
||||
test.AssertNotError(t, err, "Couldn't add certStatusC")
|
||||
|
||||
log.Clear()
|
||||
err = m.findExpiringCertificates()
|
||||
test.AssertNotError(t, err, "Failed to find expiring certs")
|
||||
// Should get 001 and 003
|
||||
test.AssertEquals(t, len(mc.Messages), 2)
|
||||
|
||||
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-a.com is going to expire in 1 days (%s)`, rawCertA.NotAfter.UTC().Format("2006-01-02 15:04:05 -0700 MST")), 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")), mc.Messages[1])
|
||||
|
||||
// A consecutive run shouldn't find anything
|
||||
mc.Clear()
|
||||
log.Clear()
|
||||
err = m.findExpiringCertificates()
|
||||
test.AssertNotError(t, err, "Failed to find expiring certs")
|
||||
test.AssertEquals(t, len(mc.Messages), 0)
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool)
|
|||
log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString))
|
||||
|
||||
var ocspResponse core.OCSPResponse
|
||||
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY createdAt DESC LIMIT 1;",
|
||||
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY createdAt DESC LIMIT 1;",
|
||||
map[string]interface{}{"serial": serialString})
|
||||
if err != nil {
|
||||
present = false
|
||||
|
|
|
|||
15
cmd/shell.go
15
cmd/shell.go
|
|
@ -130,11 +130,22 @@ type Config struct {
|
|||
DBConnect string
|
||||
}
|
||||
|
||||
Mail struct {
|
||||
Mailer struct {
|
||||
Server string
|
||||
Port string
|
||||
Username string
|
||||
Password string
|
||||
|
||||
DBDriver string
|
||||
DBConnect string
|
||||
|
||||
CertLimit int
|
||||
NagTimes []string
|
||||
// Path to a text/template email template
|
||||
EmailTemplate string
|
||||
|
||||
// DebugAddr is the address to run the /debug handlers on.
|
||||
DebugAddr string
|
||||
}
|
||||
|
||||
OCSPResponder struct {
|
||||
|
|
@ -273,7 +284,7 @@ func AmqpChannel(conf Config) (*amqp.Channel, error) {
|
|||
if conf.AMQP.TLS.CertFile != nil || conf.AMQP.TLS.KeyFile != nil {
|
||||
// But they have to give both.
|
||||
if conf.AMQP.TLS.CertFile == nil || conf.AMQP.TLS.KeyFile == nil {
|
||||
err = fmt.Errorf("AMQPS: You must set both of the configuration values AMQP.TLS.KeyFile and AMQP.TLS.CertFile.")
|
||||
err = fmt.Errorf("AMQPS: You must set both of the configuration values AMQP.TLS.KeyFile and AMQP.TLS.CertFile")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -562,6 +562,8 @@ type CertificateStatus struct {
|
|||
// code for 'unspecified').
|
||||
RevokedReason int `db:"revokedReason"`
|
||||
|
||||
LastExpirationNagSent time.Time `db:"lastExpirationNagSent"`
|
||||
|
||||
LockCol int64 `json:"-"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,22 +6,28 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// Mailer defines a mail transfer agent to use for sending mail
|
||||
type Mailer struct {
|
||||
// Mailer provides the interface for a mailer
|
||||
type Mailer interface {
|
||||
SendMail([]string, string) error
|
||||
}
|
||||
|
||||
// MailerImpl defines a mail transfer agent to use for sending mail
|
||||
type MailerImpl struct {
|
||||
Server string
|
||||
Port string
|
||||
Auth smtp.Auth
|
||||
From string
|
||||
}
|
||||
|
||||
// NewMailer constructs a Mailer to represent an account at a particular mail
|
||||
// New constructs a Mailer to represent an account on a particular mail
|
||||
// transfer agent.
|
||||
func NewMailer(server, port, username, password string) Mailer {
|
||||
func New(server, port, username, password string) MailerImpl {
|
||||
auth := smtp.PlainAuth("", username, password, server)
|
||||
return Mailer{
|
||||
return MailerImpl{
|
||||
Server: server,
|
||||
Port: port,
|
||||
Auth: auth,
|
||||
|
|
@ -31,7 +37,7 @@ func NewMailer(server, port, username, password string) Mailer {
|
|||
|
||||
// SendMail sends an email to the provided list of recipients. The email body
|
||||
// is simple text.
|
||||
func (m *Mailer) SendMail(to []string, msg string) (err error) {
|
||||
err = smtp.SendMail(m.Server+":"+m.Port, m.Auth, m.From, to, []byte(msg))
|
||||
func (m *MailerImpl) SendMail(to []string, msg string) (err error) {
|
||||
err = smtp.SendMail(net.JoinHostPort(m.Server, m.Port), m.Auth, m.From, to, []byte(msg))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
3
test.sh
3
test.sh
|
|
@ -17,7 +17,8 @@ TESTDIRS="analysis \
|
|||
sa \
|
||||
test \
|
||||
va \
|
||||
wfe"
|
||||
wfe \
|
||||
cmd/expiration-mailer"
|
||||
# cmd
|
||||
# Godeps
|
||||
|
||||
|
|
|
|||
|
|
@ -157,11 +157,17 @@
|
|||
"debugAddr": "localhost:8007"
|
||||
},
|
||||
|
||||
"mail": {
|
||||
"mailer": {
|
||||
"server": "mail.example.com",
|
||||
"port": "25",
|
||||
"username": "cert-master@example.com",
|
||||
"password": "password"
|
||||
"password": "password",
|
||||
"dbDriver": "sqlite3",
|
||||
"dbConnect": ":memory:",
|
||||
"messageLimit": 0,
|
||||
"nagTimes": ["24h", "72h", "168h", "336h"],
|
||||
"emailTemplate": "test/example-expiration-template",
|
||||
"debugAddr": "localhost:8004"
|
||||
},
|
||||
|
||||
"common": {
|
||||
|
|
|
|||
|
|
@ -144,11 +144,17 @@
|
|||
"debugAddr": "localhost:8006"
|
||||
},
|
||||
|
||||
"mail": {
|
||||
"mailer": {
|
||||
"server": "mail.example.com",
|
||||
"port": "25",
|
||||
"username": "cert-master@example.com",
|
||||
"password": "password"
|
||||
"password": "password",
|
||||
"dbDriver": "sqlite3",
|
||||
"dbConnect": ":memory:",
|
||||
"messageLimit": 0,
|
||||
"nagTimes": ["24h", "72h", "168h", "336h"],
|
||||
"emailTemplate": "test/example-expiration-template",
|
||||
"debugAddr": "localhost:8004"
|
||||
},
|
||||
|
||||
"common": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
Hello,
|
||||
|
||||
Your SSL certificate for names {{.DNSNames}} is going to expire in {{.DaysToExpiration}}
|
||||
days ({{.ExpirationDate}}), make sure you run the renewer before then!
|
||||
|
||||
Regards
|
||||
Loading…
Reference in New Issue