Compare commits

..

No commits in common. "main" and "v0.2.2" have entirely different histories.
main ... v0.2.2

25 changed files with 292 additions and 624 deletions

View File

@ -1,15 +0,0 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
groups:
aws-sdk-go:
patterns:
- "github.com/aws/aws-sdk-go-v2"
- "github.com/aws/aws-sdk-go-v2/*"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@ -13,14 +13,14 @@ permissions:
jobs:
golangci:
name: lint
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
- uses: actions/setup-go@v4
with:
go-version: 1.24.2
go-version: 1.22.2
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v3
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: latest

View File

@ -13,12 +13,12 @@ permissions:
jobs:
release:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v4
with:
go-version: 1.24.2
go-version: 1.22.2
- run: ./build-release.sh
# Upload to S3:
- uses: aws-actions/configure-aws-credentials@v4

View File

@ -11,15 +11,15 @@ on:
jobs:
test:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v4
with:
go-version: 1.24.2
go-version: 1.22.2
- name: Build
run: go build -v ./...

View File

@ -10,10 +10,10 @@ on:
jobs:
try-release:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/setup-go@v4
with:
go-version: 1.24.2
go-version: 1.22.2
- run: ./build-release.sh

View File

@ -1,37 +1,19 @@
version: "2"
linters:
default: none
disable-all: true
enable:
- gofmt
- gosec
- gosimple
- govet
- ineffassign
- misspell
- staticcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- wastedassign
settings:
gosec:
excludes:
- G404
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
linters-settings:
errcheck:
ignore: fmt:[FS]?[Pp]rint*,io:Write,os:Remove,net/http:Write,net:Write,encoding/binary:Write

View File

@ -2,57 +2,7 @@
[![Build Status](https://github.com/letsencrypt/crl-monitor/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/letsencrypt/crl-monitor/actions/workflows/test.yml?query=branch%3Amain)
CRL-Monitor monitors CRLs.
It issues certificates, revokes them, and then looks for them to appear in CRLs.
Each component runs as an AWS Lambda.
The `churner` runs periodically. On each run it issues a certificate, fetches and lints
the certificate's CRL URL, revokes the certificate, and stores its serial number and metadata
for the `checker` to later verify that it shows up as revoked. It also checks previously
seen serials. If they haven't shown up in a CRL after a reasonable amount of time, `checker`
produces an error.
The `checker` runs in response to the upload of each new CRL shard in S3. It diffs the newly
uploaded CRL shard against its previous version and verifies:
- New CRL has a later date and higher CRL number than the previous version.
- New CRL passes lints.
- For any serials removed between the old shard and the new one:
- The certificate is expired (based on fetching it by serial from Let's Encrypt).
- For any serials added (if the certificate was issued by the churner):
- The certificate's CRLDistributionPoint matches the CRL shard's IssuingDistributionPoint.
The `checker` also removes from database any certificates it sees, to indicate that their
revocation has been published, so the `churner` won't alert about them
It then marks as completed (deletes) any `churner`-issued certificates that show up on
the new CRL.
## Build and Deployment
This repository has two binaries named `checker` and two binaries named `churner`. The
binaries under `cmd` are for local use and testing. The binaries under `lambda` are for
deployment to AWS Lambda. The key difference is that the `lambda/` binaries register a
lambda handler ([`lambda.StartWithOptions()`]), which AWS then calls. That
[handler can return errors], and we have separate Cloudwatch monitoring that alerts when
any errors are detected.
The lambda binaries are built by a release workflow on GitHub Actions triggered by uploading
a release tag (starting with `v`). Those binaries are uploaded to S3 under a versioned path.
They are then deployed to Lambda using Terraform (in another repository).
[`lambda.StartWithOptions()`]: https://pkg.go.dev/github.com/aws/aws-lambda-go/lambda#StartWithOptions
[handler can return errors]: https://docs.aws.amazon.com/lambda/latest/dg/foundation-progmodel.html
## Testing
Most of the tests are unittests and can be run with:
go test ./...
There is also an integration test for DynamoDB code. To run this, install Java and run:
./db/run_integration_test.sh
CRL-Monitor monitors CRLs
## Architecture Diagram
@ -67,7 +17,6 @@ sequenceDiagram
loop timer
activate churn
churn->>ca: Issue certificate
churn->>ca: Fetch CRL
churn->>ca: Revoke certificate
churn->>ddb: Store certificate metadata
ddb->>churn: Get previous revoked serials
@ -82,10 +31,10 @@ sequenceDiagram
checker->>s3: Read current CRL
checker->>s3: Read previous CRL
Note over checker: Alert if CRL<br />fails linting
loop random selection of serials
loop all removed serials
checker->>ca: Get Certificate
end
Note over checker: Alert if CRL had<br />serials leave early
Note over checker: Alert if CRL had any<br />serials leave early
checker->>ddb: Get revoked serials
checker->>ddb: Delete seen serials
deactivate checker

View File

@ -4,17 +4,16 @@ import (
"context"
"crypto"
"crypto/x509"
"errors"
"fmt"
"log"
"math/big"
"net/http"
"strconv"
"strings"
"time"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/crl/checker"
"github.com/letsencrypt/boulder/crl/idp"
"github.com/letsencrypt/crl-monitor/checker/earlyremoval"
"github.com/letsencrypt/crl-monitor/checker/expiry"
@ -78,6 +77,7 @@ func NewFromEnv(ctx context.Context) (*Checker, error) {
}
baf := expiry.BoulderAPIFetcher{
Client: http.DefaultClient,
BaseURL: boulderBaseURL,
}
@ -113,57 +113,18 @@ type Checker struct {
issuers map[string]*x509.Certificate
}
// crlSummary is a subset of fields from *x509.RevocationList
// useful for logging, plus the number of entries and some metadata.
type crlSummary struct {
Number *big.Int
NumEntries int
ThisUpdate time.Time
NextUpdate time.Time
URL string
StorageKey storage.Key
}
func summary(crl *x509.RevocationList, key storage.Key) crlSummary {
// If getIDP fails, we will just log ""
idp, _ := getIDP(crl)
return crlSummary{
ThisUpdate: crl.ThisUpdate,
NextUpdate: crl.NextUpdate,
Number: crl.Number,
NumEntries: len(crl.RevokedCertificateEntries),
URL: idp,
StorageKey: key,
}
}
type crlsSummary struct {
Old, New crlSummary
}
func logSummary(old *x509.RevocationList, oldStorageKey storage.Key, new *x509.RevocationList, newStorageKey storage.Key) crlsSummary {
return crlsSummary{
Old: summary(old, oldStorageKey),
New: summary(new, newStorageKey),
}
}
// Check fetches a CRL and its previous version. It runs lints on the CRL, checks for early removal, and removes any
// certificates we're waiting for out of the database.
func (c *Checker) Check(ctx context.Context, bucket, object string, startingVersion *string) error {
// Read the current CRL shard
crlDER, version, err := c.storage.Fetch(ctx, storage.Key{
Bucket: bucket,
Object: object,
Version: startingVersion,
})
crlDER, version, err := c.storage.Fetch(ctx, bucket, object, startingVersion)
if err != nil {
return err
}
crl, err := x509.ParseRevocationList(crlDER)
if err != nil {
return fmt.Errorf("parsing current crl: %v", err)
return fmt.Errorf("error parsing current crl: %v", err)
}
log.Printf("loaded CRL number %d (len %d) from %s version %s", crl.Number, len(crl.RevokedCertificateEntries), object, version)
@ -178,41 +139,26 @@ func (c *Checker) Check(ctx context.Context, bucket, object string, startingVers
}
log.Printf("crl %d successfully linted", crl.Number)
_, err = getIDP(crl)
if err != nil {
return err
}
curKey := storage.Key{
Bucket: bucket,
Object: object,
Version: &version,
}
// And the previous:
prevVersion, err := c.storage.Previous(ctx, curKey)
prevVersion, err := c.storage.Previous(ctx, bucket, object, version)
if err != nil {
return err
}
prevKey := curKey
prevKey.Version = &prevVersion
prevDER, _, err := c.storage.Fetch(ctx, prevKey)
prevDER, _, err := c.storage.Fetch(ctx, bucket, object, &prevVersion)
if err != nil {
return err
}
prev, err := x509.ParseRevocationList(prevDER)
if err != nil {
return fmt.Errorf("parsing previous crl: %v", err)
return fmt.Errorf("error parsing previous crl: %v", err)
}
log.Printf("loaded previous CRL number %d (len %d) from version %s", prev.Number, len(prev.RevokedCertificateEntries), prevVersion)
context := logSummary(prev, prevKey, crl, curKey)
earlyRemoved, err := earlyremoval.Check(ctx, c.fetcher, c.maxFetch, prev, crl)
if err != nil {
return fmt.Errorf("checking for early removal: %v. context: %+v", err, context)
return fmt.Errorf("failed to check for early removal: %v", err)
}
if len(earlyRemoved) != 0 {
@ -222,7 +168,7 @@ func (c *Checker) Check(ctx context.Context, bucket, object string, startingVers
}
// Certificates removed early! This is very bad.
return fmt.Errorf("early removal of %d certificates detected! First %d: %v. context: %+v", len(earlyRemoved), len(sample), sample, context)
return fmt.Errorf("early removal of %d certificates detected! First %d: %v", len(earlyRemoved), len(sample), sample)
}
return c.lookForSeenCerts(ctx, crl)
@ -233,31 +179,20 @@ func (c *Checker) Check(ctx context.Context, bucket, object string, startingVers
func (c *Checker) lookForSeenCerts(ctx context.Context, crl *x509.RevocationList) error {
unseenCerts, err := c.db.GetAllCerts(ctx)
if err != nil {
return fmt.Errorf("getting all certs from DB: %v", err)
return fmt.Errorf("failed to read from db: %v", err)
}
var seenSerials [][]byte
var errs []error
for _, seen := range crl.RevokedCertificateEntries {
if metadata, ok := unseenCerts[db.NewCertKey(seen.SerialNumber).SerialString()]; ok {
idp, err := getIDP(crl)
if err != nil {
errs = append(errs, err)
continue
}
if metadata.CRLDistributionPoint != "" && metadata.CRLDistributionPoint != idp {
errs = append(errs, fmt.Errorf("cert %x on CRL %q has non-matching CRLDistributionPoint %q",
seen.SerialNumber, idp, metadata.CRLDistributionPoint))
continue
}
seenSerials = append(seenSerials, metadata.SerialNumber)
}
}
err = c.db.DeleteSerials(ctx, seenSerials)
if err != nil {
errs = append(errs, fmt.Errorf("deleting %d serials from DB: %v", len(seenSerials), err))
return fmt.Errorf("failed to delete from db: %v", err)
}
return errors.Join(errs...)
return nil
}
// issuerForObject takes an s3 object path, extracts the issuer prefix, and returns the right x509.Certificate
@ -274,14 +209,3 @@ func (c *Checker) issuerForObject(object string) (*x509.Certificate, error) {
return issuer, nil
}
func getIDP(crl *x509.RevocationList) (string, error) {
idps, err := idp.GetIDPURIs(crl.Extensions)
if err != nil {
return "", fmt.Errorf("extracting IssuingDistributionPoint URIs: %v", err)
}
if len(idps) == 1 {
return idps[0], nil
}
return "", fmt.Errorf("CRL had incorrect number of IssuingDistributionPoint URIs: %s", idps)
}

View File

@ -31,17 +31,13 @@ func TestCheck(t *testing.T) {
issuerName := nameID(issuer)
shouldBeGood := fmt.Sprintf("%s/should-be-good.crl", issuerName)
earlyRemoval := fmt.Sprintf("%s/early-removal.crl", issuerName)
certificatesHaveCRLDP := fmt.Sprintf("%s/certificates-have-crldp.crl", issuerName)
shouldBeGoodURL := fmt.Sprintf("http://idp/%s", shouldBeGood)
earlyRemovalURL := fmt.Sprintf("http://idp/%s", earlyRemoval)
certificatesHaveCRLDPURL := fmt.Sprintf("http://idp/%s", certificatesHaveCRLDP)
shouldBeGoodIDP := fmt.Sprintf("http://idp/%s", shouldBeGood)
earlyRemovalIDP := fmt.Sprintf("http://idp/%s", earlyRemoval)
crl1der := testdata.MakeCRL(t, &testdata.CRL1, shouldBeGoodURL, issuer, key)
crl2der := testdata.MakeCRL(t, &testdata.CRL2, shouldBeGoodURL, issuer, key)
crl3der := testdata.MakeCRL(t, &testdata.CRL3, earlyRemovalURL, issuer, key)
crl4der := testdata.MakeCRL(t, &testdata.CRL4, earlyRemovalURL, issuer, key)
crl6der := testdata.MakeCRL(t, &testdata.CRL6, certificatesHaveCRLDPURL, issuer, key)
crl7der := testdata.MakeCRL(t, &testdata.CRL7, certificatesHaveCRLDPURL, issuer, key)
crl1der := testdata.MakeCRL(t, &testdata.CRL1, shouldBeGoodIDP, issuer, key)
crl2der := testdata.MakeCRL(t, &testdata.CRL2, shouldBeGoodIDP, issuer, key)
crl3der := testdata.MakeCRL(t, &testdata.CRL3, earlyRemovalIDP, issuer, key)
crl4der := testdata.MakeCRL(t, &testdata.CRL4, earlyRemovalIDP, issuer, key)
data := map[string][]storagemock.MockObject{
shouldBeGood: {
@ -64,16 +60,6 @@ func TestCheck(t *testing.T) {
Data: crl3der,
},
},
certificatesHaveCRLDP: {
{
VersionID: "the-current-version",
Data: crl7der, // CRL6 has serial 4213, which has a CRLDP
},
{
VersionID: "the-previous-version",
Data: crl6der,
},
},
}
bucket := "crl-test"
@ -88,12 +74,11 @@ func TestCheck(t *testing.T) {
ctx := context.Background()
// Insert some serials in the "unseen-certificates" table to be checked.
// Watch the first revoked cert's serial
serial := testdata.CRL1.RevokedCertificateEntries[0].SerialNumber
require.NoError(t, checker.db.AddCert(ctx, &x509.Certificate{SerialNumber: serial}, testdata.Now))
shouldNotBeSeen := big.NewInt(12345)
require.NoError(t, checker.db.AddCert(ctx, &x509.Certificate{SerialNumber: shouldNotBeSeen}, testdata.Now))
mismatchCRLDistributionPoint := big.NewInt(4213)
require.NoError(t, checker.Check(ctx, bucket, shouldBeGood, nil))
@ -107,15 +92,6 @@ func TestCheck(t *testing.T) {
// The "early-removal" object should error on a certificate removed early
require.ErrorContains(t, checker.Check(ctx, bucket, earlyRemoval, nil), "early removal of 1 certificates detected!")
require.NoError(t, checker.db.AddCert(ctx, &x509.Certificate{
SerialNumber: mismatchCRLDistributionPoint,
CRLDistributionPoints: []string{
"http://example.com",
},
}, testdata.Now))
// The "certificates-have-crldp" object should error because the certificate CRL is a mismatch
require.ErrorContains(t, checker.Check(ctx, bucket, certificatesHaveCRLDP, nil), "has non-matching CRLDistributionPoint")
}
func Test_nameID(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"crypto/x509"
"log"
"math/big"
"math/rand/v2"
"math/rand"
"time"
"github.com/letsencrypt/boulder/crl/checker"

View File

@ -4,7 +4,7 @@ import (
"context"
"crypto/x509"
"math/big"
"math/rand/v2"
"math/rand"
"testing"
"time"
@ -68,8 +68,9 @@ func TestSample(t *testing.T) {
require.Empty(t, sample([]int{}, 999))
var data []int
// Generate a random array for tests.
length := 100 + rand.IntN(300)
// Generate a random array for tests. Insecure RNG is fine.
// #nosec G404
length := 100 + rand.Intn(300)
for i := 0; i < length; i++ {
data = append(data, i)
}

View File

@ -5,41 +5,80 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"math/big"
"net/http"
"time"
"github.com/letsencrypt/crl-monitor/retryhttp"
)
type BoulderAPIFetcher struct {
Client *http.Client
BaseURL string
}
func (baf *BoulderAPIFetcher) getBody(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "CRL-Monitor/0.1")
resp, err := baf.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http status %d (%s)", resp.StatusCode, string(body))
}
return body, nil
}
// getWithRetries is a simple wrapper around client.Do that will retry on a fixed backoff schedule
func (baf *BoulderAPIFetcher) getWithRetries(ctx context.Context, url string) ([]byte, error) {
// A fixed sequence of retries. We start with 0 seconds, retrying
// immediately, and increase a few seconds between each retry. The final
// value is zero so that we don't sleep before returning the final error.
var err error
for _, backoff := range []int{0, 1, 1, 2, 3, 0} {
var body []byte
body, err = baf.getBody(ctx, url)
if err == nil {
return body, nil
}
time.Sleep(time.Duration(backoff) * time.Second)
}
return nil, err
}
// FetchNotAfter downloads a certificate, parses it, and returns the NotAfter on
// it. It uses a non-acme path to download a certificate unauthenticated by
// serial. So it is specific to Boulder's API, not a generic ACME API client.
func (baf *BoulderAPIFetcher) FetchNotAfter(ctx context.Context, serial *big.Int) (time.Time, error) {
// The baseURL is followed by a hex-encoded serial
url := fmt.Sprintf("%s/%s", baf.BaseURL, formatSerial(serial))
url := fmt.Sprintf("%s/%036x", baf.BaseURL, serial)
body, err := retryhttp.Get(ctx, url)
body, err := baf.getWithRetries(ctx, url)
if err != nil {
return time.Time{}, fmt.Errorf("fetching NotAfter for serial %s: %w", formatSerial(serial), err)
return time.Time{}, fmt.Errorf("error fetching NotAfter for serial %d: %w", serial, err)
}
block, _ := pem.Decode(body)
if block == nil {
return time.Time{}, fmt.Errorf("parsing PEM for serial %s: %s", formatSerial(serial), string(body))
return time.Time{}, fmt.Errorf("error parsing PEM for serial %d: %s", serial, string(body))
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return time.Time{}, fmt.Errorf("parsing certificate for serial %s: %s", formatSerial(serial), err)
return time.Time{}, fmt.Errorf("error parsing certificate for serial %d: %w", serial, err)
}
return cert.NotAfter, nil
}
func formatSerial(serial *big.Int) string {
return fmt.Sprintf("%036x", serial)
}

View File

@ -123,7 +123,7 @@ func TestBoulderAPIFetcher(t *testing.T) {
res.Write([]byte(testCert))
}))
fetcher := BoulderAPIFetcher{BaseURL: testServer.URL + somePrefix}
fetcher := BoulderAPIFetcher{BaseURL: testServer.URL + somePrefix, Client: http.DefaultClient}
serial := new(big.Int)
serial.SetString(serialhex, 16)

View File

@ -6,6 +6,7 @@ import (
"context"
"fmt"
"math/big"
"net/http"
"testing"
"time"
@ -35,7 +36,7 @@ func TestBoulderAPI(t *testing.T) {
} {
t.Run(tc.subdomain, func(t *testing.T) {
baseURL := fmt.Sprintf("https://%s.api.letsencrypt.org/get/cert", tc.subdomain)
baf := BoulderAPIFetcher{BaseURL: baseURL}
baf := BoulderAPIFetcher{Client: http.DefaultClient, BaseURL: baseURL}
serial := new(big.Int)
serial.SetString(tc.serial, 16)

View File

@ -70,28 +70,6 @@ var CRL5 = x509.RevocationList{
RevokedCertificateEntries: nil,
}
// CRL6 contains serial 4213, which will have a CRLDistributionPoint
// that doesn't match the CRL.
var CRL6 = x509.RevocationList{
ThisUpdate: Now.Add(4 * time.Hour),
NextUpdate: Now.Add(24 * time.Hour),
Number: big.NewInt(1),
RevokedCertificateEntries: []x509.RevocationListEntry{
{SerialNumber: big.NewInt(4213), RevocationTime: Now},
},
}
// CRL7 also contains serial 4213, which will have a CRLDistributionPoint
// that doesn't match the CRL.
var CRL7 = x509.RevocationList{
ThisUpdate: Now.Add(5 * time.Hour),
NextUpdate: Now.Add(25 * time.Hour),
Number: big.NewInt(2),
RevokedCertificateEntries: []x509.RevocationListEntry{
{SerialNumber: big.NewInt(4213), RevocationTime: Now},
},
}
func MakeIssuer(t *testing.T) (*x509.Certificate, crypto.Signer) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)

View File

@ -6,25 +6,20 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"log/slog"
mathrand "math/rand/v2"
"os"
"time"
"github.com/caddyserver/certmagic"
"github.com/libdns/route53"
"github.com/mholt/acmez/v3"
"github.com/mholt/acmez/v3/acme"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
"github.com/letsencrypt/boulder/crl/checker"
"github.com/letsencrypt/crl-monitor/cmd"
"github.com/letsencrypt/crl-monitor/db"
"github.com/letsencrypt/crl-monitor/retryhttp"
)
const (
@ -50,20 +45,21 @@ type Churner struct {
// `baseDomain` should be a domain name that the `dnsProvider` can create/delete
// records for. The certs will be issued from the CA at `acmeDirectory`.
// The resulting serials are stored into `db`
func New(baseDomain string, acmeDirectory string, dnsProvider certmagic.DNSProvider, db *db.Database, cutoff time.Time) (*Churner, error) {
slogger := slog.New(slog.NewTextHandler(os.Stderr, nil))
func New(baseDomain string, acmeDirectory string, dnsProvider certmagic.ACMEDNSProvider, db *db.Database, cutoff time.Time) (*Churner, error) {
zapLogger, err := zap.NewProduction()
if err != nil {
return nil, err
}
acmeClient := acmez.Client{
Client: &acme.Client{
Directory: acmeDirectory,
Logger: slogger,
Logger: zapLogger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeDNS01: &certmagic.DNS01Solver{
DNSManager: certmagic.DNSManager{
DNSProvider: dnsProvider,
PropagationDelay: 60 * time.Second, // Route53 docs say 60 seconds in normal conditions,
},
DNSProvider: dnsProvider,
PropagationDelay: 60 * time.Second, // Route53 docs say 60 seconds in normal conditions
},
},
}
@ -121,17 +117,10 @@ func (c *Churner) RegisterAccount(ctx context.Context) error {
}
func (c *Churner) retryObtain(ctx context.Context, certPrivateKey crypto.Signer, sans []string) ([]acme.Certificate, error) {
csr, err := acmez.NewCSR(certPrivateKey, sans)
if err != nil {
return nil, err
}
params, err := acmez.OrderParametersFromCSR(c.acmeAccount, csr)
if err != nil {
return nil, err
}
var err error
var certificates []acme.Certificate
for retry := 0; retry < 5; retry++ {
certificates, err = c.acmeClient.ObtainCertificate(ctx, params)
certificates, err = c.acmeClient.ObtainCertificate(ctx, c.acmeAccount, certPrivateKey, sans)
if err != nil {
log.Printf("error obtaining certificate on retry %d: %v", retry, err)
time.Sleep(time.Second)
@ -144,7 +133,8 @@ func (c *Churner) retryObtain(ctx context.Context, certPrivateKey crypto.Signer,
// Churn issues a certificate, revokes it, and stores the result in DynamoDB
func (c *Churner) Churn(ctx context.Context) error {
certPrivateKey, err := randomKey()
// Generate either an ecdsa or rsa private key
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
@ -154,47 +144,14 @@ func (c *Churner) Churn(ctx context.Context) error {
return err
}
// certificates contains all the possible cert chains. We don't
// care about alternate chains, but we do care about getting
// the parent of the certificate we just got, so we can validate its CRL.
// certificates contains all the possible cert chains. We only care about
// the cert, so we just take the first one and parse it.
firstChain := certificates[0].ChainPEM
block, remaining := pem.Decode(firstChain)
block, _ := pem.Decode(firstChain)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
block, _ = pem.Decode(remaining)
issuer, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return err
}
// If the certificate has any CRLDistributionPoints, check that they can be fetched,
// parsed, verified, and linted. We don't try to check for revocation at this stage
// because it may be several hours before a new CRL is uploaded that reflects the
// revocation we're about to do. Contrariwise, we check for non-revocation, since
// we're fetching the CRL before revoking.
for _, url := range cert.CRLDistributionPoints {
body, err := retryhttp.Get(ctx, url)
if err != nil {
return fmt.Errorf("fetching CRL %q from CRLDistributionPoint of certificate %036x: %s",
url, cert.SerialNumber, err)
}
crl, err := x509.ParseRevocationList(body)
if err != nil {
return fmt.Errorf("fetching CRL %q from CRLDistributionPoint of certificate %036x: %s",
url, cert.SerialNumber, err)
}
err = checker.Validate(crl, issuer, 24*time.Hour)
if err != nil {
return err
}
for _, entry := range crl.RevokedCertificateEntries {
if entry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
return fmt.Errorf("certificate %x was found on CRL %s before it was revoked", cert.SerialNumber, url)
}
}
}
err = c.acmeClient.RevokeCertificate(ctx, c.acmeAccount, cert, c.acmeAccount.PrivateKey, acme.ReasonCessationOfOperation)
if err != nil {
@ -204,19 +161,16 @@ func (c *Churner) Churn(ctx context.Context) error {
return c.db.AddCert(ctx, cert, time.Now())
}
// randomKey generates either an ecdsa or rsa private key
func randomKey() (crypto.Signer, error) {
if mathrand.IntN(2) == 0 {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
} else {
return rsa.GenerateKey(rand.Reader, 2048)
}
}
// randDomains picks the domains to include on the certificate.
// We put a single domain which includes the current time and a random value.
func randDomains(baseDomain string) []string {
domain := fmt.Sprintf("r%dz%x.%s", time.Now().Unix(), mathrand.Uint32(), baseDomain)
randomSuffix := make([]byte, 2)
_, err := rand.Read(randomSuffix)
if err != nil {
// Something has to go terribly wrong for this
panic(fmt.Sprintf("random read failed: %v", err))
}
domain := fmt.Sprintf("r%dz%x.%s", time.Now().Unix(), randomSuffix, baseDomain)
return []string{domain}
}

View File

@ -18,7 +18,7 @@ func TestRandDomains(t *testing.T) {
base := "revoked.invalid"
domains := randDomains(base)
require.Len(t, domains, 1)
require.Regexp(t, regexp.MustCompile(`r[0-9]{10}z[0-9a-f]+\.`+regexp.QuoteMeta(base)), domains[0])
require.Regexp(t, regexp.MustCompile(`r[0-9]{10}z[0-9a-f]{4}\.`+regexp.QuoteMeta(base)), domains[0])
second := randDomains(base)
require.NotEqual(t, domains, second, "Domains should be different each invocation")

View File

@ -47,8 +47,7 @@ func New(ctx context.Context, table, dynamoEndpoint string) (*Database, error) {
// That is the CertKey plus the revocation time today.
type CertMetadata struct {
CertKey
RevocationTime time.Time `dynamodbav:"RT,unixtime"`
CRLDistributionPoint string `dynamodbav:"DP,string,omitempty"`
RevocationTime time.Time `dynamodbav:"RT,unixtime"`
}
// CertKey is the DynamoDB primary key, which is the serial number.
@ -68,19 +67,9 @@ func (ck CertKey) SerialString() string {
// AddCert inserts the metadata for monitoring
func (db *Database) AddCert(ctx context.Context, certificate *x509.Certificate, revocationTime time.Time) error {
var crlDistributionPoint string
// TODO: Once all issued certificates have a CRLDistributionPoint, error out when
// the extension is absent.
if len(certificate.CRLDistributionPoints) > 0 {
crlDistributionPoint = certificate.CRLDistributionPoints[0]
}
if len(certificate.CRLDistributionPoints) > 1 {
return fmt.Errorf("too many CRLDistributionPoints in certificate: %d", len(certificate.CRLDistributionPoints))
}
item, err := attributevalue.MarshalMap(CertMetadata{
CertKey: NewCertKey(certificate.SerialNumber),
RevocationTime: revocationTime,
CRLDistributionPoint: crlDistributionPoint,
CertKey: NewCertKey(certificate.SerialNumber),
RevocationTime: revocationTime,
})
if err != nil {
return err
@ -120,7 +109,7 @@ func (db *Database) GetAllCerts(ctx context.Context) (map[string]CertMetadata, e
certs := make(map[string]CertMetadata, len(certList))
for _, cert := range certList {
certs[cert.SerialString()] = cert
certs[cert.CertKey.SerialString()] = cert
}
return certs, nil
}
@ -152,3 +141,16 @@ func (db *Database) DeleteSerials(ctx context.Context, serialNumbers [][]byte) e
}
return nil
}
// StaticResolver is used in test and dev to use the local dynamodb
func StaticResolver(url string) func(service, region string, opts ...interface{}) (aws.Endpoint, error) {
return func(service, region string, opts ...interface{}) (aws.Endpoint, error) {
if service != dynamodb.ServiceID {
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
}
return aws.Endpoint{
PartitionID: "aws",
URL: url,
}, nil
}
}

View File

@ -4,7 +4,6 @@ import (
"bytes"
"context"
"crypto/x509"
"fmt"
"math/big"
"testing"
"time"
@ -70,57 +69,3 @@ func smoketest(t *testing.T, handle *db.Database) {
}
require.Equal(t, expected, remaining)
}
func TestAddCertCRLDP(t *testing.T) {
handle := mock.NewMockedDB(t)
ctx := context.Background()
revocationTime := time.Now().Add(100 * time.Hour)
int111 := big.NewInt(111)
int4s := big.NewInt(444444)
int60s := big.NewInt(606060)
err := handle.AddCert(ctx, &x509.Certificate{
SerialNumber: int111,
}, revocationTime)
if err != nil {
t.Errorf("inserting plain cert: %s", err)
}
err = handle.AddCert(ctx, &x509.Certificate{
SerialNumber: int4s,
CRLDistributionPoints: []string{
"http://example.com/crl",
"http://example.net/crl",
},
}, revocationTime)
if err == nil {
t.Errorf("inserting cert with two CRLDistributionPoints: got success, want error")
}
err = handle.AddCert(ctx, &x509.Certificate{
SerialNumber: int60s,
CRLDistributionPoints: []string{
"http://example.com/crl",
},
}, revocationTime)
if err != nil {
t.Errorf("inserting cert with one CRLDistributionPoint: %s", err)
}
results, err := handle.GetAllCerts(ctx)
if err != nil {
t.Fatalf("getting all certs: %s", err)
}
serialString := fmt.Sprintf("%036x", int60s)
metadata, ok := results[serialString]
if !ok {
t.Errorf("getting all certs: expected entry for %s, got %+v", serialString, metadata)
}
if metadata.CRLDistributionPoint != "http://example.com/crl" {
t.Errorf("CRL for %s = %q, want %q", serialString, metadata.CRLDistributionPoint, "http://example.com/crl")
}
}

View File

@ -19,10 +19,11 @@ import (
// run most tests outside the db package.
func TestIntegrationDynamoDB(t *testing.T) {
cfg := aws.NewConfig()
cfg.EndpointResolverWithOptions = aws.EndpointResolverWithOptionsFunc(db.StaticResolver("http://localhost:8000"))
cfg.Credentials = aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) {
return aws.Credentials{AccessKeyID: "Bogus", SecretAccessKey: "Bogus"}, nil
})
handle, err := db.New(context.Background(), "unseen-certificates", "http://localhost:8000")
handle, err := db.New("unseen-certificates", cfg)
require.NoError(t, err)
smoketest(t, handle)

85
go.mod
View File

@ -1,66 +1,63 @@
module github.com/letsencrypt/crl-monitor
go 1.23.0
go 1.22.0
toolchain go1.24.2
toolchain go1.22.2
require (
github.com/aws/aws-lambda-go v1.49.0
github.com/aws/aws-sdk-go-v2 v1.36.4
github.com/aws/aws-sdk-go-v2/config v1.29.16
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.2
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.2
github.com/caddyserver/certmagic v0.21.7
github.com/aws/aws-lambda-go v1.46.0
github.com/aws/aws-sdk-go-v2 v1.26.1
github.com/aws/aws-sdk-go-v2/config v1.27.10
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.13.13
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.1
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1
github.com/caddyserver/certmagic v0.20.0
github.com/letsencrypt/boulder v0.0.0-20240424004736-7ee5b469a6a9
github.com/libdns/route53 v1.5.1
github.com/mholt/acmez/v3 v3.1.2
github.com/stretchr/testify v1.10.0
github.com/libdns/route53 v1.3.3
github.com/mholt/acmez v1.2.0
github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.69 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.20.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.40.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/weppos/publicsuffix-go v0.30.3-0.20240411085455-21202160c2ed // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c // indirect
github.com/zmap/zlint/v3 v3.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.22.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

174
go.sum
View File

@ -1,60 +1,71 @@
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/aws/aws-lambda-go v1.49.0 h1:z4VhTqkFZPM3xpEtTqWqRqsRH4TZBMJqTkRiBPYLqIQ=
github.com/aws/aws-lambda-go v1.49.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E=
github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A=
github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178=
github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.2 h1:Nl1i1+ZtpafH5DHr4LYpAgPwvWjDc3bfPlcZpLw3ffQ=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.2/go.mod h1:P9puVqIaBsnqbUcfDOIk0dsKaa7jckuRxwBbg6NzF9Y=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.35 h1:th/m+Q18CkajTw1iqx2cKkLCij/uz8NMwJFPK91p2ug=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.35/go.mod h1:dkJuf0a1Bc8HAA0Zm2MoTGm/WDC18Td9vSbrQ1+VqE8=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.3 h1:2FCJAT5wyPs5JjAFoLgaEB0MIiWvXiJ0T6PZiKDkJoo=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.3/go.mod h1:rUOhTo9+gtTYTMnGD+xiiks/2Z8vssPP+uSMNhJBbmI=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.5 h1:JSQ8/BuqZHaeE/kVgimmjHZ27wTKjYHujo6Oo6M1Iv4=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.5/go.mod h1:4iQhABsZl371BGh/fJq/qJcHzxoNX3kHTmhOXQWYhjU=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.3 h1:VHPZakq2L7w+RLzV54LmQavbvheFaR2u1NomJRSEfcU=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.3/go.mod h1:DX1e/lkbsAt0MkY3NgLYuH4jQvRfw8MYxTe9feR7aXM=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.16 h1:TLsOzHW9zlJoMgjcKQI/7bolyv/DL0796y4NigWgaw8=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.16/go.mod h1:mNoiR5qsO9TxXZ6psjjQ3M+Zz7hURFTumXHF+UKjyAU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.16 h1:2HuI7vWKhFWsBhIr2Zq8KfFZT6xqaId2XXnXZjkbEuc=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.16/go.mod h1:BrwWnsfbFtFeRjdx0iM1ymvlqDX1Oz68JsQaibX/wG8=
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 h1:MmLCRqP4U4Cw9gJ4bNrCG0mWqEtBlmAVleyelcHARMU=
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3/go.mod h1:AMPjK2YnRh0YgOID3PqhJA1BRNfXDfGOnSsKHtAe8yA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.2 h1:T6Wu+8E2LeTUqzqQ/Bh1EoFNj1u4jUyveMgmTlu9fDU=
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.2/go.mod h1:chSY8zfqmS0OnhZoO/hpPx/BHfAIL80m77HwhRLYScY=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/aws-lambda-go v1.46.0 h1:UWVnvh2h2gecOlFhHQfIPQcD8pL/f7pVCutmFl+oXU8=
github.com/aws/aws-lambda-go v1.46.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
github.com/aws/aws-sdk-go-v2 v1.17.8/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
github.com/aws/aws-sdk-go-v2/config v1.18.21/go.mod h1:+jPQiVPz1diRnjj6VGqWcLK6EzNmQ42l7J3OqGTLsSY=
github.com/aws/aws-sdk-go-v2/config v1.27.10 h1:PS+65jThT0T/snC5WjyfHHyUgG+eBoupSDV+f838cro=
github.com/aws/aws-sdk-go-v2/config v1.27.10/go.mod h1:BePM7Vo4OBpHreKRUMuDXX+/+JWP38FLkzl5m27/Jjs=
github.com/aws/aws-sdk-go-v2/credentials v1.13.20/go.mod h1:xtZnXErtbZ8YGXC3+8WfajpMBn5Ga/3ojZdxHq6iI8o=
github.com/aws/aws-sdk-go-v2/credentials v1.17.10 h1:qDZ3EA2lv1KangvQB6y258OssCHD0xvaGiEDkG4X/10=
github.com/aws/aws-sdk-go-v2/credentials v1.17.10/go.mod h1:6t3sucOaYDwDssHQa0ojH1RpmVmF5/jArkye1b2FKMI=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.13.13 h1:loQ4VSt3hTm9n8ST9jveArwmhqAc5aiRJXlxLPxCNTw=
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.13.13/go.mod h1:RjdeQvzJuUf9jWj+ta+7l3VnVpDZ+RmtP/p+QdwRIpI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.2/go.mod h1:cDh1p6XkSGSwSRIArWRc6+UqAQ7x4alQ0QfpVR6f+co=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.32/go.mod h1:RudqOgadTWdcS3t/erPQo24pcVEoYyqj/kKW5Vya21I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.26/go.mod h1:vq86l7956VgFr0/FWQ2BWnK07QC3WYsepKzy33qqY5U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.33/go.mod h1:zG2FcwjQarWaqXSCGpgcr3RSjZ6dHGguZSppUL0XR7Q=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.1 h1:dZXY07Dm59TxAjJcUfNMJHLDI/gLMxTRZefn2jFAVsw=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.31.1/go.mod h1:lVLqEtX+ezgtfalyJs7Peb0uv9dEpAQP5yuq2O26R44=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.20.4 h1:hSwDD19/e01z3pfyx+hDeX5T/0Sn+ZEnnTO5pVWKWx8=
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.20.4/go.mod h1:61CuGwE7jYn0g2gl7K3qoT4vCY59ZQEixkPu8PN5IrE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.6 h1:6tayEze2Y+hiL3kdnEUxSPsP+pJsUfwLSFspFl1ru9Q=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.6/go.mod h1:qVNb/9IOVsLCZh0x2lnagrBwQ9fxajUpXS7OZfIsKn0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.26/go.mod h1:Bd4C/4PkVGubtNe5iMXu5BNnaBi/9t/UsFspPt4ram8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
github.com/aws/aws-sdk-go-v2/service/route53 v1.27.7/go.mod h1:Jhu94omkrksnqX6Xs4Qo10eA1Fx+2NYKjZMU4GvZLp0=
github.com/aws/aws-sdk-go-v2/service/route53 v1.40.4 h1:ZZKiHm4cN8IDDZ2kh8DTk+YnYBjVsiFdwf5FwVs//IQ=
github.com/aws/aws-sdk-go-v2/service/route53 v1.40.4/go.mod h1:RTfjFUctf+Zyq8e4rgLXmz43+0kIoIXbENvrFtilumI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.8/go.mod h1:GNIveDnP+aE3jujyUSH5aZ/rktsTM5EvtKnCqBZawdw=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.4 h1:WzFol5Cd+yDxPAdnzTA5LmpHYSWinhmSj4rQChV0ee8=
github.com/aws/aws-sdk-go-v2/service/sso v1.20.4/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.8/go.mod h1:44qFP1g7pfd+U+sQHLPalAPKnyfTZjJsYR4xIwsJy5o=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.9/go.mod h1:yyW88BEPXA2fGFyI2KCcZC3dNpiT0CZAHaF+i656/tQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU=
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
@ -62,8 +73,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -74,9 +85,8 @@ github.com/google/certificate-transparency-go v1.1.6/go.mod h1:0OJjOsOk+wj6aYQgP
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -85,8 +95,9 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -100,16 +111,17 @@ github.com/letsencrypt/boulder v0.0.0-20240424004736-7ee5b469a6a9 h1:1WzTMHK3XWz
github.com/letsencrypt/boulder v0.0.0-20240424004736-7ee5b469a6a9/go.mod h1:CtAtcgvLkR5K75e/MrIvIlMnCiC4/+Wu2U8ipjUDaSQ=
github.com/letsencrypt/pkcs11key/v4 v4.0.0 h1:qLc/OznH7xMr5ARJgkZCCWk+EomQkiNTOoOF5LAgagc=
github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/route53 v1.5.1 h1:dkdcc2CKY/EHBBzAKqE0Cko7MKR8uVJ3GvpzwKu/UKM=
github.com/libdns/route53 v1.5.1/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=
github.com/libdns/route53 v1.3.3 h1:16sTxbbRGm0zODz0p0aVHHIyTqtHzEn3j0s4dGzQvNI=
github.com/libdns/route53 v1.3.3/go.mod h1:n1Xy55lpfdxMIx4CVWAM16GQac+/OZcnm1xBjMyhZAo=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
@ -137,8 +149,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ=
github.com/weppos/publicsuffix-go v0.30.3-0.20240411085455-21202160c2ed h1:2SVbw+/Q/si5J66tuM12841RSkH/3Q4TQpiUkUctBgY=
@ -146,8 +158,8 @@ github.com/weppos/publicsuffix-go v0.30.3-0.20240411085455-21202160c2ed/go.mod h
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
@ -167,8 +179,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -179,13 +189,12 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -198,15 +207,14 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -225,9 +233,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -247,15 +254,14 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=

View File

@ -1,50 +0,0 @@
package retryhttp
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func getBody(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "CRL-Monitor/0.1")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http status %d (%s)", resp.StatusCode, string(body))
}
return body, nil
}
// Get is a simple wrapper around http.Client.Do that will retry on a fixed backoff schedule
func Get(ctx context.Context, url string) ([]byte, error) {
// A fixed exponential backoff schedule. The final value is zero so that we don't sleep before
// returning the final error.
var err error
for _, backoff := range []int{1000, 1250, 1562, 1953, 2441, 3051, 3814, 4768, 5960, 7450, 9313, 11641, 0} {
var body []byte
body, err = getBody(ctx, url)
if err == nil {
return body, nil
}
time.Sleep(time.Duration(backoff) * time.Millisecond)
}
return nil, err
}

View File

@ -20,12 +20,6 @@ type Storage struct {
S3Client s3client
}
// The parameters used to fetch a unique item from storage.
type Key struct {
Bucket, Object string
Version *string
}
func New(ctx context.Context) *Storage {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
@ -40,32 +34,29 @@ func New(ctx context.Context) *Storage {
// The bucket and object names are required.
// If version is nil, the current version is returned.
// Returns the retrieved DER CRL bytes and what VersionID it was.
func (s *Storage) Fetch(ctx context.Context, key Key) ([]byte, string, error) {
func (s *Storage) Fetch(ctx context.Context, bucket, object string, version *string) ([]byte, string, error) {
resp, err := s.S3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &key.Bucket,
Key: &key.Object,
VersionId: key.Version,
Bucket: &bucket,
Key: &object,
VersionId: version,
})
if err != nil {
return nil, "", fmt.Errorf("retrieving CRL %s %s version %v: %w", key.Bucket, key.Object, key.Version, err)
return nil, "", fmt.Errorf("error retrieving CRL %s %s version %v: %w", bucket, object, version, err)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", fmt.Errorf("reading CRL %s %s version %v: %w", key.Bucket, key.Object, key.Version, err)
return nil, "", fmt.Errorf("error reading CRL %s %s version %v: %w", bucket, object, version, err)
}
return body, *resp.VersionId, err
}
// Previous returns the previous version of a CRL shard, which can then be fetched.
func (s *Storage) Previous(ctx context.Context, key Key) (string, error) {
if key.Version == nil {
return "", fmt.Errorf("Previous called with no Version")
}
func (s *Storage) Previous(ctx context.Context, bucket, object, version string) (string, error) {
resp, err := s.S3Client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
Bucket: &key.Bucket,
Prefix: &key.Object,
Bucket: &bucket,
Prefix: &object,
})
if err != nil {
return "", err
@ -79,14 +70,14 @@ func (s *Storage) Previous(ctx context.Context, key Key) (string, error) {
break
}
if v.VersionId != nil && *v.VersionId == *key.Version {
if v.VersionId != nil && *v.VersionId == version {
// This is the version of interest; select the next one
found = true
}
}
if (!found || prevVersion == nil) && resp.IsTruncated != nil && *resp.IsTruncated {
return "", fmt.Errorf("too many versions and pagination not implemented! %+v", key)
return "", fmt.Errorf("too many versions and pagination not implemented! %s %s %s", bucket, object, version)
}
if !found {

View File

@ -7,12 +7,11 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/stretchr/testify/require"
"github.com/letsencrypt/crl-monitor/storage"
"github.com/letsencrypt/crl-monitor/storage/mock"
)
func TestStorage(t *testing.T) {
mockStorage := mock.New(t, "somebucket", map[string][]mock.MockObject{
storage := mock.New(t, "somebucket", map[string][]mock.MockObject{
"123/0.crl": {
{VersionID: "111", Data: []byte{0xaa, 0xbb}},
{VersionID: "222", Data: []byte{0xcc, 0xdd}},
@ -57,11 +56,7 @@ func TestStorage(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
crl, version, err := mockStorage.Fetch(context.Background(), storage.Key{
Bucket: "somebucket",
Object: tt.object,
Version: tt.version,
})
crl, version, err := storage.Fetch(context.Background(), "somebucket", tt.object, tt.version)
require.NoError(t, err)
require.Equal(t, tt.expectedVer, version)
require.Equal(t, tt.expectedCRL, crl)
@ -92,11 +87,7 @@ func TestStorage(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
version, err := mockStorage.Previous(context.Background(), storage.Key{
Bucket: "somebucket",
Object: tt.object,
Version: &tt.version,
})
version, err := storage.Previous(context.Background(), "somebucket", tt.object, tt.version)
require.NoError(t, err)
require.Equal(t, tt.expectedVer, version)
})
@ -122,11 +113,7 @@ func TestStorage(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
version, err := mockStorage.Previous(context.Background(), storage.Key{
Bucket: "somebucket",
Object: tt.object,
Version: &tt.version,
})
version, err := storage.Previous(context.Background(), "somebucket", tt.object, tt.version)
require.Error(t, err)
require.Equal(t, "", version)
})