Observer: detect CRL IDP mismatch (#8067)

Give boulder-observer the ability to detect if the CRL it fetches is the
CRL it expects, by comparing that CRLs issuingDistributionPoint
extension to the prober's configured URL. Only do this if instructed to
(by configuring the CRL prober as "partitioned") because non-partitioned
CRLs do not necessarily contain an IDP.

Fixes https://github.com/letsencrypt/boulder/issues/7527
This commit is contained in:
Aaron Gable 2025-03-14 16:52:29 -05:00 committed by GitHub
parent ebf232cccb
commit d045b387ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 16 deletions

View File

@ -4,15 +4,19 @@ import (
"crypto/x509"
"io"
"net/http"
"slices"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/crl/idp"
)
// CRLProbe is the exported 'Prober' object for monitors configured to
// monitor CRL availability & characteristics.
type CRLProbe struct {
url string
partitioned bool
cNextUpdate *prometheus.GaugeVec
cThisUpdate *prometheus.GaugeVec
cCertCount *prometheus.GaugeVec
@ -47,6 +51,19 @@ func (p CRLProbe) Probe(timeout time.Duration) (bool, time.Duration) {
return false, dur
}
// Partitioned CRLs MUST contain an issuingDistributionPoint extension, which
// MUST contain the URL from which they were fetched, to prevent substitution
// attacks.
if p.partitioned {
idps, err := idp.GetIDPURIs(crl.Extensions)
if err != nil {
return false, dur
}
if len(idps) != 0 && !slices.Contains(idps, p.url) {
return false, dur
}
}
// Report metrics for this CRL
p.cThisUpdate.WithLabelValues(p.url).Set(float64(crl.ThisUpdate.Unix()))
p.cNextUpdate.WithLabelValues(p.url).Set(float64(crl.NextUpdate.Unix()))

View File

@ -4,9 +4,10 @@ import (
"fmt"
"net/url"
"github.com/prometheus/client_golang/prometheus"
"github.com/letsencrypt/boulder/observer/probers"
"github.com/letsencrypt/boulder/strictyaml"
"github.com/prometheus/client_golang/prometheus"
)
const (
@ -17,7 +18,8 @@ const (
// CRLConf is exported to receive YAML configuration
type CRLConf struct {
URL string `yaml:"url"`
URL string `yaml:"url"`
Partitioned bool `yaml:"partitioned"`
}
// Kind returns a name that uniquely identifies the `Kind` of `Configurer`.
@ -87,7 +89,7 @@ func (c CRLConf) MakeProber(collectors map[string]prometheus.Collector) (probers
return nil, fmt.Errorf("crl prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", certCountName, coll)
}
return CRLProbe{c.URL, nextUpdateColl, thisUpdateColl, certCountColl}, nil
return CRLProbe{c.URL, c.Partitioned, nextUpdateColl, thisUpdateColl, certCountColl}, nil
}
// Instrument constructs any `prometheus.Collector` objects the `CRLProbe` will

View File

@ -3,10 +3,11 @@ package probers
import (
"testing"
"github.com/letsencrypt/boulder/observer/probers"
"github.com/letsencrypt/boulder/test"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/yaml.v3"
"github.com/letsencrypt/boulder/observer/probers"
"github.com/letsencrypt/boulder/test"
)
func TestCRLConf_MakeProber(t *testing.T) {
@ -70,25 +71,20 @@ func TestCRLConf_MakeProber(t *testing.T) {
}
func TestCRLConf_UnmarshalSettings(t *testing.T) {
type fields struct {
url interface{}
}
tests := []struct {
name string
fields fields
fields probers.Settings
want probers.Configurer
wantErr bool
}{
{"valid", fields{"google.com"}, CRLConf{"google.com"}, false},
{"invalid (map)", fields{make(map[string]interface{})}, nil, true},
{"invalid (list)", fields{make([]string, 0)}, nil, true},
{"valid", probers.Settings{"url": "google.com"}, CRLConf{"google.com", false}, false},
{"valid with partitioned", probers.Settings{"url": "google.com", "partitioned": true}, CRLConf{"google.com", true}, false},
{"invalid (map)", probers.Settings{"url": make(map[string]interface{})}, nil, true},
{"invalid (list)", probers.Settings{"url": make([]string, 0)}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
settings := probers.Settings{
"url": tt.fields.url,
}
settingsBytes, _ := yaml.Marshal(settings)
settingsBytes, _ := yaml.Marshal(tt.fields)
t.Log(string(settingsBytes))
c := CRLConf{}
got, err := c.UnmarshalSettings(settingsBytes)