feat(bimi): reconcile the BIMI record to Cloudflare and keep it in step with the logo
The reconciler never wrote the BIMI record: only the console's verification knew about it, so a stored logo was never published to the zone. - SyncDnsRecordsInput carries the BIMI logo and certificate URLs; the record is created, updated (a= added or dropped) and, when the logo is removed, deleted. Only a record carrying this reconciler's comment is retired: one made by hand is left alone - bimi.Publish decides the record in one place for the timer sweep, the console's button and the panel's verification. A held certificate is advertised only while it is valid for the logo being served: replacing the logo ends the certificate, and a record that kept naming it makes every receiver reject BIMI, including those that need no certificate - saving or removing the logo or the certificate triggers the reconciliation in the background; the panel shows the record as the server writes it, says when a certificate is held back and why, and has a publish-now button - a read error is not 'no logo': the sync leaves the record alone instead of deleting it over a database hiccup Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ff9028927
commit
6e49ca00e2
17 files changed
+641
-59
No files matched your search
@@ -58,7 +58,9 @@ type Inputs struct {
|
||||
// the one the operator keeps elsewhere, used only when none is held.
|
||||
CertificatePEM []byte
|
||||
ExternalCertificateURL string
|
||||
Now time.Time
|
||||
// Withheld is why a held certificate is not in the record (see Publish).
|
||||
Withheld string
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
// RunChecks runs every check and returns them in the order a receiver would
|
||||
@@ -214,7 +216,12 @@ func checkCertificate(ctx context.Context, in Inputs, fetch Fetcher) []Check {
|
||||
for _, v := range violations {
|
||||
reasons = append(reasons, v.String())
|
||||
}
|
||||
return append([]Check{{ID: "certificate", Status: StatusFail, Detail: strings.Join(reasons, "; ")}}, out...)
|
||||
detail := strings.Join(reasons, "; ")
|
||||
if in.Withheld != "" {
|
||||
// The record stays valid for receivers that do not need a certificate
|
||||
detail = "held here but NOT published in the DNS record, because it does not apply: " + detail
|
||||
}
|
||||
return append([]Check{{ID: "certificate", Status: StatusFail, Detail: detail}}, out...)
|
||||
}
|
||||
if len(cert.Warnings) > 0 {
|
||||
return append([]Check{{ID: "certificate", Status: StatusWarn, Detail: strings.Join(cert.Warnings, "; ")}}, out...)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package bimi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Publication is what the domain's BIMI DNS record should say right now.
|
||||
//
|
||||
// One decision, taken in one place, because three things have to agree on it:
|
||||
// the record the reconciler writes to the zone, the one the console expects to
|
||||
// find there, and the setup check. They used to compute it separately, and the
|
||||
// reconciler did not compute it at all, so a logo that was stored was never
|
||||
// published.
|
||||
type Publication struct {
|
||||
// LogoURL is empty when the domain has no logo: then there is no record.
|
||||
LogoURL string
|
||||
// CertificateURL is the a= tag, empty when none should be published.
|
||||
CertificateURL string
|
||||
// Withheld says why a certificate that is held here is not being published.
|
||||
Withheld string
|
||||
}
|
||||
|
||||
// Publish decides the record for a domain.
|
||||
//
|
||||
// A certificate held here is published only while it is valid for the logo
|
||||
// being served. The certificate vouches for one exact SVG, so the moment the
|
||||
// logo is replaced it stops applying, and a record that still names it makes
|
||||
// receivers reject BIMI outright, including the ones that would have shown the
|
||||
// logo without a certificate. Withholding it keeps those working and leaves the
|
||||
// reason for the console to show.
|
||||
func Publish(svg, pem []byte, externalURL, domain, logoURL, hostedCertificateURL string, now time.Time) Publication {
|
||||
if len(svg) == 0 {
|
||||
return Publication{}
|
||||
}
|
||||
p := Publication{LogoURL: logoURL, CertificateURL: strings.TrimSpace(externalURL)}
|
||||
|
||||
if len(pem) == 0 {
|
||||
return p
|
||||
}
|
||||
|
||||
if _, violations := InspectCertificate(pem, domain, svg, now); len(violations) > 0 {
|
||||
rules := make([]string, 0, len(violations))
|
||||
for _, v := range violations {
|
||||
rules = append(rules, v.Rule)
|
||||
}
|
||||
p.Withheld = strings.Join(rules, ", ")
|
||||
return p
|
||||
}
|
||||
p.CertificateURL = hostedCertificateURL
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package bimi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
pubLogo = "https://mail.ebay.com/.well-known/bimi/default.svg"
|
||||
pubCert = "https://mail.ebay.com/.well-known/bimi/vmc.pem"
|
||||
)
|
||||
|
||||
func TestPublishNothingWithoutALogo(t *testing.T) {
|
||||
if got := Publish(nil, nil, "https://x.test/v.pem", "ebay.com", pubLogo, pubCert, fixtureNow); got != (Publication{}) {
|
||||
t.Fatalf("a domain with no logo must publish nothing, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishLogoOnly(t *testing.T) {
|
||||
got := Publish(read(t, "ebay.svg"), nil, "", "ebay.com", pubLogo, pubCert, fixtureNow)
|
||||
if got.LogoURL != pubLogo || got.CertificateURL != "" || got.Withheld != "" {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishAnExternalCertificateURL(t *testing.T) {
|
||||
got := Publish(read(t, "ebay.svg"), nil, " https://vmc.example.test/x.pem ", "ebay.com", pubLogo, pubCert, fixtureNow)
|
||||
if got.CertificateURL != "https://vmc.example.test/x.pem" {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishTheHeldCertificateWhileItIsValidForTheLogo(t *testing.T) {
|
||||
got := Publish(read(t, "ebay.svg"), read(t, "ebay.pem"), "https://old.test/x.pem", "ebay.com", pubLogo, pubCert, fixtureNow)
|
||||
// The held one wins over an external URL: it is the one checked against this logo
|
||||
if got.CertificateURL != pubCert || got.Withheld != "" {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The reason this exists: replacing the logo under a held certificate must not
|
||||
// leave a record that points receivers at a certificate for another picture.
|
||||
func TestPublishWithholdsACertificateThatNoLongerMatchesTheLogo(t *testing.T) {
|
||||
got := Publish(read(t, "cnn.svg"), read(t, "ebay.pem"), "", "ebay.com", pubLogo, pubCert, fixtureNow)
|
||||
if got.LogoURL != pubLogo {
|
||||
t.Errorf("the logo must still be published: %+v", got)
|
||||
}
|
||||
if got.CertificateURL != "" || got.Withheld != "logoMismatch" {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishWithholdsAnExpiredCertificate(t *testing.T) {
|
||||
got := Publish(read(t, "ebay.svg"), read(t, "ebay.pem"), "", "ebay.com", pubLogo, pubCert, time.Date(2028, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
if got.CertificateURL != "" || got.Withheld != "validity" {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishFallsBackToTheExternalURLWhenTheHeldCertificateIsWithheld(t *testing.T) {
|
||||
got := Publish(read(t, "cnn.svg"), read(t, "ebay.pem"), "https://vmc.example.test/x.pem", "ebay.com", pubLogo, pubCert, fixtureNow)
|
||||
if got.CertificateURL != "https://vmc.example.test/x.pem" || got.Withheld == "" {
|
||||
t.Fatalf("%+v", got)
|
||||
}
|
||||
}
|
||||
@@ -207,14 +207,10 @@ func BuildDnsRecordSpecs(spec DnsRecordSpec) []DnsRecord {
|
||||
|
||||
// BIMI, published only when there is a logo to point at.
|
||||
if spec.BimiLogoURL != "" {
|
||||
value := fmt.Sprintf("v=BIMI1; l=%s;", spec.BimiLogoURL)
|
||||
if spec.BimiVmcURL != "" {
|
||||
value = fmt.Sprintf("v=BIMI1; l=%s; a=%s;", spec.BimiLogoURL, spec.BimiVmcURL)
|
||||
}
|
||||
records = append(records, DnsRecord{
|
||||
Type: "TXT",
|
||||
Name: fmt.Sprintf("default._bimi.%s", domainName),
|
||||
Value: value,
|
||||
Value: BimiRecordValue(spec.BimiLogoURL, spec.BimiVmcURL),
|
||||
TTL: 1,
|
||||
Comment: "LambdaMail BIMI Record",
|
||||
})
|
||||
@@ -316,3 +312,15 @@ func hostInsideDomain(host, domain string) bool {
|
||||
// inside "example.test".
|
||||
return host == domain || strings.HasSuffix(host, "."+domain)
|
||||
}
|
||||
|
||||
// BimiRecordValue is the text of a domain's BIMI TXT record. The reconciler
|
||||
// writes it and the console shows it, so both come from here.
|
||||
func BimiRecordValue(logoURL, certificateURL string) string {
|
||||
if logoURL == "" {
|
||||
return ""
|
||||
}
|
||||
if certificateURL != "" {
|
||||
return fmt.Sprintf("v=BIMI1; l=%s; a=%s;", logoURL, certificateURL)
|
||||
}
|
||||
return fmt.Sprintf("v=BIMI1; l=%s;", logoURL)
|
||||
}
|
||||
Reference in new issue
Block a user