Skip to content

Commit f4b9543

Browse files
committed
identify outbound clients with a contact User-Agent
c2sp.org/tlog-tiles: clients SHOULD include a way for the operator to contact them in the User-Agent (an email and/or a +https:// URL), and logs MAY rate-limit anonymous or unreachable clients. Skylight enforces this — anonymous entry-bundle reads get HTTP 429. Stamp a contact UA on everything in the repo that talks to a log or mirror: the cactus server's mirror_push client and sign-subtree quorum requests ("cactus (+https://github.com/mcpherrinm/cactus)") and cactus-cli's reads. cactus-pollinate already ships with its own. The integration stub witness now records the User-Agent it sees so the header cannot silently regress.
1 parent 5202b6e commit f4b9543

5 files changed

Lines changed: 46 additions & 2 deletions

File tree

cert/cosigner_request.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ func requestOne(ctx context.Context, m MirrorEndpoint, body []byte, subtree *MTC
208208
return MTCSignature{}, err
209209
}
210210
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
211+
// c2sp.org/tlog-tiles: clients SHOULD carry an operator contact in
212+
// the User-Agent; mirrors MAY rate-limit anonymous clients.
213+
req.Header.Set("User-Agent", "cactus (+https://github.com/mcpherrinm/cactus)")
211214
resp, err := http.DefaultClient.Do(req)
212215
if err != nil {
213216
return MTCSignature{}, err

cmd/cactus-cli/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,14 @@ func certVerify(certPath, logURL string) {
404404

405405
// httpGet fetches url and returns the body; treats 4xx/5xx as errors.
406406
func httpGet(url string) ([]byte, error) {
407-
resp, err := http.Get(url)
407+
req, err := http.NewRequest(http.MethodGet, url, nil)
408+
if err != nil {
409+
return nil, err
410+
}
411+
// c2sp.org/tlog-tiles: clients SHOULD carry a contact in the
412+
// User-Agent; logs MAY rate-limit anonymous clients.
413+
req.Header.Set("User-Agent", "cactus-cli (+https://github.com/mcpherrinm/cactus)")
414+
resp, err := http.DefaultClient.Do(req)
408415
if err != nil {
409416
return nil, err
410417
}

cmd/cactus/main.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ import (
5151
"github.com/letsencrypt/cactus/tlogx"
5252
)
5353

54+
// serverUserAgent identifies the cactus server's outbound mirror
55+
// traffic. c2sp.org/tlog-tiles: clients SHOULD include a way for the
56+
// operator to contact them in the User-Agent, and logs MAY rate-limit
57+
// anonymous or unreachable clients.
58+
const serverUserAgent = "cactus (+https://github.com/mcpherrinm/cactus)"
59+
60+
// uaTransport stamps serverUserAgent onto outgoing requests that don't
61+
// already carry a User-Agent.
62+
type uaTransport struct{ base http.RoundTripper }
63+
64+
func (t uaTransport) RoundTrip(req *http.Request) (*http.Response, error) {
65+
if req.Header.Get("User-Agent") == "" {
66+
req = req.Clone(req.Context())
67+
req.Header.Set("User-Agent", serverUserAgent)
68+
}
69+
return t.base.RoundTrip(req)
70+
}
71+
5472
// caMirrorRequestsAdapter adapts a *prometheus.CounterVec to the
5573
// cert.CounterVec interface.
5674
type caMirrorRequestsAdapter struct {
@@ -120,7 +138,10 @@ func buildPushClients(
120138
fsys storage.FS,
121139
logger *slog.Logger,
122140
) ([]*mirrorpush.Client, error) {
123-
httpClient := &http.Client{Timeout: cfg.MirrorPush.RequestTimeout()}
141+
httpClient := &http.Client{
142+
Timeout: cfg.MirrorPush.RequestTimeout(),
143+
Transport: uaTransport{http.DefaultTransport},
144+
}
124145
out := make([]*mirrorpush.Client, 0, len(cfg.MirrorPush.Targets))
125146
for i, t := range cfg.MirrorPush.Targets {
126147
alg, err := signer.ParseAlgorithm(t.Algorithm)

integration/multi_mirror_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/http/httptest"
7+
"strings"
78
"testing"
89
"time"
910

@@ -132,6 +133,13 @@ func TestMultiCosignerQuorum(t *testing.T) {
132133
t.Errorf("witness %d served %d requests, want 1", i, n)
133134
}
134135
}
136+
// tlog-tiles: the CA SHOULD identify itself with an operator
137+
// contact in the User-Agent so mirrors can reach (and not
138+
// rate-limit) it.
139+
ua, _ := stubs[0].lastUA.Load().(string)
140+
if !strings.Contains(ua, "+https://github.com/mcpherrinm/cactus") {
141+
t.Errorf("witness saw User-Agent %q, want the cactus contact UA", ua)
142+
}
135143
}
136144

137145
// TestMultiCosignerQuorumNotMet: with a single witness but quorum=2,

integration/witness_helpers_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ type stubWitness struct {
6262

6363
// calls counts served requests.
6464
calls atomic.Int64
65+
66+
// lastUA records the most recent request's User-Agent, so tests can
67+
// assert the CA identifies itself per tlog-tiles.
68+
lastUA atomic.Value // string
6569
}
6670

6771
// newStubWitness builds a stub witness signing for logID under the
@@ -92,6 +96,7 @@ func (w *stubWitness) endpoint(url string) cert.MirrorEndpoint {
9296
// cert/cosigner_request.go looks for.
9397
func (w *stubWitness) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
9498
w.calls.Add(1)
99+
w.lastUA.Store(r.Header.Get("User-Agent"))
95100
if w.delay > 0 {
96101
time.Sleep(w.delay)
97102
}

0 commit comments

Comments
 (0)