feat(bimi): hold the certificate, check the whole setup, serve the VMC
A Verified Mark Certificate cannot be made here: an authority issues it after checking the organisation. What the service can do is hold the file the authority returns, serve it where the DNS record points, and refuse one that would make receivers ignore BIMI without a word. - migration 0015: domain_bimi keeps the PEM chain next to the logo - bimi.InspectCertificate reads a chain and reports every reason it would not work: private key blocks refused outright, the brand-indicator usage, the validity period, the domain (exact, wildcard or organisational), and that the logo embedded in the certificate (gzipped data URI in the logotype extension) is byte for byte the logo being published. Tested against two real public VMCs (eBay via DigiCert, CNN via Valimail) plus mutations - GET /.well-known/bimi/vmc.pem (public, conditional, cacheable for an hour), and PUT/DELETE /api/v1/admin/bimi/certificate; the GET of the logo now reports the certificate as it stands against the logo being served, and replacing a logo under a held certificate says it no longer matches - GET /api/v1/admin/bimi/check looks at what a receiver would: the logo, DMARC enforcement, the BIMI record, the public logo URL, the certificate and its public URL. Its HTTP client is HTTPS only and refuses non-public addresses after DNS, since the URLs come from DNS and an operator field - the DNS record points a= at the held certificate when there is one Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0f556b190
commit
109b8360dd
21 files changed
+2129
-19
No files matched your search
@@ -0,0 +1,337 @@
|
||||
package httppresentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lambdamail/protocols/internal/domain/bimi"
|
||||
)
|
||||
|
||||
// memoryBimiStore is the store without a database.
|
||||
type memoryBimiStore struct {
|
||||
logos map[string]*BimiLogo
|
||||
}
|
||||
|
||||
func (m *memoryBimiStore) Save(_ context.Context, domain string, svg []byte, vmcURL string) (string, error) {
|
||||
held := m.logos[domain]
|
||||
logo := &BimiLogo{SVG: svg, ETag: "etag-" + domain, VmcURL: vmcURL}
|
||||
if held != nil {
|
||||
// Replacing the logo keeps a held certificate, as the real store does
|
||||
logo.VmcPEM = held.VmcPEM
|
||||
}
|
||||
m.logos[domain] = logo
|
||||
return logo.ETag, nil
|
||||
}
|
||||
|
||||
func (m *memoryBimiStore) Load(_ context.Context, domain string) (*BimiLogo, error) {
|
||||
return m.logos[domain], nil
|
||||
}
|
||||
|
||||
func (m *memoryBimiStore) Delete(_ context.Context, domain string) error {
|
||||
delete(m.logos, domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBimiStore) SaveCertificate(_ context.Context, domain string, pem []byte) (bool, error) {
|
||||
logo := m.logos[domain]
|
||||
if logo == nil {
|
||||
return false, nil
|
||||
}
|
||||
logo.VmcPEM = pem
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *memoryBimiStore) DeleteCertificate(_ context.Context, domain string) error {
|
||||
if logo := m.logos[domain]; logo != nil {
|
||||
logo.VmcPEM = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeBimiDNS map[string][]string
|
||||
|
||||
func (f fakeBimiDNS) LookupTXT(_ context.Context, name string) ([]string, error) {
|
||||
if records, ok := f[name]; ok {
|
||||
return records, nil
|
||||
}
|
||||
return nil, errors.New("no such host")
|
||||
}
|
||||
|
||||
func fixture(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("../../domain/bimi/testdata/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// The certificates in the fixtures are valid on this date.
|
||||
var bimiTestNow = time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
func newBimiRouter(store *memoryBimiStore) *Router {
|
||||
r := &Router{mux: http.NewServeMux()}
|
||||
r.bimi = &bimiAPI{
|
||||
store: store,
|
||||
sessions: testVerifier(interopSecret),
|
||||
defaultDomain: "ebay.com",
|
||||
mailHost: "mail.ebay.com",
|
||||
now: func() time.Time { return bimiTestNow },
|
||||
dns: fakeBimiDNS{},
|
||||
fetch: func(context.Context, string) (bimi.Response, error) {
|
||||
return bimi.Response{}, errors.New("offline")
|
||||
},
|
||||
}
|
||||
r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo)
|
||||
r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate)
|
||||
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/check", r.handleBimiCheck)
|
||||
return r
|
||||
}
|
||||
|
||||
func bimiCall(r *Router, method, target, body string, authed bool) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(method, target, strings.NewReader(body))
|
||||
if authed {
|
||||
req.Header.Set("Authorization", "Bearer "+adminSurfaceToken2())
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
r.mux.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func jsonBody(t *testing.T, rec *httptest.ResponseRecorder) map[string]any {
|
||||
t.Helper()
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("not JSON (%d): %s", rec.Code, rec.Body)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func putLogo(t *testing.T, r *Router, svg []byte) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
payload, _ := json.Marshal(map[string]string{"svg": string(svg)})
|
||||
return bimiCall(r, http.MethodPut, "/api/v1/admin/bimi?domain=ebay.com", string(payload), true)
|
||||
}
|
||||
|
||||
func putCertificate(r *Router, pem []byte) *httptest.ResponseRecorder {
|
||||
payload, _ := json.Marshal(map[string]string{"pem": string(pem)})
|
||||
return bimiCall(r, http.MethodPut, "/api/v1/admin/bimi/certificate?domain=ebay.com", string(payload), true)
|
||||
}
|
||||
|
||||
func TestBimiAdminRoutesNeedAnAdminSession(t *testing.T) {
|
||||
r := newBimiRouter(&memoryBimiStore{logos: map[string]*BimiLogo{}})
|
||||
for _, target := range []string{
|
||||
"/api/v1/admin/bimi?domain=ebay.com",
|
||||
"/api/v1/admin/bimi/certificate?domain=ebay.com",
|
||||
"/api/v1/admin/bimi/check?domain=ebay.com",
|
||||
} {
|
||||
for _, method := range []string{http.MethodGet, http.MethodPut, http.MethodDelete} {
|
||||
if rec := bimiCall(r, method, target, "{}", false); rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("%s %s without a session answered %d", method, target, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
if rec := bimiCall(r, http.MethodGet, "/api/v1/admin/bimi/check", "", true); rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("no domain answered %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBimiCertificateNeedsALogoFirst(t *testing.T) {
|
||||
r := newBimiRouter(&memoryBimiStore{logos: map[string]*BimiLogo{}})
|
||||
rec := putCertificate(r, fixture(t, "ebay.pem"))
|
||||
if rec.Code != http.StatusConflict {
|
||||
t.Fatalf("answered %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBimiAcceptsTheRealCertificateAndServesIt(t *testing.T) {
|
||||
store := &memoryBimiStore{logos: map[string]*BimiLogo{}}
|
||||
r := newBimiRouter(store)
|
||||
|
||||
if rec := putLogo(t, r, fixture(t, "ebay.svg")); rec.Code != http.StatusOK {
|
||||
t.Fatalf("the real logo was refused: %s", rec.Body)
|
||||
}
|
||||
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/vmc.pem", "", false); rec.Code != http.StatusNotFound {
|
||||
t.Errorf("before upload the public URL answered %d, want 404", rec.Code)
|
||||
}
|
||||
|
||||
rec := putCertificate(r, fixture(t, "ebay.pem"))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("the real certificate was refused: %s", rec.Body)
|
||||
}
|
||||
cert := jsonBody(t, rec)["certificate"].(map[string]any)
|
||||
if cert["valid"] != true || cert["matches_logo"] != true || cert["kind"] != "vmc" {
|
||||
t.Errorf("certificate report = %v", cert)
|
||||
}
|
||||
|
||||
// The public document: no session, the PEM, cacheable, and conditional
|
||||
public := bimiCall(r, http.MethodGet, "/.well-known/bimi/vmc.pem", "", false)
|
||||
if public.Code != http.StatusOK || !strings.Contains(public.Body.String(), "BEGIN CERTIFICATE") {
|
||||
t.Fatalf("public answered %d", public.Code)
|
||||
}
|
||||
if got := public.Header().Get("Content-Type"); got != "application/x-pem-file" {
|
||||
t.Errorf("content type = %q", got)
|
||||
}
|
||||
if public.Header().Get("X-Content-Type-Options") != "nosniff" || !strings.Contains(public.Header().Get("Cache-Control"), "public") {
|
||||
t.Errorf("headers = %v", public.Header())
|
||||
}
|
||||
conditional := httptest.NewRequest(http.MethodGet, "/.well-known/bimi/vmc.pem", nil)
|
||||
conditional.Header.Set("If-None-Match", public.Header().Get("ETag"))
|
||||
again := httptest.NewRecorder()
|
||||
r.mux.ServeHTTP(again, conditional)
|
||||
if again.Code != http.StatusNotModified {
|
||||
t.Errorf("conditional request answered %d", again.Code)
|
||||
}
|
||||
|
||||
// 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))
|
||||
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" {
|
||||
t.Errorf("urls = %v / %v", got["logo_url"], got["certificate_url"])
|
||||
}
|
||||
if got["certificate"].(map[string]any)["present"] != true {
|
||||
t.Errorf("certificate = %v", got["certificate"])
|
||||
}
|
||||
|
||||
// Removing it leaves the logo
|
||||
if rec := bimiCall(r, http.MethodDelete, "/api/v1/admin/bimi/certificate?domain=ebay.com", "", true); rec.Code != http.StatusOK {
|
||||
t.Fatalf("delete answered %d", rec.Code)
|
||||
}
|
||||
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/vmc.pem", "", false); rec.Code != http.StatusNotFound {
|
||||
t.Errorf("after delete the public URL answered %d", rec.Code)
|
||||
}
|
||||
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/default.svg", "", false); rec.Code != http.StatusOK {
|
||||
t.Errorf("the logo disappeared with the certificate: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBimiRefusesACertificateForAnotherLogo(t *testing.T) {
|
||||
store := &memoryBimiStore{logos: map[string]*BimiLogo{}}
|
||||
r := newBimiRouter(store)
|
||||
// CNN's logo is valid SVG Tiny PS but is not the one eBay's certificate holds
|
||||
if rec := putLogo(t, r, fixture(t, "cnn.svg")); rec.Code != http.StatusOK {
|
||||
t.Fatalf("logo refused: %s", rec.Body)
|
||||
}
|
||||
|
||||
rec := putCertificate(r, fixture(t, "ebay.pem"))
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("answered %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
violations, _ := jsonBody(t, rec)["violations"].([]any)
|
||||
if len(violations) == 0 || !strings.Contains(violations[0].(string), "logoMismatch") {
|
||||
t.Errorf("violations = %v", violations)
|
||||
}
|
||||
if len(store.logos["ebay.com"].VmcPEM) != 0 {
|
||||
t.Error("a certificate that does not match was stored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBimiNeverStoresAPrivateKey(t *testing.T) {
|
||||
store := &memoryBimiStore{logos: map[string]*BimiLogo{}}
|
||||
r := newBimiRouter(store)
|
||||
putLogo(t, r, fixture(t, "ebay.svg"))
|
||||
|
||||
key := "-----BEGIN PRIVATE KEY-----\nQUJD\n-----END PRIVATE KEY-----\n"
|
||||
rec := putCertificate(r, append(fixture(t, "ebay.pem"), key...))
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("answered %d", rec.Code)
|
||||
}
|
||||
if len(store.logos["ebay.com"].VmcPEM) != 0 {
|
||||
t.Error("a private key was stored and would be served publicly")
|
||||
}
|
||||
if strings.Contains(rec.Body.String(), "QUJD") {
|
||||
t.Error("the refusal echoed the key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBimiCertificateBodyMustBeJSON(t *testing.T) {
|
||||
r := newBimiRouter(&memoryBimiStore{logos: map[string]*BimiLogo{"ebay.com": {SVG: fixture(t, "ebay.svg")}}})
|
||||
if rec := bimiCall(r, http.MethodPut, "/api/v1/admin/bimi/certificate?domain=ebay.com", "-----BEGIN CERTIFICATE-----", true); rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("answered %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBimiReplacingTheLogoWarnsAboutTheHeldCertificate(t *testing.T) {
|
||||
store := &memoryBimiStore{logos: map[string]*BimiLogo{}}
|
||||
r := newBimiRouter(store)
|
||||
putLogo(t, r, fixture(t, "ebay.svg"))
|
||||
putCertificate(r, fixture(t, "ebay.pem"))
|
||||
|
||||
// Another valid logo: the certificate no longer vouches for what is served
|
||||
rec := putLogo(t, r, fixture(t, "cnn.svg"))
|
||||
if rec.Code != http.StatusOK || jsonBody(t, rec)["certificate_mismatch"] != true {
|
||||
t.Fatalf("no warning: %d %s", rec.Code, rec.Body)
|
||||
}
|
||||
cert := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi?domain=ebay.com", "", true))["certificate"].(map[string]any)
|
||||
if cert["valid"] != false || cert["matches_logo"] != false {
|
||||
t.Errorf("the console should show the certificate as no longer matching: %v", cert)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBimiCheckReportsEachStep(t *testing.T) {
|
||||
store := &memoryBimiStore{logos: map[string]*BimiLogo{}}
|
||||
r := newBimiRouter(store)
|
||||
putLogo(t, r, fixture(t, "ebay.svg"))
|
||||
|
||||
rec := bimiCall(r, http.MethodGet, "/api/v1/admin/bimi/check?domain=ebay.com", "", true)
|
||||
body := jsonBody(t, rec)
|
||||
if rec.Code != http.StatusOK || body["ready"] != false {
|
||||
t.Fatalf("with nothing published it must not be ready: %d %v", rec.Code, body)
|
||||
}
|
||||
ids := map[string]string{}
|
||||
for _, c := range body["checks"].([]any) {
|
||||
m := c.(map[string]any)
|
||||
ids[m["id"].(string)] = m["status"].(string)
|
||||
}
|
||||
if ids["logo"] != "ok" || ids["dmarc"] != "fail" || ids["record"] != "fail" {
|
||||
t.Errorf("checks = %v", ids)
|
||||
}
|
||||
|
||||
none := bimiCall(r, http.MethodGet, "/api/v1/admin/bimi/check?domain=unknown.test", "", true)
|
||||
if jsonBody(t, none)["configured"] != false {
|
||||
t.Errorf("a domain with no logo: %s", none.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicAddress(t *testing.T) {
|
||||
for address, want := range map[string]bool{
|
||||
"8.8.8.8": true,
|
||||
"1.1.1.1": true,
|
||||
"2001:4860::8888": true,
|
||||
"127.0.0.1": false,
|
||||
"10.0.0.5": false,
|
||||
"172.16.3.4": false,
|
||||
"192.168.1.1": false,
|
||||
"169.254.169.254": false,
|
||||
"100.64.0.1": false,
|
||||
"0.0.0.0": false,
|
||||
"::1": false,
|
||||
"fe80::1": false,
|
||||
"fc00::1": false,
|
||||
"224.0.0.1": false,
|
||||
} {
|
||||
if got := publicAddress(net.ParseIP(address)); got != want {
|
||||
t.Errorf("publicAddress(%s) = %v, want %v", address, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The fetcher is what keeps the console's checks from reaching this server's
|
||||
// own network. It refuses a literal private address before any request leaves.
|
||||
func TestFetcherRefusesPrivateAndPlainHTTP(t *testing.T) {
|
||||
fetch := newBimiFetcher()
|
||||
for _, url := range []string{"http://example.test/logo.svg", "https://127.0.0.1/logo.svg", "https://169.254.169.254/latest/meta-data/", "https://[::1]/x"} {
|
||||
if _, err := fetch(context.Background(), url); err == nil {
|
||||
t.Errorf("%s was fetched", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user