fix(bimi): each domain's logo under its own URL, and the publisher actually wired

Two bugs found by the setup check right after the last deploy:

- every domain's BIMI record named the deployment's own mail host, and the
  server picked the domain from the host name, so only the primary domain's
  logo could be fetched: ciberlab.seg.br's record pointed at a 404. The logo
  and the certificate are now served at /.well-known/bimi/<domain>/..., the
  record names that, and the webmail routes it. The old path still answers
- SetBimiPublisher ran before SetBimiAPI, found no BIMI API to attach to and
  was dropped, so saving a logo never published the DNS record by itself. It is
  kept on the router now, so the order does not matter

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-16 18:49:52 -03:00
1 parent e3366ef09d
commit 60c6f5731c
7 files changed
+173 -21

No files matched your search

+6 -1
View File
@@ -226,7 +226,12 @@ everyone who sends from it. Publishing one needs three things, and the console
file, DMARC, the BIMI record in DNS, the public URL of the logo, the file, DMARC, the BIMI record in DNS, the public URL of the logo, the
certificate and its public URL, and names the one that is wrong. certificate and its public URL, and names the one that is wrong.
The logo is served from `https://<mail host>/.well-known/bimi/default.svg`. The logo is served from `https://<mail host>/.well-known/bimi/<domain>/default.svg`
and the certificate from `.../<domain>/vmc.pem`: one mail host, a path per
domain, so every domain the server hosts can publish its own. (The earlier
`/.well-known/bimi/default.svg` chose the domain from the host name, which could
name only the primary domain; every other domain's record pointed at a 404. It
still answers, for records already published.)
**The DNS record follows the picture.** With a Cloudflare token configured, the **The DNS record follows the picture.** With a Cloudflare token configured, the
`default._bimi.<domain>` TXT record is part of what the DNS reconciler `default._bimi.<domain>` TXT record is part of what the DNS reconciler
@@ -22,15 +22,15 @@ func bimiPublication(ctx context.Context, repo *postgres.BimiRepository, cfg con
// Unknown is not "no logo": returning an empty publication here would // Unknown is not "no logo": returning an empty publication here would
// have the sync delete the record over a database hiccup. // have the sync delete the record over a database hiccup.
log.Printf("BIMI: could not read the logo for %s, leaving its record alone: %v", domain, err) log.Printf("BIMI: could not read the logo for %s, leaving its record alone: %v", domain, err)
return bimi.Publication{LogoURL: httppresentation.BimiLogoURL(cfg.PrimaryMailHost)} return bimi.Publication{LogoURL: httppresentation.BimiLogoURL(cfg.PrimaryMailHost, domain)}
} }
if logo == nil { if logo == nil {
return bimi.Publication{} return bimi.Publication{}
} }
return bimi.Publish( return bimi.Publish(
logo.SVG, logo.VmcPEM, logo.VmcURL, domain, logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
httppresentation.BimiLogoURL(cfg.PrimaryMailHost), httppresentation.BimiLogoURL(cfg.PrimaryMailHost, domain),
httppresentation.BimiVmcURL(cfg.PrimaryMailHost), httppresentation.BimiVmcURL(cfg.PrimaryMailHost, domain),
time.Now(), time.Now(),
) )
} }
@@ -9,6 +9,7 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"regexp"
"strings" "strings"
"time" "time"
@@ -53,8 +54,12 @@ type bimiAPI struct {
publish func(domain string) publish func(domain string)
} }
// SetBimiPublisher wires what happens after a logo or certificate changes. // SetBimiPublisher wires what happens after a logo or certificate changes. It
// may be called before or after SetBimiAPI: it was once called before, found no
// BIMI API to attach to, and was dropped without a word, so saving a logo never
// published anything.
func (r *Router) SetBimiPublisher(publish func(domain string)) { func (r *Router) SetBimiPublisher(publish func(domain string)) {
r.bimiPublish = publish
if r.bimi != nil { if r.bimi != nil {
r.bimi.publish = publish r.bimi.publish = publish
} }
@@ -81,6 +86,7 @@ func (r *Router) SetBimiAPI(store BimiStore, sessionSecret, defaultDomain, mailH
now: time.Now, now: time.Now,
dns: netResolver{}, dns: netResolver{},
fetch: newBimiFetcher(), fetch: newBimiFetcher(),
publish: r.bimiPublish,
} }
if sessionSecret != "" { if sessionSecret != "" {
r.bimi.sessions = NewWebSessionVerifier(sessionSecret) r.bimi.sessions = NewWebSessionVerifier(sessionSecret)
@@ -98,12 +104,15 @@ func (r *Router) handleBimiLogo(w http.ResponseWriter, req *http.Request) {
http.NotFound(w, req) http.NotFound(w, req)
return return
} }
r.serveBimiLogo(w, req, r.bimi.domainFor(req))
}
func (r *Router) serveBimiLogo(w http.ResponseWriter, req *http.Request, domain string) {
if req.Method != http.MethodGet && req.Method != http.MethodHead { if req.Method != http.MethodGet && req.Method != http.MethodHead {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required") writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
return return
} }
domain := r.bimi.domainFor(req)
logo, err := r.bimi.store.Load(req.Context(), domain) logo, err := r.bimi.store.Load(req.Context(), domain)
if err != nil || logo == nil { if err != nil || logo == nil {
http.NotFound(w, req) http.NotFound(w, req)
@@ -202,12 +211,16 @@ func (r *Router) handleBimiCertificate(w http.ResponseWriter, req *http.Request)
http.NotFound(w, req) http.NotFound(w, req)
return return
} }
r.serveBimiCertificate(w, req, r.bimi.domainFor(req))
}
func (r *Router) serveBimiCertificate(w http.ResponseWriter, req *http.Request, domain string) {
if req.Method != http.MethodGet && req.Method != http.MethodHead { if req.Method != http.MethodGet && req.Method != http.MethodHead {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required") writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
return return
} }
logo, err := r.bimi.store.Load(req.Context(), r.bimi.domainFor(req)) logo, err := r.bimi.store.Load(req.Context(), domain)
if err != nil || logo == nil || len(logo.VmcPEM) == 0 { if err != nil || logo == nil || len(logo.VmcPEM) == 0 {
http.NotFound(w, req) http.NotFound(w, req)
return return
@@ -285,7 +298,7 @@ func (r *Router) handleBimiCheck(w http.ResponseWriter, req *http.Request) {
// sync writes it, so "the record is wrong" means it differs from what the // sync writes it, so "the record is wrong" means it differs from what the
// reconciler would publish. // reconciler would publish.
publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain, publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
BimiLogoURL(r.bimi.mailHost), BimiVmcURL(r.bimi.mailHost), r.bimi.now()) BimiLogoURL(r.bimi.mailHost, domain), BimiVmcURL(r.bimi.mailHost, domain), r.bimi.now())
in := bimi.Inputs{ in := bimi.Inputs{
Domain: domain, Domain: domain,
LogoURL: publication.LogoURL, LogoURL: publication.LogoURL,
@@ -321,7 +334,7 @@ func (a *bimiAPI) get(w http.ResponseWriter, req *http.Request, domain string) {
return return
} }
publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain, publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
BimiLogoURL(a.mailHost), BimiVmcURL(a.mailHost), a.now()) BimiLogoURL(a.mailHost, domain), BimiVmcURL(a.mailHost, domain), a.now())
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"configured": true, "configured": true,
"etag": logo.ETag, "etag": logo.ETag,
@@ -333,8 +346,8 @@ func (a *bimiAPI) get(w http.ResponseWriter, req *http.Request, domain string) {
"vmc_url": logo.VmcURL, "vmc_url": logo.VmcURL,
"bytes": len(logo.SVG), "bytes": len(logo.SVG),
"svg": string(logo.SVG), "svg": string(logo.SVG),
"logo_url": BimiLogoURL(a.mailHost), "logo_url": BimiLogoURL(a.mailHost, domain),
"certificate_url": BimiVmcURL(a.mailHost), "certificate_url": BimiVmcURL(a.mailHost, domain),
"certificate": a.describeCertificate(domain, logo), "certificate": a.describeCertificate(domain, logo),
}) })
} }
@@ -487,12 +500,45 @@ func (a *bimiAPI) put(w http.ResponseWriter, req *http.Request, domain string) {
writeJSON(w, http.StatusOK, response) writeJSON(w, http.StatusOK, response)
} }
// BimiLogoURL is where a domain's mark is served from. // domainName accepts what a mail domain can be, and nothing a path could use to
func BimiLogoURL(mailHost string) string { // reach anything else.
return fmt.Sprintf("https://%s/.well-known/bimi/default.svg", mailHost) var domainName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$`)
// handleBimiPerDomain serves /.well-known/bimi/<domain>/default.svg and
// /.well-known/bimi/<domain>/vmc.pem.
//
// The legacy paths pick the domain from the host the request arrived on, which
// can name only the one domain whose mail host that is. The record a domain
// publishes names a URL on the deployment's mail host whatever the domain is,
// so every domain other than the primary one pointed at a logo that was not its
// own and got a 404.
func (r *Router) handleBimiPerDomain(w http.ResponseWriter, req *http.Request) {
if r.bimi == nil {
http.NotFound(w, req)
return
}
parts := strings.Split(strings.TrimPrefix(req.URL.Path, "/.well-known/bimi/"), "/")
if len(parts) != 2 || !domainName.MatchString(strings.ToLower(parts[0])) {
http.NotFound(w, req)
return
}
domain := strings.ToLower(parts[0])
switch parts[1] {
case "default.svg":
r.serveBimiLogo(w, req, domain)
case "vmc.pem":
r.serveBimiCertificate(w, req, domain)
default:
http.NotFound(w, req)
}
} }
// BimiVmcURL is where the certificate held here is served from. // BimiLogoURL is where a domain's mark is served from.
func BimiVmcURL(mailHost string) string { func BimiLogoURL(mailHost, domain string) string {
return fmt.Sprintf("https://%s/.well-known/bimi/vmc.pem", mailHost) return fmt.Sprintf("https://%s/.well-known/bimi/%s/default.svg", mailHost, strings.ToLower(domain))
}
// BimiVmcURL is where the certificate held for a domain is served from.
func BimiVmcURL(mailHost, domain string) string {
return fmt.Sprintf("https://%s/.well-known/bimi/%s/vmc.pem", mailHost, strings.ToLower(domain))
} }
@@ -92,6 +92,7 @@ func newBimiRouter(store *memoryBimiStore) *Router {
} }
r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo) r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo)
r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate) r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate)
r.mux.HandleFunc("/.well-known/bimi/", r.handleBimiPerDomain)
r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin) r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin)
r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin) r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin)
r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck) r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck)
@@ -195,7 +196,7 @@ func TestBimiAcceptsTheRealCertificateAndServesIt(t *testing.T) {
// The console reads it back, with the URLs the DNS record should carry // The console reads it back, with the URLs the DNS record should carry
got := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi?domain=ebay.com", "", true)) got := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi?domain=ebay.com", "", true))
if got["logo_url"] != "https://mail.ebay.com/.well-known/bimi/default.svg" || got["certificate_url"] != "https://mail.ebay.com/.well-known/bimi/vmc.pem" { if got["logo_url"] != "https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg" || got["certificate_url"] != "https://mail.ebay.com/.well-known/bimi/ebay.com/vmc.pem" {
t.Errorf("urls = %v / %v", got["logo_url"], got["certificate_url"]) t.Errorf("urls = %v / %v", got["logo_url"], got["certificate_url"])
} }
if got["certificate"].(map[string]any)["present"] != true { if got["certificate"].(map[string]any)["present"] != true {
@@ -409,7 +410,7 @@ func TestBimiCheckExpectsWhatTheReconcilerPublishes(t *testing.T) {
r.bimi.dns = fakeBimiDNS{ r.bimi.dns = fakeBimiDNS{
"_dmarc.ebay.com": {"v=DMARC1; p=reject"}, "_dmarc.ebay.com": {"v=DMARC1; p=reject"},
"default._bimi.ebay.com": {"v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/default.svg;"}, "default._bimi.ebay.com": {"v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg;"},
} }
body := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi/check?domain=ebay.com", "", true)) body := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi/check?domain=ebay.com", "", true))
@@ -439,12 +440,12 @@ func TestBimiGetReportsTheRecordItWouldPublish(t *testing.T) {
} }
got := read() got := read()
if got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/default.svg;" || got["certificate_withheld"] != "" { if got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg;" || got["certificate_withheld"] != "" {
t.Fatalf("logo only: %v", got) t.Fatalf("logo only: %v", got)
} }
putCertificate(r, fixture(t, "ebay.pem")) putCertificate(r, fixture(t, "ebay.pem"))
if got := read(); got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/default.svg; a=https://mail.ebay.com/.well-known/bimi/vmc.pem;" { if got := read(); got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg; a=https://mail.ebay.com/.well-known/bimi/ebay.com/vmc.pem;" {
t.Fatalf("with its certificate: %v", got["dns_record"]) t.Fatalf("with its certificate: %v", got["dns_record"])
} }
@@ -455,3 +456,83 @@ func TestBimiGetReportsTheRecordItWouldPublish(t *testing.T) {
t.Fatalf("after replacing the logo: %v / %v", got["dns_record"], got["certificate_withheld"]) t.Fatalf("after replacing the logo: %v / %v", got["dns_record"], got["certificate_withheld"])
} }
} }
// The bug this pins: every domain's record named the deployment's own mail host,
// and the host chose the domain, so only the primary domain's logo was ever
// found. A second domain's logo has to be served under its own name.
func TestBimiServesEachDomainsLogoUnderItsOwnName(t *testing.T) {
store := &memoryBimiStore{logos: map[string]*BimiLogo{
"ebay.com": {SVG: fixture(t, "ebay.svg"), ETag: "e1"},
"cnn.com": {SVG: fixture(t, "cnn.svg"), ETag: "c1", VmcPEM: fixture(t, "ebay.pem")},
}}
r := newBimiRouter(store) // its default domain, and the host these requests arrive on, is ebay.com
logo := bimiCall(r, http.MethodGet, "/.well-known/bimi/cnn.com/default.svg", "", false)
if logo.Code != http.StatusOK || logo.Body.String() != string(fixture(t, "cnn.svg")) {
t.Fatalf("the second domain's logo: %d", logo.Code)
}
if got := logo.Header().Get("Content-Type"); got != "image/svg+xml" {
t.Errorf("content type = %q", got)
}
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/cnn.com/vmc.pem", "", false); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "BEGIN CERTIFICATE") {
t.Errorf("the second domain's certificate: %d", rec.Code)
}
// Case does not matter in a host name
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/CNN.com/default.svg", "", false); rec.Code != http.StatusOK {
t.Errorf("upper case: %d", rec.Code)
}
// The legacy path still answers with the host's own domain
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/default.svg", "", false); rec.Code != http.StatusOK || rec.Body.String() != string(fixture(t, "ebay.svg")) {
t.Errorf("legacy path: %d", rec.Code)
}
}
func TestBimiPerDomainPathsRefuseWhatIsNotADomainLogo(t *testing.T) {
store := &memoryBimiStore{logos: map[string]*BimiLogo{"ebay.com": {SVG: fixture(t, "ebay.svg"), ETag: "e1"}}}
r := newBimiRouter(store)
for _, path := range []string{
"/.well-known/bimi/nobody.test/default.svg", // a domain with no logo
"/.well-known/bimi/ebay.com/other.svg", // not a document we serve
"/.well-known/bimi/ebay.com/", // no document
"/.well-known/bimi/ebay.com/x/default.svg", // extra segment
"/.well-known/bimi/ebay.com/default.svg/x",
"/.well-known/bimi/-bad.com/default.svg", // not a host name
"/.well-known/bimi/a_b.com/default.svg",
"/.well-known/bimi/localhost/default.svg", // a single label is not a mail domain
"/.well-known/bimi/%2e%2e/default.svg",
} {
if rec := bimiCall(r, http.MethodGet, path, "", false); rec.Code != http.StatusNotFound {
t.Errorf("%s answered %d, want 404", path, rec.Code)
}
}
if rec := bimiCall(r, http.MethodPost, "/.well-known/bimi/ebay.com/default.svg", "", false); rec.Code != http.StatusMethodNotAllowed {
t.Errorf("POST answered %d", rec.Code)
}
}
// The wiring bug: the publisher was handed over before the BIMI API existed and
// was dropped. Either order has to work.
func TestBimiPublisherSurvivesTheOrderItIsWiredIn(t *testing.T) {
store := &memoryBimiStore{logos: map[string]*BimiLogo{}}
publisher := func(string) {}
before := &Router{mux: http.NewServeMux()}
before.SetBimiPublisher(publisher)
before.SetBimiAPI(store, "secret", "ebay.com", "mail.ebay.com")
if before.bimi.publish == nil {
t.Error("a publisher set before the API was lost")
}
after := &Router{mux: http.NewServeMux()}
after.SetBimiAPI(store, "secret", "ebay.com", "mail.ebay.com")
after.SetBimiPublisher(publisher)
if after.bimi.publish == nil {
t.Error("a publisher set after the API was lost")
}
none := &Router{mux: http.NewServeMux()}
none.SetBimiAPI(store, "secret", "ebay.com", "mail.ebay.com")
if none.bimi.publish != nil {
t.Error("with no DNS provider there is nothing to publish to")
}
}
@@ -41,6 +41,7 @@ type Router struct {
dnsReconciler DomainReconciler dnsReconciler DomainReconciler
dnsStatus DnsStatusWriter dnsStatus DnsStatusWriter
bimi *bimiAPI bimi *bimiAPI
bimiPublish func(domain string)
// degradedFunc reports a condition that leaves the service running but // degradedFunc reports a condition that leaves the service running but
// not fit for production, the clearest case being a self-signed // not fit for production, the clearest case being a self-signed
// certificate standing in for one Traefik never issued // certificate standing in for one Traefik never issued
@@ -81,6 +82,7 @@ func (r *Router) registerRoutes() {
r.mux.HandleFunc("/.well-known/security.txt", r.handleSecurityTxt) r.mux.HandleFunc("/.well-known/security.txt", r.handleSecurityTxt)
r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo) r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo)
r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate) r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate)
r.mux.HandleFunc("/.well-known/bimi/", r.handleBimiPerDomain)
r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin) r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin)
r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin) r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin)
r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck) r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck)
+10
View File
@@ -36,6 +36,16 @@ const nextConfig: NextConfig = {
source: "/.well-known/bimi/default.svg", source: "/.well-known/bimi/default.svg",
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/default.svg`, destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/default.svg`,
}, },
{
// A record names its own domain in the path, so every domain the server
// hosts can publish its own logo and certificate from this one host.
source: "/.well-known/bimi/:domain/default.svg",
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/:domain/default.svg`,
},
{
source: "/.well-known/bimi/:domain/vmc.pem",
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/:domain/vmc.pem`,
},
{ {
// The BIMI certificate (VMC/CMC) the DNS record's a= tag points at, // The BIMI certificate (VMC/CMC) the DNS record's a= tag points at,
// fetched the same way as the logo above. // fetched the same way as the logo above.
@@ -66,4 +66,12 @@ describe("the routes the protocols service owns", () => {
expect(config).toContain("/.well-known/bimi/vmc.pem"); expect(config).toContain("/.well-known/bimi/vmc.pem");
expect(config).toMatch(/bimi\/vmc\.pem[\s\S]{0,200}PROTOCOLS_SERVICE_URL/); expect(config).toMatch(/bimi\/vmc\.pem[\s\S]{0,200}PROTOCOLS_SERVICE_URL/);
}); });
// Each domain's record names its own logo and certificate under its own name.
// Without these rewrites only the host's primary domain had a logo that could
// be fetched; every other domain's record pointed at a 404.
it("routes the per-domain BIMI logo and certificate to the service", () => {
expect(config).toMatch(/bimi\/:domain\/default\.svg[\s\S]{0,200}PROTOCOLS_SERVICE_URL[^\n]*:domain\/default\.svg/);
expect(config).toMatch(/bimi\/:domain\/vmc\.pem[\s\S]{0,200}PROTOCOLS_SERVICE_URL[^\n]*:domain\/vmc\.pem/);
});
}); });