Skip to content

Commit d045b38

Browse files
authored
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 #7527
1 parent ebf232c commit d045b38

File tree

3 files changed

+31
-16
lines changed

3 files changed

+31
-16
lines changed

observer/probers/crl/crl.go

+17
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import (
44
"crypto/x509"
55
"io"
66
"net/http"
7+
"slices"
78
"time"
89

910
"github.com/prometheus/client_golang/prometheus"
11+
12+
"github.com/letsencrypt/boulder/crl/idp"
1013
)
1114

1215
// CRLProbe is the exported 'Prober' object for monitors configured to
1316
// monitor CRL availability & characteristics.
1417
type CRLProbe struct {
1518
url string
19+
partitioned bool
1620
cNextUpdate *prometheus.GaugeVec
1721
cThisUpdate *prometheus.GaugeVec
1822
cCertCount *prometheus.GaugeVec
@@ -47,6 +51,19 @@ func (p CRLProbe) Probe(timeout time.Duration) (bool, time.Duration) {
4751
return false, dur
4852
}
4953

54+
// Partitioned CRLs MUST contain an issuingDistributionPoint extension, which
55+
// MUST contain the URL from which they were fetched, to prevent substitution
56+
// attacks.
57+
if p.partitioned {
58+
idps, err := idp.GetIDPURIs(crl.Extensions)
59+
if err != nil {
60+
return false, dur
61+
}
62+
if len(idps) != 0 && !slices.Contains(idps, p.url) {
63+
return false, dur
64+
}
65+
}
66+
5067
// Report metrics for this CRL
5168
p.cThisUpdate.WithLabelValues(p.url).Set(float64(crl.ThisUpdate.Unix()))
5269
p.cNextUpdate.WithLabelValues(p.url).Set(float64(crl.NextUpdate.Unix()))

observer/probers/crl/crl_conf.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import (
44
"fmt"
55
"net/url"
66

7+
"github.com/prometheus/client_golang/prometheus"
8+
79
"github.com/letsencrypt/boulder/observer/probers"
810
"github.com/letsencrypt/boulder/strictyaml"
9-
"github.com/prometheus/client_golang/prometheus"
1011
)
1112

1213
const (
@@ -17,7 +18,8 @@ const (
1718

1819
// CRLConf is exported to receive YAML configuration
1920
type CRLConf struct {
20-
URL string `yaml:"url"`
21+
URL string `yaml:"url"`
22+
Partitioned bool `yaml:"partitioned"`
2123
}
2224

2325
// 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
8789
return nil, fmt.Errorf("crl prober received collector %q of wrong type, got: %T, expected *prometheus.GaugeVec", certCountName, coll)
8890
}
8991

90-
return CRLProbe{c.URL, nextUpdateColl, thisUpdateColl, certCountColl}, nil
92+
return CRLProbe{c.URL, c.Partitioned, nextUpdateColl, thisUpdateColl, certCountColl}, nil
9193
}
9294

9395
// Instrument constructs any `prometheus.Collector` objects the `CRLProbe` will

observer/probers/crl/crl_conf_test.go

+9-13
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package probers
33
import (
44
"testing"
55

6-
"github.com/letsencrypt/boulder/observer/probers"
7-
"github.com/letsencrypt/boulder/test"
86
"github.com/prometheus/client_golang/prometheus"
97
"gopkg.in/yaml.v3"
8+
9+
"github.com/letsencrypt/boulder/observer/probers"
10+
"github.com/letsencrypt/boulder/test"
1011
)
1112

1213
func TestCRLConf_MakeProber(t *testing.T) {
@@ -70,25 +71,20 @@ func TestCRLConf_MakeProber(t *testing.T) {
7071
}
7172

7273
func TestCRLConf_UnmarshalSettings(t *testing.T) {
73-
type fields struct {
74-
url interface{}
75-
}
7674
tests := []struct {
7775
name string
78-
fields fields
76+
fields probers.Settings
7977
want probers.Configurer
8078
wantErr bool
8179
}{
82-
{"valid", fields{"google.com"}, CRLConf{"google.com"}, false},
83-
{"invalid (map)", fields{make(map[string]interface{})}, nil, true},
84-
{"invalid (list)", fields{make([]string, 0)}, nil, true},
80+
{"valid", probers.Settings{"url": "google.com"}, CRLConf{"google.com", false}, false},
81+
{"valid with partitioned", probers.Settings{"url": "google.com", "partitioned": true}, CRLConf{"google.com", true}, false},
82+
{"invalid (map)", probers.Settings{"url": make(map[string]interface{})}, nil, true},
83+
{"invalid (list)", probers.Settings{"url": make([]string, 0)}, nil, true},
8584
}
8685
for _, tt := range tests {
8786
t.Run(tt.name, func(t *testing.T) {
88-
settings := probers.Settings{
89-
"url": tt.fields.url,
90-
}
91-
settingsBytes, _ := yaml.Marshal(settings)
87+
settingsBytes, _ := yaml.Marshal(tt.fields)
9288
t.Log(string(settingsBytes))
9389
c := CRLConf{}
9490
got, err := c.UnmarshalSettings(settingsBytes)

0 commit comments

Comments
 (0)