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:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-16 16:50:33 -03:00
1 parent c0f556b190
commit 109b8360dd
21 files changed
+2129 -19

No files matched your search

@@ -22,9 +22,17 @@ func (s bimiStore) Load(ctx context.Context, domain string) (*httppresentation.B
if err != nil || logo == nil {
return nil, err
}
return &httppresentation.BimiLogo{SVG: logo.SVG, ETag: logo.ETag, VmcURL: logo.VmcURL}, nil
return &httppresentation.BimiLogo{SVG: logo.SVG, ETag: logo.ETag, VmcURL: logo.VmcURL, VmcPEM: logo.VmcPEM}, nil
}
func (s bimiStore) Delete(ctx context.Context, domain string) error {
return s.repo.Delete(ctx, domain)
}
func (s bimiStore) SaveCertificate(ctx context.Context, domain string, pem []byte) (bool, error) {
return s.repo.SaveCertificate(ctx, domain, pem)
}
func (s bimiStore) DeleteCertificate(ctx context.Context, domain string) error {
return s.repo.DeleteCertificate(ctx, domain)
}
@@ -239,7 +239,7 @@ func run(cfg config) {
}
// The BIMI mark: served to receivers over the well-known URL, managed from
// the domains panel.
router.SetBimiAPI(bimiStore{postgres.NewBimiRepository(pool)}, cfg.JwtSecret, cfg.domain())
router.SetBimiAPI(bimiStore{postgres.NewBimiRepository(pool)}, cfg.JwtSecret, cfg.domain(), cfg.PrimaryMailHost)
// The TLS panel reads the live watcher instead of the constants the admin
// service used to return, which reported a healthy certificate whatever
@@ -722,7 +722,13 @@ func (s *dnsSpecSource) ExpectedRecords(ctx context.Context, domain string) ([]e
if s.bimi != nil {
if logo, err := s.bimi.Load(ctx, domain); err == nil && logo != nil {
spec.BimiLogoURL = httppresentation.BimiLogoURL(s.cfg.PrimaryMailHost)
spec.BimiVmcURL = logo.VmcURL
// The certificate held here wins: it is the one verified against the
// logo being served. An external URL is the fallback.
if len(logo.VmcPEM) > 0 {
spec.BimiVmcURL = httppresentation.BimiVmcURL(s.cfg.PrimaryMailHost)
} else {
spec.BimiVmcURL = logo.VmcURL
}
}
}
@@ -0,0 +1,256 @@
package bimi
import (
"bytes"
"context"
"fmt"
"strings"
"time"
)
// What decides whether a receiver shows the logo is spread over six places that
// each fail silently: the logo file, the DMARC policy, the BIMI DNS record, the
// public URL of the logo, the certificate, and the public URL of the
// certificate. RunChecks looks at all of them the way a receiver would and says
// which one is wrong, instead of leaving the operator to wait for a message to
// arrive somewhere and show nothing.
// Status of one check.
type Status string
const (
StatusOK Status = "ok"
StatusWarn Status = "warn"
StatusFail Status = "fail"
)
// Check is the outcome of one step.
type Check struct {
ID string
Status Status
Detail string
}
// Resolver looks up TXT records.
type Resolver interface {
LookupTXT(ctx context.Context, name string) ([]string, error)
}
// Response is what a fetch returned.
type Response struct {
Status int
ContentType string
Body []byte
}
// Fetcher gets a public HTTPS URL.
type Fetcher func(ctx context.Context, url string) (Response, error)
// Inputs describe what should be published.
type Inputs struct {
Domain string
// LogoURL and CertificateURL are where the DNS record is expected to
// point. CertificateURL is empty when no certificate is published.
LogoURL string
CertificateURL string
SVG []byte
// CertificatePEM is the chain held here, if any. ExternalCertificateURL is
// the one the operator keeps elsewhere, used only when none is held.
CertificatePEM []byte
ExternalCertificateURL string
Now time.Time
}
// RunChecks runs every check and returns them in the order a receiver would
// meet them. It never fails as a whole: a lookup that errors is a check that
// fails, with the reason.
func RunChecks(ctx context.Context, in Inputs, dns Resolver, fetch Fetcher) []Check {
checks := []Check{
checkLogo(in),
checkDMARC(ctx, in.Domain, dns),
checkRecord(ctx, in, dns),
checkLogoURL(ctx, in, fetch),
}
checks = append(checks, checkCertificate(ctx, in, fetch)...)
return checks
}
func checkLogo(in Inputs) Check {
violations := Validate(in.SVG)
if len(violations) == 0 {
return Check{ID: "logo", Status: StatusOK, Detail: fmt.Sprintf("SVG Tiny PS valid, %d bytes", len(in.SVG))}
}
reasons := make([]string, 0, len(violations))
for _, v := range violations {
reasons = append(reasons, v.String())
}
return Check{ID: "logo", Status: StatusFail, Detail: strings.Join(reasons, "; ")}
}
// checkDMARC: BIMI is only honoured for a domain that enforces DMARC. A policy
// of "none" is the common reason a correct logo and certificate show nothing.
func checkDMARC(ctx context.Context, domain string, dns Resolver) Check {
records, err := dns.LookupTXT(ctx, "_dmarc."+domain)
if err != nil {
return Check{ID: "dmarc", Status: StatusFail, Detail: fmt.Sprintf("no DMARC record at _dmarc.%s (%v)", domain, err)}
}
var record string
for _, r := range records {
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(r)), "v=dmarc1") {
record = r
break
}
}
if record == "" {
return Check{ID: "dmarc", Status: StatusFail, Detail: "no DMARC record: BIMI needs a policy of quarantine or reject"}
}
tags := parseTags(record)
policy := strings.ToLower(tags["p"])
switch policy {
case "quarantine", "reject":
default:
return Check{ID: "dmarc", Status: StatusFail, Detail: fmt.Sprintf("DMARC policy is %q: BIMI needs quarantine or reject", policy)}
}
if pct, ok := tags["pct"]; ok && strings.TrimSpace(pct) != "100" {
return Check{ID: "dmarc", Status: StatusWarn, Detail: fmt.Sprintf("DMARC applies to only %s%% of mail; BIMI needs pct=100 (or no pct)", pct)}
}
return Check{ID: "dmarc", Status: StatusOK, Detail: "DMARC policy is " + policy}
}
// checkRecord compares the published BIMI record with the one the service
// would publish.
func checkRecord(ctx context.Context, in Inputs, dns Resolver) Check {
name := "default._bimi." + in.Domain
records, err := dns.LookupTXT(ctx, name)
if err != nil {
return Check{ID: "record", Status: StatusFail, Detail: fmt.Sprintf("no TXT record at %s (%v)", name, err)}
}
var found map[string]string
for _, r := range records {
tags := parseTags(r)
if strings.EqualFold(tags["v"], "BIMI1") {
found = tags
break
}
}
if found == nil {
return Check{ID: "record", Status: StatusFail, Detail: fmt.Sprintf("%s has no v=BIMI1 record", name)}
}
var problems []string
if found["l"] != in.LogoURL {
problems = append(problems, fmt.Sprintf("l= is %q, expected %q", found["l"], in.LogoURL))
}
if found["a"] != in.CertificateURL {
if in.CertificateURL == "" {
problems = append(problems, fmt.Sprintf("a= is %q but no certificate is published here", found["a"]))
} else {
problems = append(problems, fmt.Sprintf("a= is %q, expected %q", found["a"], in.CertificateURL))
}
}
if len(problems) > 0 {
return Check{ID: "record", Status: StatusFail, Detail: strings.Join(problems, "; ")}
}
return Check{ID: "record", Status: StatusOK, Detail: "the BIMI record points at this logo" + ifs(in.CertificateURL != "", " and certificate")}
}
func checkLogoURL(ctx context.Context, in Inputs, fetch Fetcher) Check {
resp, err := fetch(ctx, in.LogoURL)
if err != nil {
return Check{ID: "logoUrl", Status: StatusFail, Detail: fmt.Sprintf("could not fetch %s (%v)", in.LogoURL, err)}
}
switch {
case resp.Status != 200:
return Check{ID: "logoUrl", Status: StatusFail, Detail: fmt.Sprintf("%s answered %d", in.LogoURL, resp.Status)}
case !strings.HasPrefix(strings.ToLower(resp.ContentType), "image/svg+xml"):
return Check{ID: "logoUrl", Status: StatusFail, Detail: fmt.Sprintf("served as %q, receivers require image/svg+xml", resp.ContentType)}
case !bytes.Equal(resp.Body, in.SVG):
return Check{ID: "logoUrl", Status: StatusFail, Detail: "the public URL serves a different file than the one stored (a cache in front?)"}
}
return Check{ID: "logoUrl", Status: StatusOK, Detail: "the logo is public over HTTPS and matches"}
}
// checkCertificate returns one or two checks: the certificate itself, and for a
// hosted one, that the public URL really serves it.
func checkCertificate(ctx context.Context, in Inputs, fetch Fetcher) []Check {
now := in.Now
if now.IsZero() {
now = time.Now()
}
var pemData []byte
var out []Check
switch {
case len(in.CertificatePEM) > 0:
pemData = in.CertificatePEM
if in.CertificateURL != "" {
out = append(out, checkCertificateURL(ctx, in, fetch))
}
case in.ExternalCertificateURL != "":
resp, err := fetch(ctx, in.ExternalCertificateURL)
if err != nil || resp.Status != 200 {
detail := fmt.Sprintf("could not fetch %s", in.ExternalCertificateURL)
if err != nil {
detail += fmt.Sprintf(" (%v)", err)
} else {
detail += fmt.Sprintf(": it answered %d", resp.Status)
}
return []Check{{ID: "certificate", Status: StatusFail, Detail: detail}}
}
pemData = resp.Body
default:
return []Check{{
ID: "certificate",
Status: StatusWarn,
Detail: "no certificate: Gmail and Apple Mail show the logo only with a VMC or CMC from a certificate authority; some other receivers show it without one",
}}
}
cert, violations := InspectCertificate(pemData, in.Domain, in.SVG, now)
if len(violations) > 0 {
reasons := make([]string, 0, len(violations))
for _, v := range violations {
reasons = append(reasons, v.String())
}
return append([]Check{{ID: "certificate", Status: StatusFail, Detail: strings.Join(reasons, "; ")}}, out...)
}
if len(cert.Warnings) > 0 {
return append([]Check{{ID: "certificate", Status: StatusWarn, Detail: strings.Join(cert.Warnings, "; ")}}, out...)
}
detail := fmt.Sprintf("%s issued to %s, valid until %s, and it carries this logo", strings.ToUpper(string(cert.Kind)), cert.Organization, cert.NotAfter.UTC().Format("2006-01-02"))
return append([]Check{{ID: "certificate", Status: StatusOK, Detail: detail}}, out...)
}
func checkCertificateURL(ctx context.Context, in Inputs, fetch Fetcher) Check {
resp, err := fetch(ctx, in.CertificateURL)
switch {
case err != nil:
return Check{ID: "certificateUrl", Status: StatusFail, Detail: fmt.Sprintf("could not fetch %s (%v)", in.CertificateURL, err)}
case resp.Status != 200:
return Check{ID: "certificateUrl", Status: StatusFail, Detail: fmt.Sprintf("%s answered %d", in.CertificateURL, resp.Status)}
case !bytes.Equal(bytes.TrimSpace(resp.Body), bytes.TrimSpace(in.CertificatePEM)):
return Check{ID: "certificateUrl", Status: StatusFail, Detail: "the public URL serves a different file than the one stored"}
}
return Check{ID: "certificateUrl", Status: StatusOK, Detail: "the certificate is public over HTTPS and matches"}
}
// parseTags reads "k=v; k=v" records (DMARC, BIMI).
func parseTags(record string) map[string]string {
tags := map[string]string{}
for _, part := range strings.Split(record, ";") {
key, value, found := strings.Cut(strings.TrimSpace(part), "=")
if found {
tags[strings.ToLower(strings.TrimSpace(key))] = strings.TrimSpace(value)
}
}
return tags
}
func ifs(condition bool, text string) string {
if condition {
return text
}
return ""
}
@@ -0,0 +1,176 @@
package bimi
import (
"context"
"errors"
"strings"
"testing"
)
type fakeDNS map[string][]string
func (f fakeDNS) LookupTXT(_ context.Context, name string) ([]string, error) {
records, ok := f[name]
if !ok {
return nil, errors.New("no such host")
}
return records, nil
}
func fakeFetch(files map[string]Response) Fetcher {
return func(_ context.Context, url string) (Response, error) {
resp, ok := files[url]
if !ok {
return Response{}, errors.New("connection refused")
}
return resp, nil
}
}
func byID(checks []Check) map[string]Check {
out := map[string]Check{}
for _, c := range checks {
out[c.ID] = c
}
return out
}
const (
logoURL = "https://mail.ebay.com/.well-known/bimi/default.svg"
vmcURL = "https://mail.ebay.com/.well-known/bimi/vmc.pem"
)
// goodSVG is a logo the validator accepts.
const goodSVG = `<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny-ps" viewBox="0 0 100 100"><title>Example</title><rect width="100" height="100" fill="#fff"/></svg>`
func TestRunChecksAllGoodWithTheRealCertificate(t *testing.T) {
svg := read(t, "ebay.svg")
pemData := read(t, "ebay.pem")
in := Inputs{Domain: "ebay.com", LogoURL: logoURL, CertificateURL: vmcURL, SVG: svg, CertificatePEM: pemData, Now: fixtureNow}
dns := fakeDNS{
"_dmarc.ebay.com": {"v=DMARC1; p=reject; pct=100; rua=mailto:x@ebay.com"},
"default._bimi.ebay.com": {"v=BIMI1; l=" + logoURL + "; a=" + vmcURL + ";"},
}
fetch := fakeFetch(map[string]Response{
logoURL: {Status: 200, ContentType: "image/svg+xml", Body: svg},
vmcURL: {Status: 200, ContentType: "application/x-pem-file", Body: pemData},
})
got := byID(RunChecks(context.Background(), in, dns, fetch))
for _, id := range []string{"logo", "dmarc", "record", "logoUrl", "certificate", "certificateUrl"} {
if got[id].Status != StatusOK {
t.Errorf("%s = %s (%s)", id, got[id].Status, got[id].Detail)
}
}
}
func TestRunChecksDMARC(t *testing.T) {
cases := []struct {
record string
want Status
}{
{"v=DMARC1; p=none", StatusFail},
{"v=DMARC1; p=quarantine", StatusOK},
{"v=DMARC1; p=reject; pct=100", StatusOK},
{"v=DMARC1; p=reject; pct=50", StatusWarn},
{"v=spf1 -all", StatusFail},
}
for _, c := range cases {
got := checkDMARC(context.Background(), "example.test", fakeDNS{"_dmarc.example.test": {c.record}})
if got.Status != c.want {
t.Errorf("%q: %s (%s), want %s", c.record, got.Status, got.Detail, c.want)
}
}
if got := checkDMARC(context.Background(), "example.test", fakeDNS{}); got.Status != StatusFail {
t.Errorf("missing record: %s", got.Status)
}
}
func TestRunChecksRecordMismatchesAreSpelledOut(t *testing.T) {
in := Inputs{Domain: "example.test", LogoURL: "https://mail.example.test/.well-known/bimi/default.svg", CertificateURL: "https://mail.example.test/.well-known/bimi/vmc.pem"}
missing := checkRecord(context.Background(), in, fakeDNS{})
if missing.Status != StatusFail || !strings.Contains(missing.Detail, "default._bimi.example.test") {
t.Errorf("missing: %+v", missing)
}
wrong := checkRecord(context.Background(), in, fakeDNS{"default._bimi.example.test": {"v=BIMI1; l=https://elsewhere.test/logo.svg;"}})
if wrong.Status != StatusFail || !strings.Contains(wrong.Detail, "l=") || !strings.Contains(wrong.Detail, "a=") {
t.Errorf("wrong: %+v", wrong)
}
// Without a certificate here, an a= in DNS is a fault too: it points nowhere
noCert := Inputs{Domain: "example.test", LogoURL: in.LogoURL}
stray := checkRecord(context.Background(), noCert, fakeDNS{"default._bimi.example.test": {"v=BIMI1; l=" + in.LogoURL + "; a=https://old.test/x.pem"}})
if stray.Status != StatusFail {
t.Errorf("stray a=: %+v", stray)
}
}
func TestRunChecksLogoURL(t *testing.T) {
in := Inputs{Domain: "example.test", LogoURL: logoURL, SVG: []byte(goodSVG)}
cases := map[string]struct {
resp Response
err bool
want Status
}{
"ok": {Response{Status: 200, ContentType: "image/svg+xml; charset=utf-8", Body: []byte(goodSVG)}, false, StatusOK},
"not found": {Response{Status: 404}, false, StatusFail},
"wrong type": {Response{Status: 200, ContentType: "text/html", Body: []byte(goodSVG)}, false, StatusFail},
"different bytes": {Response{Status: 200, ContentType: "image/svg+xml", Body: []byte("<svg/>")}, false, StatusFail},
"unreachable": {Response{}, true, StatusFail},
}
for name, c := range cases {
files := map[string]Response{}
if !c.err {
files[logoURL] = c.resp
}
if got := checkLogoURL(context.Background(), in, fakeFetch(files)); got.Status != c.want {
t.Errorf("%s: %s (%s), want %s", name, got.Status, got.Detail, c.want)
}
}
}
func TestRunChecksCertificate(t *testing.T) {
svg := read(t, "ebay.svg")
pemData := read(t, "ebay.pem")
base := Inputs{Domain: "ebay.com", LogoURL: logoURL, SVG: svg, Now: fixtureNow}
t.Run("none is a warning, with the reason", func(t *testing.T) {
got := byID(checkCertificate(context.Background(), base, fakeFetch(nil)))["certificate"]
if got.Status != StatusWarn || !strings.Contains(got.Detail, "Gmail") {
t.Errorf("%+v", got)
}
})
t.Run("hosted but the public URL serves something else", func(t *testing.T) {
in := base
in.CertificatePEM, in.CertificateURL = pemData, vmcURL
got := byID(checkCertificate(context.Background(), in, fakeFetch(map[string]Response{vmcURL: {Status: 200, Body: []byte("other")}})))
if got["certificate"].Status != StatusOK || got["certificateUrl"].Status != StatusFail {
t.Errorf("%+v", got)
}
})
t.Run("hosted for a logo that changed since", func(t *testing.T) {
in := base
in.CertificatePEM, in.CertificateURL, in.SVG = pemData, vmcURL, []byte(goodSVG)
got := byID(checkCertificate(context.Background(), in, fakeFetch(map[string]Response{vmcURL: {Status: 200, Body: pemData}})))
if got["certificate"].Status != StatusFail || !strings.Contains(got["certificate"].Detail, "logoMismatch") {
t.Errorf("%+v", got["certificate"])
}
})
t.Run("kept elsewhere, fetched and inspected", func(t *testing.T) {
in := base
in.ExternalCertificateURL = "https://vmc.example.test/x.pem"
got := byID(checkCertificate(context.Background(), in, fakeFetch(map[string]Response{in.ExternalCertificateURL: {Status: 200, Body: pemData}})))
if got["certificate"].Status != StatusOK {
t.Errorf("%+v", got["certificate"])
}
bad := byID(checkCertificate(context.Background(), in, fakeFetch(nil)))
if bad["certificate"].Status != StatusFail {
t.Errorf("unreachable external certificate: %+v", bad["certificate"])
}
})
}
@@ -0,0 +1,15 @@
Real Verified Mark Certificates, used only to test the parser.
Both are public: a BIMI certificate is published on purpose at the URL in the
`a=` tag of the sender's `default._bimi` DNS record, so that any mail provider
can fetch it.
| Files | Published by | Fetched from |
|-------|--------------|--------------|
| `ebay.pem`, `ebay.svg` | eBay, issued by DigiCert | `default._bimi.ebay.com` (2026-10-16) |
| `cnn.pem`, `cnn.svg` | CNN, issued through Valimail | `default._bimi.cnn.com` (2026-10-16) |
The tests pin the clock to 2026-10-16, inside both validity periods. They hold
the format the parser has to read: a leaf certificate with the "Brand Indicator
for Message Identification" extended key usage and a logotype extension whose
data URI is the logo, gzipped and base64 encoded.
+144
View File
@@ -0,0 +1,144 @@
-----BEGIN CERTIFICATE-----
MIIMqTCCCpGgAwIBAgIQDLZYmZx5WjoZx+5ENKgQvTANBgkqhkiG9w0BAQsFADBf
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xNzA1BgNVBAMT
LkRpZ2lDZXJ0IFZlcmlmaWVkIE1hcmsgUlNBNDA5NiBTSEEyNTYgMjAyMSBDQTEw
HhcNMjYwODI4MDAwMDAwWhcNMjcwOTAxMjM1OTU5WjCCAUMxEzARBgsrBgEEAYI3
PAIBAxMCVVMxGTAXBgsrBgEEAYI3PAIBAhMIRGVsYXdhcmUxHTAbBgNVBA8TFFBy
aXZhdGUgT3JnYW5pemF0aW9uMRAwDgYDVQQFEwcyOTc2NzMwMQswCQYDVQQGEwJV
UzEQMA4GA1UECBMHR2VvcmdpYTEQMA4GA1UEBxMHQXRsYW50YTEbMBkGA1UECRMS
MTkwIE1hcmlldHRhIFN0IE5XMSEwHwYDVQQKExhDYWJsZSBOZXdzIE5ldHdvcmss
IEluYy4xITAfBgNVBAMTGENhYmxlIE5ld3MgTmV0d29yaywgSW5jLjEfMB0GCisG
AQQBg55fAQ0TD1JlZ2lzdGVyZWQgTWFyazESMBAGCisGAQQBg55fAQMTAlVTMRcw
FQYKKwYBBAGDnl8BBBMHNTgxNzkzMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBALyeWFAqMTLwoyBIgdj2TEVoTgikbTjri0AR7W0nFPIW0X/yrG1jPaG2
lB/OHFWGplM44aKkTvQ9fw+/bTK4Mc9exuRTyGXD9xx71VbKyngJgFdZOJYUMKO7
nq2jbriLB6HeE02QTcxrGO+fTuhX38qUw46SNKSw7FnOreYnx88QHyU9x31XE71a
eqYGR5216exHj7G2jKyT8H4WDVaIzRv/GFG7z75Kmg49+RgQyvBo3n7Okqvdk7q5
Ta/LYPpi1nd0/KpfMHAz8NaPAIRMYxm7UaFI0iPuE6wDOoVSIhGYPuj3LluHdRhX
qXWU+ChUEby5unBLYJDt/pcPUcyE8H0CAwEAAaOCB3kwggd1MB8GA1UdIwQYMBaA
FL6fvY1XbZW1rWPDl06rqIRdOgf1MB0GA1UdDgQWBBQzLUCqtZi1xOrnA3sLqvdw
53Zw0jASBgNVHREECzAJggdjbm4uY29tMFAGA1UdIARJMEcwNwYKYIZIAYb9bAAC
BTApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDAYK
KwYBBAGDnl8BATATBgNVHSUEDDAKBggrBgEFBQcDHzCBpQYDVR0fBIGdMIGaMEug
SaBHhkVodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRWZXJpZmllZE1h
cmtSU0E0MDk2U0hBMjU2MjAyMUNBMS5jcmwwS6BJoEeGRWh0dHA6Ly9jcmw0LmRp
Z2ljZXJ0LmNvbS9EaWdpQ2VydFZlcmlmaWVkTWFya1JTQTQwOTZTSEEyNTYyMDIx
Q0ExLmNybDBkBggrBgEFBQcBAQRYMFYwVAYIKwYBBQUHMAKGSGh0dHA6Ly9jYWNl
cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFZlcmlmaWVkTWFya1JTQTQwOTZTSEEy
NTYyMDIxQ0ExLmNydDAMBgNVHRMBAf8EAjAAMIIFDAYIKwYBBQUHAQwEggT+MIIE
+qKCBPagggTyMIIE7jCCBOowggTmFg1pbWFnZS9zdmcreG1sMCMwITAJBgUrDgMC
GgUABBTqjIHaYzxmoWJiE0p4V2zfBnY46TCCBK4WggSqZGF0YTppbWFnZS9zdmcr
eG1sO2Jhc2U2NCxINHNJQUFBQUFBQUFDcDFVVFcvYk9CQTl4NytDcTU0S2tEU0hG
TCtNT0VVYkZFMkJibEZnQVY4WHJxTEd3bXB0UTFia3RMOSszMUFKMGtOUVlBc2s1
SGlHbkhuejVsR1hieDcrN2NYVURxZnVzRjlYcEUwbDJuMXp1TzMyZCt2cWZ2eW1V
dlhtYW5INWgxTGlRN3R2aCsxNEdGYmk3ZTNoYXlzKzl2MzlhU3d1WVd1TnkxTDh0
ZmtnM2o4Y0Q4TW92dlQzZCtyalh1amkzTXcxVmlKb1k4UzcrNjYvRmVhMUVFb2gv
V202K3htRXJjVFg3YW45TWh5K2RYMjdyc1p1LzEwZFQ1WG9idGZWcCszM2R2aWJL
Z0hrKzlPNjJvM2pjYlZjbnM5bmZYYjZNTnd0clRGbWlaU1BSMVlQZmJmLzU2V0Rs
SE5lbG1pMW1McjIvTzd3c0s2TU1DS0hxUDI4bGlTcjAzSGJBTWh4YUUvdE1MVVZN
SS9kMkxkWDE1OC9YeTVuYzNGNTNJNDdBY2o5dW5wMWZRMFVZQk9JLzR3eGF5K2Rk
ZHBPTnBDdUd5UEo2Rm9GdUNsclVoUzBsWlowYWhUcExJMzJ5dW1FUFNtdkkrK05p
cnFXUnBHSHorbW9yRlVFMGorRldNczZCWjBYRjcxQ2NxZkllRDd2VUlBUEoyM3g3
eFZaM0lyYWNTUkxRbkZjMUtIc2ViS0dxLzhDVjlKT1dxU0NQeXVEbUkzSVIwNDdM
dXh4VUdXZ0FJYkFmdkl2SWtnemdscVNaZ1FCQ1BJakFzb2NBZ0owQUk0a0pGVXJY
b3AxWXhNS05adzlNUTNXNEtLdkdXTXhlUVgrT2Y3c2w4VS8yenVnM25rN0taZDEy
Q2x2MFZRQ0JCejFHU3hIcHBWOGdrbkJjbDhOeVJUQlFDUW14RWVPekhGZWR3VE9i
T1BRbWpReVlMb1lDbXhZNkEwUUpwVnhFUzBoV25NRzlHVWxxd29tQ0d0cWtpRXpB
a284RENDZFRTYVpGaGU0SHpYOTd2MU5FZHhORWQrUGF2a0xmVkwwR0c2ZDBIOERq
aVRZWUUyaEZwbkk2VTNwbkZmMGJGQTQ0RHdtQlRncXpCc3ZHNWNzNnhCallCbkp3
RE1IRXNsZ1ZaRVJWS1FzSjg1RjVpUnRTZTE0OHF4di9IQTFRMGlJbDFsQ1NlREFN
dXMxNC9MOElIS1FyRGpzT3FHZVl6L09NbVNlQmd0TnNwckFCSkpCWjZybWZHQW16
eUJmZ2toUEVPMFR4UFFFTWNoWjZNOGdqZlNFblYrem5CL2NzNnM0TE1lWU95VCtu
d0I1Ymp0V2FaeUkyK1c2MER4clFORk1laXdtYTBEVkVCdEVvT2JKMTBYdVJRL2Mx
L1Nrb04rN3pRK0RTdHNZQ1Y1WlVhSGhaMktnQUNyZkpZNFpyTGFzME1WT2hSSnd6
QnNLNDN0VlBpb0pmNmRIVTVaZk44NzVqWXZsTVM0dXJpMGFEeEsvZVI3UUpFU0lw
Ky9rVC9xY2hjeGY5NnZGZjJ5VHVidTlCZ0FBMIGLBgorBgEEAdZ5AgQCBH0EewB5
AHcAVVlTrjCWAIBs0utSCKbJnpMYKKwQVrRCHFU2FUxfdawAAAGgSGMMygAABAMA
SDBGAiEAlJdKzRd67dcx2hlBC2RAcJRjdrR8XUyZyenMZ/TtsboCIQCsiYcIGQQ6
SVZG6GkOmLwf3MeyEaHSj5QjDNx4qyU9xTANBgkqhkiG9w0BAQsFAAOCAgEAVzEq
16pm/ip7w7G/qya9tB2uyv+dvbq+W265tquwubh0KwwoG5aM0xhWZVDV51sR+DM2
ZEvHVhubgsuXUp5K5XisXxQVgTkjFsZ3WL3CFQ/uZbK6SXEMpO188ER/qKvo3jnj
JPTX8GmAnyJwPESinbDzj/jHFPZp0aey1R5LxqyDCOvgs3KO28+M0rOQTyZQYjFe
HIwRFBfF/lF9ioQcQlAxafwbl44pW/Ticc4gSAT/7EvPT2mVKNfcC1IXVPG7nUvH
2RiNpDZmWwFxbnbK3PGfks+b6Br9QI6DXSOropIzXH+yunwk+SCDaqi46cEAUPOH
m9coCPVd5Sf9VhI3YnbiL0o2aGPk2KaFii96t+z68ibscdw42PPbjESwGeEHRT3B
hKoS5IRUYyCCBf6lEdg2n43z7J5L3VyiWK5ezhdnJ1eoLFNVMIxzjNqGJuanVaXc
Vmu4XSwxqVqidR8RVoW5fWVVqKOndvFtO97I7ZOlz638DqiersMASMBEMnczajvN
lyw1GdkEEM9LADwDq/1IyionsTGD3mm4YbWkZTlzMxiyLtzwDnleXssG4Fj5Ep2x
Dzmzajf8EiI1A6FXeTBkTsWRJhoqkdQGGpn9mcJzHugofCEkFG71b9Tf4jwYbn49
iOgI0ojsOWpNIVstxRBNW91bftm5FaDFQR6iOMo=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIHCzCCBPOgAwIBAgIQDFtvfuuz8cKs3aE6mJ29gjANBgkqhkiG9w0BAQsFADCB
iDELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxDTALBgNVBAcTBExlaGkxFzAV
BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
MScwJQYDVQQDEx5EaWdpQ2VydCBWZXJpZmllZCBNYXJrIFJvb3QgQ0EwHhcNMjEw
NzAxMDAwMDAwWhcNMzYwNjMwMjM1OTU5WjBfMQswCQYDVQQGEwJVUzEXMBUGA1UE
ChMORGlnaUNlcnQsIEluYy4xNzA1BgNVBAMTLkRpZ2lDZXJ0IFZlcmlmaWVkIE1h
cmsgUlNBNDA5NiBTSEEyNTYgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQDcL2QJnDBab/pzEf3Yoi6mAXmKD34MOgqMouGLTT4CvWSugTi/
xjPPbFTf2zJG4Pgx34YaIGYzvnTSgpscpNf+sstiAPDrEPwuWk/ZQmo4E47crnOV
hHTHltOc8XRip1UwCPzOB2hwmnZwFTPP6kS6hp8ao9P5qa40Up03sh1QhhxvhVTX
f6DsxSjMVpCz2BICa56mW7lPeEf5BgqqdaeUoEvYdw4mw+37V5vugUF8pH1lus8J
f97DYPCC8uf14Rww8NXhOeLLyUylqP9t0WS5nlwPjiobJAm/VdhgrOzbAuUyU5uJ
2bmT4btp6aHNvmOesHvhES96fWdjsu2jdVQTa9e87PK+VzUiSdDDNHwtY/maUG0y
9AYThU9PaKEkGifmF1+88dzN+IeTsy469ikQ5nWEs7lEiW8omO2aqDcY2BYRfcUD
aP4EX5RdVwLlIf3WgsVVejgZ0adlqB+imip2DHFHD7xnCA+akDfRALgdckNtxGic
MxWCuttmuQGsf7+3OexM0qyMo3F+yvr+Hr8zrV9FjJn8R1crABvouX+9m24hj2Ql
I3OGAXSj3rrLMXZZmFeAb1iTPqpfkDAJb5tpMTqWWbEas2L76aoztFt9n4kZQfc3
DDx4L4HxUkF+GBsi/pqp50Op2taInqnTkl6cZt1JFaXJyIv/zZrXgTwN1QIDAQAB
o4IBlzCCAZMwHQYDVR0OBBYEFL6fvY1XbZW1rWPDl06rqIRdOgf1MB8GA1UdIwQY
MBaAFOxvIqSzBOLBY4fmd2PqRmlO7vzrMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUE
DDAKBggrBgEFBQcDHzASBgNVHRMBAf8ECDAGAQH/AgEAMHwGCCsGAQUFBwEBBHAw
bjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEYGCCsGAQUF
BzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRWZXJpZmll
ZE1hcmtSb290Q0EuY3J0MEgGA1UdHwRBMD8wPaA7oDmGN2h0dHA6Ly9jcmwzLmRp
Z2ljZXJ0LmNvbS9EaWdpQ2VydFZlcmlmaWVkTWFya1Jvb3RDQS5jcmwwUAYDVR0g
BEkwRzA3BgpghkgBhv1sAAIFMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGln
aWNlcnQuY29tL0NQUzAMBgorBgEEAYOeXwEBMA0GCSqGSIb3DQEBCwUAA4ICAQCq
g+EBenB6355D4oN3WUj741u8a/McUbAWAh2jvBq9beUBpeortI0zhwo2jXQLIDjt
lJdtNwcZWmmV6ouc6cp3ZqGnk0HRnD1HNASCQ33hTw5UaqG0tuEUOq8VKNnNmVBk
XNGb66PVw6RFjQH4/WKDBqiglhiI23f5cpDalfSXX4RVJZ3XLGPVSMmq2TOHmQWF
+h1NR6pVyDdXJoouUK8WOkIksYyjlxMatVX0QasX8tepHizWBMZC0HMg3Q2gr0h4
HKCducumhbuuRnSwpYhG+OU+QAITKK29FicuGMx1i+Cg1PyQEIdmq41LgzLendaR
tewhUuFfm4FYG/b3sSklg+vesw8CbDE3gXQR7Y/b1M1jiZ6vnlnP/q72ZRUB5ApN
kA/h9fkoJNR8+R42BDfJ7o9Qu9EWnMeshNB26EIUpM20FdYfOhtWI6IQQ9kmnLwk
6yzD2nolhwuSEp32hK9X5q/jPfXstjOsQCkUkRoGkGBkSERU2xjaWhOYYJbrmg4c
1Qtqcr65mc9FyqYckZ0LdLMjd8SPQn/jQPLrQ+5PYcfI/GEZIZ4dqZ73enWRxMKq
1d/v891yZBQrmsa+f/H1uHAHmGfMzwcA6WWlXZYnjJD0qPLpUzyBwFoGd9YIy0/I
saBEzyI2AsX9Eal3wrjflZOyMzE7zYKV5YEtRDDBWA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
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
0wberd/tg3N0dJsSSznZjwYB
-----END CERTIFICATE-----
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.2" baseProfile="tiny-ps" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 967.5 967.5" xml:space="preserve">
<title>CNN</title>
<path fill="#CC0000" d="M779.5,323.2v261.4c0,10.4-6.5,19.1-16.2,21.8c-1.9,0.5-3.8,0.8-5.7,0.8c-7.4,0-15.8-3.7-22-14.1L674,486.9
l-61.3-105.8c-3.4-5.8-8.2-8.5-12.7-7.3c-3.9,1-6.4,4.6-6.4,9v201.8c0,10.4-6.5,19.1-16.2,21.8c-8.3,2.2-19.9-0.1-27.7-13.3
l-56.2-96.9l-66.7-115c-3.4-5.8-8.2-8.5-12.8-7.3c-4,1.1-6.6,4.9-6.4,9v197.3c0,13-11.4,24.4-24.4,24.4H283
c-66.8,0-120.9-54.1-120.9-120.8c0-66.8,54.1-120.9,120.8-120.9h0.1h52v-39.6h-52c-88.6,1-159.7,73.7-158.7,162.3
c1,87.2,71.5,157.7,158.7,158.7h101.2c38.5,0,63.2-22.5,63.1-64.1v-91.5c0,0,64.5,111.2,67.5,116.2c41,69.6,118.4,40.8,118.4-19.1
v-97.1c0,0,64.5,111.2,67.5,116.2c41,69.6,118.4,40.8,118.4-19.1V323.2H779.5z"/>
<path fill="#CC0000" d="M175.3,483.7c0.1,59.4,48.2,107.6,107.7,107.7h100.2c6.3,0,11.2-6,11.2-11.2V382.9
c0-10.4,6.5-19.1,16.2-21.8c8.3-2.2,19.9,0.1,27.7,13.3c0.4,0.7,34.1,58.9,66.8,115.1c28.7,49.4,55.7,96,56.2,96.8
c3.4,5.8,8.2,8.5,12.8,7.3c4-1.1,6.6-4.9,6.4-9V382.9c0-10.4,6.5-19.1,16.1-21.8c8.2-2.2,19.8,0.1,27.6,13.3
c0.4,0.7,30,51.7,61.4,105.8c30,51.7,61,105.2,61.6,106.1c3.4,5.8,8.2,8.5,12.8,7.3c4-1.1,6.6-4.9,6.4-9V323.2h-39.7v155.7
c0,0-64.5-111.2-67.5-116.2c-41-69.6-118.4-40.8-118.4,19.1v97.1c0,0-64.5-111.2-67.5-116.2c-41-69.6-118.4-40.8-118.4,19.1v159.1
c0.1,5.9-4.5,10.7-10.3,10.8c-0.1,0-0.2,0-0.3,0h-60.8c-37.6,0-68-30.4-68-68s30.4-68,68-68H335V376h-52
C223.6,376.1,175.4,424.3,175.3,483.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,145 @@
-----BEGIN CERTIFICATE-----
MIIMxTCCCq2gAwIBAgIQCAlX4tQpnQMqdL+3VMj70zANBgkqhkiG9w0BAQsFADBf
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xNzA1BgNVBAMT
LkRpZ2lDZXJ0IFZlcmlmaWVkIE1hcmsgUlNBNDA5NiBTSEEyNTYgMjAyMSBDQTEw
HhcNMjYwNzMxMDAwMDAwWhcNMjcwNzMwMjM1OTU5WjCCASwxEzARBgsrBgEEAYI3
PAIBAxMCVVMxGTAXBgsrBgEEAYI3PAIBAhMIRGVsYXdhcmUxHTAbBgNVBA8TFFBy
aXZhdGUgT3JnYW5pemF0aW9uMRAwDgYDVQQFEwcyODcxMzUyMQswCQYDVQQGEwJV
UzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEBxMIU2FuIEpvc2UxHDAaBgNV
BAkTEzIxMDUgRSBIYW1pbHRvbiBBdmUxEzARBgNVBAoTCmVCYXksIEluYy4xEzAR
BgNVBAMTCmVCYXksIEluYy4xHzAdBgorBgEEAYOeXwENEw9SZWdpc3RlcmVkIE1h
cmsxEjAQBgorBgEEAYOeXwEDEwJVUzEXMBUGCisGAQQBg55fAQQTBzQ0MDg0MjMw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCavWp79EddD6sGELSbUndP
YMMfMReYDQip95UcIv5oftvFnaKEleeFbII2cHhyBV2qpo0Owb8b5cuWXU2R+oiM
MDuxcIf63oUC594zqVW4uwcTu5c3rdxkag2sO134SKRMlvw4/cZdd3mX4t0N+818
jtU79DtIz7qN/R35BpGgmg+Z7VZNIHZZDlcWqvM3KdyAfg6lpvJDBt5acUYIAyqn
2639CmFpREYl/LykqQJzpmx0JEXPruBjAjRBbsMcWH3qvlBSNAbheljN/5vs0x8T
kkpyegPf6yvWPfWFvhf59PHK9srFnokvENcC06fDs8KtW2ziN18CgZYnk4aMvBrx
AgMBAAGjggesMIIHqDAfBgNVHSMEGDAWgBS+n72NV22Vta1jw5dOq6iEXToH9TAd
BgNVHQ4EFgQUDTLqc07pn6IOX44v66+0NfysV6MwgaAGA1UdEQSBmDCBlYIKZWJh
eS5jby51a4IHZWJheS5kZYIIZWJheS5jb22CEW5vcmVwbHkxLmViYXkuY29tgg1y
ZXBseS5lYmF5LmVzghFyZXBseS5lYmF5LmNvbS5hdYINcmVwbHkuZWJheS5pdIIQ
cmVwbHkuZWJheS5jby51a4INcmVwbHkuZWJheS5kZYIPcmVwbHk1LmViYXkuY29t
MFAGA1UdIARJMEcwNwYKYIZIAYb9bAACBTApMCcGCCsGAQUFBwIBFhtodHRwOi8v
d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDAYKKwYBBAGDnl8BATATBgNVHSUEDDAKBggr
BgEFBQcDHzCBpQYDVR0fBIGdMIGaMEugSaBHhkVodHRwOi8vY3JsMy5kaWdpY2Vy
dC5jb20vRGlnaUNlcnRWZXJpZmllZE1hcmtSU0E0MDk2U0hBMjU2MjAyMUNBMS5j
cmwwS6BJoEeGRWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFZlcmlm
aWVkTWFya1JTQTQwOTZTSEEyNTYyMDIxQ0ExLmNybDBkBggrBgEFBQcBAQRYMFYw
VAYIKwYBBQUHMAKGSGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
dFZlcmlmaWVkTWFya1JTQTQwOTZTSEEyNTYyMDIxQ0ExLmNydDAMBgNVHRMBAf8E
AjAAMIIEsAYIKwYBBQUHAQwEggSiMIIEnqKCBJqgggSWMIIEkjCCBI4wggSKFg1p
bWFnZS9zdmcreG1sMCMwITAJBgUrDgMCGgUABBQDJLNL8Giz899rSPVMpJc6EIYs
DDCCBFIWggROZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxINHNJQUFBQUFBQUFD
blZVVFkvYk9BeTk5MWNZM3F1a2lQcXc1VUV5ZzUxdUIzT1lBZ1cyM1h1cThjYkd1
a2tRRzBuYlg5OUhxczBNc0dnQVI4K1VTRDQrVWw3ZmZmMHlWZWYrTkkrSC9hWW1Z
K3VxMytmRDg3amZiZXBQSHg5MHFxdDUyZTZmdDlOaDMyL3FiLzFjMzkyK1djL24z
V3MzVjFlZnQzUC80WFQ0ZDV4d2JCbjMzL1J4cnF2ejJGL3VEMTgzdGExczVhamhw
NjZROUdZK2JqTk9Iay85M0ovT3ZSajM4NlllbHVWNHMxcGRMaGR6OGVadzJxMmN0
WGFGaFBYdG02cGFMK015OWJlZnh5K2poazFMSm5KSXRsNlZMVDYwcThiblRYMy9Y
L0dCNGRUbnBRSzNhVlAvOFNDL3VycU16OHV3cVlYUjBJKzdZU2t2S3dteDJyMks5
SFRZSFg2RjJwVVY2TGhkaG12UStPZURoUXc0L0o1OE1LU0lyUEZaSjlNcUFFMWlN
MTBCcldteVZSNTdIcmhWT0JzWlpBQlNWbEUwUVVmWUJNQWxualZ4MHJjVW5Ba1Mz
Q255NllxdUtiOVg3eW1LcTNQR0lZc3pTWEZVdG1GZnBRR2t1cXpocDUxbWJ2Q1Z0
Y21BQ1FmQjBwcUdXV3BLd3F6aDVGa1RjVEEya203Z0w0QzhpZGxxcVNHeEMzTnhN
QXJ4WEtJVHRscFp5V0VIQUJYQnEwWEtxRHRPNlVDaFkyYXhKR0hxb1lSdFRHUUNB
NzlSSms3S3BSQVZ2MDQ1NHFOQUdiWUFCUjBwb1lGOGphRi9SSXluVjhKOEwzMytY
eVBmUmU5OEtvMk14SFJ0aEl3Z0hIU0htbFBoaVpvTHlycGxYZGtRbEJpRHBERCtr
ZDNSRFdSRGcwR3JhRmhVYXJra3RBM1VpVFZubENQUENzOUMwbzZyWTZGYUV3YUk4
amJpakV2SUVSRE1vMjdsMFpVQzBaZVd4MGE3QUkwN01CV0F4RlJTZ2hSNmlLZ2Vz
NEs5Z2pJM2pxWHkwdUxTTXdXdmVNYno5TUw3RmZ5ZGJOWTI0YStmODUrNGYrUWl4
TUdneVRSRHVNQUw5MkxHQ3duU1Z3dHhpd012L1A1MzI3QXVIRUc5QklPVURUNDFx
dW1La3BDbE9hTVl5bHhzWkRHd0RjRndIWGh5VzY3UWlxVGd6OVBzQ3NCZndyMGdu
cllnOVdvcGtCSGFXWm9KR3c5d0NSU0tqTXJKQUFlK0xIeExIaHRNTUZKNnVlR04z
RE1aNGFjcjBSZjBPK1ZTYzUrb0xjbzVHN2hrWEtwSnV3NlVZakplaHI2ZGtoR3gz
TVJrY1dmUjNJRUxub2kxOVJoOVFaclJvd1M2WnZ6MU1lTmx6Wi9TMngvaHVqVmcr
UVVBQUE9PTCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AFVZU64wlgCAbNLrUgimyZ6T
GCisEFa0QhxVNhVMX3WsAAABn7gGRosAAAQDAEgwRgIhAIuPenLVXIsZEtLGDOIA
BQTuWMt6kA44mnTJtKY54ZmdAiEAn7uNidm55MgI8mhbnAYC1SZ0I7BsOD9NIFgm
eoCGfs4wDQYJKoZIhvcNAQELBQADggIBAL8vksiwFDsEapsHi3pw8FV8lLH1WpLi
+M7u3ZsDjMDN7cSKAoWbeqH/eQ1dds7M+WCl6fjMgLdURiAyB1dDOxP0tyO+q4Io
fCjraNKrocRRzj4SADx4Ry558EuHIc43W4GebFSJFLp2mCB9HkRJ3pJAGvaoz6t9
PLGmyzQN2BOdZkrsWgiYMaftrCnrqeVyQ3v57KiE+oPMlH8EtOnGgTXHLDRnDmy/
AUxTc+irAzOE2GEExJhGiWvAWmiA/2jEen1iP61V+PPRfrC7Md/Q+Z67xj9VsS6U
6Cz+HDrZe+DEcAi0op9qFzt9wCvYTgyIafHXFobyvrFbYo3MYSLsT6qn4Svk3C3m
1KdCQkjFbBDQvOSPAQrzStdyGwrTJMXXFgUE2wN3MtSovemL0f4+/CSW2CthMVUo
0UZOguZ5KplodbJAkZcdCAQVqkp3dR/f3nwhhixbY+N/KmqLwPRcn+TAVnP8AbKQ
acny9E8BryF+b1dOkIfzvyWjLVyAXZVTAtXIFkiJiWFo6WT0uyHRDBT0cwGHYeik
KT0pI6lcsL5ScUlAVnuG3dm8oT0K+H1oNy4xf7IKAyd6BlcZVD2E1MMicf7ikn1v
RUwwrQsPGq09+JE7wlthiJijWXlGNCzELu6oO6ztyqQ9U6dLdUjvPxdQBLSu3Dnj
RjlGzqYTaTCs
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIHCzCCBPOgAwIBAgIQDFtvfuuz8cKs3aE6mJ29gjANBgkqhkiG9w0BAQsFADCB
iDELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxDTALBgNVBAcTBExlaGkxFzAV
BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
MScwJQYDVQQDEx5EaWdpQ2VydCBWZXJpZmllZCBNYXJrIFJvb3QgQ0EwHhcNMjEw
NzAxMDAwMDAwWhcNMzYwNjMwMjM1OTU5WjBfMQswCQYDVQQGEwJVUzEXMBUGA1UE
ChMORGlnaUNlcnQsIEluYy4xNzA1BgNVBAMTLkRpZ2lDZXJ0IFZlcmlmaWVkIE1h
cmsgUlNBNDA5NiBTSEEyNTYgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQDcL2QJnDBab/pzEf3Yoi6mAXmKD34MOgqMouGLTT4CvWSugTi/
xjPPbFTf2zJG4Pgx34YaIGYzvnTSgpscpNf+sstiAPDrEPwuWk/ZQmo4E47crnOV
hHTHltOc8XRip1UwCPzOB2hwmnZwFTPP6kS6hp8ao9P5qa40Up03sh1QhhxvhVTX
f6DsxSjMVpCz2BICa56mW7lPeEf5BgqqdaeUoEvYdw4mw+37V5vugUF8pH1lus8J
f97DYPCC8uf14Rww8NXhOeLLyUylqP9t0WS5nlwPjiobJAm/VdhgrOzbAuUyU5uJ
2bmT4btp6aHNvmOesHvhES96fWdjsu2jdVQTa9e87PK+VzUiSdDDNHwtY/maUG0y
9AYThU9PaKEkGifmF1+88dzN+IeTsy469ikQ5nWEs7lEiW8omO2aqDcY2BYRfcUD
aP4EX5RdVwLlIf3WgsVVejgZ0adlqB+imip2DHFHD7xnCA+akDfRALgdckNtxGic
MxWCuttmuQGsf7+3OexM0qyMo3F+yvr+Hr8zrV9FjJn8R1crABvouX+9m24hj2Ql
I3OGAXSj3rrLMXZZmFeAb1iTPqpfkDAJb5tpMTqWWbEas2L76aoztFt9n4kZQfc3
DDx4L4HxUkF+GBsi/pqp50Op2taInqnTkl6cZt1JFaXJyIv/zZrXgTwN1QIDAQAB
o4IBlzCCAZMwHQYDVR0OBBYEFL6fvY1XbZW1rWPDl06rqIRdOgf1MB8GA1UdIwQY
MBaAFOxvIqSzBOLBY4fmd2PqRmlO7vzrMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUE
DDAKBggrBgEFBQcDHzASBgNVHRMBAf8ECDAGAQH/AgEAMHwGCCsGAQUFBwEBBHAw
bjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEYGCCsGAQUF
BzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRWZXJpZmll
ZE1hcmtSb290Q0EuY3J0MEgGA1UdHwRBMD8wPaA7oDmGN2h0dHA6Ly9jcmwzLmRp
Z2ljZXJ0LmNvbS9EaWdpQ2VydFZlcmlmaWVkTWFya1Jvb3RDQS5jcmwwUAYDVR0g
BEkwRzA3BgpghkgBhv1sAAIFMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGln
aWNlcnQuY29tL0NQUzAMBgorBgEEAYOeXwEBMA0GCSqGSIb3DQEBCwUAA4ICAQCq
g+EBenB6355D4oN3WUj741u8a/McUbAWAh2jvBq9beUBpeortI0zhwo2jXQLIDjt
lJdtNwcZWmmV6ouc6cp3ZqGnk0HRnD1HNASCQ33hTw5UaqG0tuEUOq8VKNnNmVBk
XNGb66PVw6RFjQH4/WKDBqiglhiI23f5cpDalfSXX4RVJZ3XLGPVSMmq2TOHmQWF
+h1NR6pVyDdXJoouUK8WOkIksYyjlxMatVX0QasX8tepHizWBMZC0HMg3Q2gr0h4
HKCducumhbuuRnSwpYhG+OU+QAITKK29FicuGMx1i+Cg1PyQEIdmq41LgzLendaR
tewhUuFfm4FYG/b3sSklg+vesw8CbDE3gXQR7Y/b1M1jiZ6vnlnP/q72ZRUB5ApN
kA/h9fkoJNR8+R42BDfJ7o9Qu9EWnMeshNB26EIUpM20FdYfOhtWI6IQQ9kmnLwk
6yzD2nolhwuSEp32hK9X5q/jPfXstjOsQCkUkRoGkGBkSERU2xjaWhOYYJbrmg4c
1Qtqcr65mc9FyqYckZ0LdLMjd8SPQn/jQPLrQ+5PYcfI/GEZIZ4dqZ73enWRxMKq
1d/v891yZBQrmsa+f/H1uHAHmGfMzwcA6WWlXZYnjJD0qPLpUzyBwFoGd9YIy0/I
saBEzyI2AsX9Eal3wrjflZOyMzE7zYKV5YEtRDDBWA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
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
0wberd/tg3N0dJsSSznZjwYB
-----END CERTIFICATE-----
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg version="1.2" baseProfile="tiny-ps" viewBox="0 0 216 216" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<title>bimi-svg-tiny-12-ps</title>
<g id="Bkg">
<rect fill="#FFFFFF" width="216" height="216"/>
</g>
<g id="Logo">
<g>
<path fill="#F5AF02" d="M134.1,110.3c-8.7,0.3-14.1,1.9-14.1,7.6c0,3.7,3,7.7,10.5,7.7c10.1,0,15.4-5.5,15.4-14.5v-1 C142.4,110.2,138,110.2,134.1,110.3z M155.5,122.2c0,2.8,0.1,5.5,0.3,8h-8.9c-0.2-2-0.3-4.1-0.3-6c-4.8,5.9-10.6,7.6-18.5,7.6 c-11.8,0-18.1-6.2-18.1-13.5c0-10.5,8.6-14.1,23.5-14.5c4.1-0.1,8.7-0.1,12.5-0.1v-1c0-7-4.5-9.9-12.3-9.9c-5.8,0-10,2.4-10.5,6.5 h-10.1c1.1-10.3,11.9-12.9,21.4-12.9c11.4,0,21,4.1,21,16.1V122.2L155.5,122.2z"/>
<path fill="#E53238" d="M51.1,105.2c-0.4-9-6.8-12.3-13.8-12.3c-7.5,0-13.4,3.8-14.5,12.3H51.1z M22.7,111.6 c0.5,8.7,6.5,13.9,14.8,13.9c5.7,0,10.8-2.3,12.5-7.4h9.9C58,128.4,47,131.9,37.6,131.9c-17.1,0-24.7-9.4-24.7-22.1 c0-14,7.8-23.2,24.8-23.2c13.5,0,23.5,7.1,23.5,22.5v2.5L22.7,111.6L22.7,111.6z"/>
<path fill="#0064D2" d="M85.8,125.3c8.9,0,15-6.4,15-16.1s-6.1-16.1-15-16.1s-15,6.4-15,16.1S76.9,125.3,85.8,125.3z M61.2,69.6 h9.6v24.1c4.7-5.6,11.2-7.2,17.6-7.2c10.7,0,22.6,7.2,22.6,22.8c0,13-9.4,22.5-22.7,22.5c-7,0-13.5-2.5-17.6-7.4 c0,2-0.1,4-0.3,5.9H61c0.2-3,0.3-6.8,0.3-9.9L61.2,69.6L61.2,69.6z"/>
<path fill="#86B817" d="M204.3,88.1l-29.7,58.3h-10.7l8.5-16.2l-22.3-42.1h11.2l16.4,32.9l16.4-32.9H204.3z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,268 @@
package bimi
import (
"bytes"
"compress/gzip"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"strings"
"time"
)
// A BIMI certificate is not something this service can make. A Verified Mark
// Certificate (VMC) or Common Mark Certificate (CMC) is issued by a certificate
// authority after it has checked the organisation, and, for a VMC, a registered
// trademark. What the service can do is hold the certificate the authority
// returns, serve it where the DNS record points, and check it against the logo
// before anything is published, because a certificate whose embedded logo is
// not the logo at the "l=" URL makes the receiver ignore BIMI without a word.
var (
// id-kp-BrandIndicatorforMessageIdentification (RFC 9495 / BIMI draft).
oidBrandIndicatorEKU = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 31}
// id-pe-logotype (RFC 3709): where the logo is embedded.
oidLogotype = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 12}
// The mark type the issuer records in the subject: "Registered Mark" for a
// VMC, "Prior Use Mark" for a CMC.
oidMarkType = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 53087, 1, 13}
)
// MaxCertificateBytes bounds what is accepted. Real chains are about 10 KB.
const MaxCertificateBytes = 64 * 1024
// Kind is what sort of mark certificate this is.
type Kind string
const (
KindVMC Kind = "vmc"
KindCMC Kind = "cmc"
KindUnknown Kind = "unknown"
)
// Certificate describes a parsed BIMI certificate chain for the console.
type Certificate struct {
Subject string
Organization string
Issuer string
NotBefore time.Time
NotAfter time.Time
DNSNames []string
MarkType string
Kind Kind
ChainLength int
// MatchesLogo is true when the logo embedded in the certificate is, byte
// for byte, the logo being published.
MatchesLogo bool
Warnings []string
}
// InspectCertificate parses a PEM chain and reports every reason it would not
// work for domain and logo. A nil Certificate means nothing could be read.
//
// All reasons at once, for the same reason Validate reports all of its own.
func InspectCertificate(pemData []byte, domain string, logo []byte, now time.Time) (*Certificate, []Violation) {
var out []Violation
add := func(rule, detail string) { out = append(out, Violation{Rule: rule, Detail: detail}) }
if len(pemData) == 0 {
return nil, []Violation{{Rule: "empty", Detail: "the file is empty"}}
}
if len(pemData) > MaxCertificateBytes {
return nil, []Violation{{Rule: "size", Detail: fmt.Sprintf("%d bytes; the limit is %d", len(pemData), MaxCertificateBytes)}}
}
var certs []*x509.Certificate
rest := pemData
for {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}
// Anything but a certificate is refused outright. A private key pasted
// here by mistake would be served to the whole Internet from a public URL.
if block.Type != "CERTIFICATE" {
return nil, []Violation{{Rule: "pemBlock", Detail: fmt.Sprintf("found a %q block; only CERTIFICATE blocks are accepted, and never a private key", block.Type)}}
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, []Violation{{Rule: "certificate", Detail: "a certificate in the file could not be parsed"}}
}
certs = append(certs, cert)
}
if len(certs) == 0 {
return nil, []Violation{{Rule: "pem", Detail: "no PEM certificate found"}}
}
leaf := certs[0]
info := &Certificate{
Subject: leaf.Subject.CommonName,
Issuer: leaf.Issuer.CommonName,
NotBefore: leaf.NotBefore,
NotAfter: leaf.NotAfter,
DNSNames: leaf.DNSNames,
ChainLength: len(certs),
Kind: KindUnknown,
}
if len(leaf.Subject.Organization) > 0 {
info.Organization = leaf.Subject.Organization[0]
}
info.MarkType = markType(leaf)
switch {
case strings.Contains(strings.ToLower(info.MarkType), "registered"):
info.Kind = KindVMC
case strings.Contains(strings.ToLower(info.MarkType), "prior use"):
info.Kind = KindCMC
}
if !hasBrandIndicatorEKU(leaf) {
add("extendedKeyUsage", "the first certificate is not a BIMI certificate (no Brand Indicator for Message Identification usage); the leaf certificate must come first")
}
switch {
case now.Before(leaf.NotBefore):
add("validity", fmt.Sprintf("not valid until %s", leaf.NotBefore.UTC().Format("2006-01-02")))
case now.After(leaf.NotAfter):
add("validity", fmt.Sprintf("expired on %s", leaf.NotAfter.UTC().Format("2006-01-02")))
case leaf.NotAfter.Sub(now) < 30*24*time.Hour:
info.Warnings = append(info.Warnings, fmt.Sprintf("expires on %s: ask the authority for a renewal", leaf.NotAfter.UTC().Format("2006-01-02")))
}
if !coversDomain(leaf, domain) {
add("domain", fmt.Sprintf("the certificate does not cover %s (it covers %s)", domain, strings.Join(leaf.DNSNames, ", ")))
}
embedded, err := embeddedLogo(leaf)
switch {
case err != nil:
add("logotype", err.Error())
case !bytes.Equal(embedded, logo):
add("logoMismatch", "the logo inside the certificate is not the logo published for this domain; upload exactly the SVG the certificate was issued for")
default:
info.MatchesLogo = true
}
if len(certs) == 1 {
info.Warnings = append(info.Warnings, "the file holds only the leaf certificate; Gmail and Apple Mail expect the intermediate certificates after it, as the authority delivers them")
} else {
for i := 0; i+1 < len(certs); i++ {
if err := certs[i].CheckSignatureFrom(certs[i+1]); err != nil {
info.Warnings = append(info.Warnings, fmt.Sprintf("certificate %d was not signed by certificate %d: the chain is out of order or incomplete", i+1, i+2))
break
}
}
}
return info, out
}
func hasBrandIndicatorEKU(cert *x509.Certificate) bool {
for _, usage := range cert.UnknownExtKeyUsage {
if usage.Equal(oidBrandIndicatorEKU) {
return true
}
}
return false
}
// markType reads the issuer's mark-type attribute from the subject.
func markType(cert *x509.Certificate) string {
for _, name := range cert.Subject.Names {
if name.Type.Equal(oidMarkType) {
if value, ok := name.Value.(string); ok {
return value
}
}
}
return ""
}
// coversDomain applies the rule receivers use: the certificate must name the
// sender's domain, or the organisational domain above it ("ebay.com" covers
// "reply.ebay.com"), exactly or through a wildcard.
func coversDomain(cert *x509.Certificate, domain string) bool {
domain = strings.ToLower(strings.TrimSuffix(domain, "."))
for _, name := range cert.DNSNames {
name = strings.ToLower(name)
if name == domain || strings.HasSuffix(domain, "."+strings.TrimPrefix(name, "*.")) {
return true
}
}
return false
}
// embeddedLogo returns the SVG carried in the certificate's logotype extension.
//
// The extension is a nest of ASN.1 structures, but the logo inside is a single
// IA5String holding a data URI, so it is found by that, not by walking the
// structure: "data:image/svg+xml;base64," followed by a gzipped SVG. The type
// says svg+xml and the payload is gzip, which is what issuers actually produce.
func embeddedLogo(cert *x509.Certificate) ([]byte, error) {
var ext []byte
for _, e := range cert.Extensions {
if e.Id.Equal(oidLogotype) {
ext = e.Value
break
}
}
if ext == nil {
return nil, fmt.Errorf("the certificate has no embedded logo (logotype extension)")
}
marker := []byte("data:image/svg+xml")
at := bytes.Index(ext, marker)
if at < 0 {
return nil, fmt.Errorf("the embedded logo is not an SVG data URI")
}
uri, err := ia5StringAt(ext, at)
if err != nil {
return nil, err
}
_, payload, found := strings.Cut(uri, ",")
if !found {
return nil, fmt.Errorf("the embedded logo is malformed")
}
raw, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return nil, fmt.Errorf("the embedded logo is not valid base64")
}
if len(raw) >= 2 && raw[0] == 0x1f && raw[1] == 0x8b {
reader, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil {
return nil, fmt.Errorf("the embedded logo is not valid gzip")
}
// A decompression bomb inside a certificate is still a bomb
return io.ReadAll(io.LimitReader(reader, 4*MaxLogoBytes))
}
return raw, nil
}
// ia5StringAt returns the ASN.1 IA5String whose content begins at offset, by
// reading the tag and length just before it.
func ia5StringAt(der []byte, offset int) (string, error) {
for _, back := range []int{2, 3, 4} {
start := offset - back
if start < 0 || der[start] != 0x16 {
continue
}
var length, header int
switch lenByte := der[start+1]; {
case back == 2 && lenByte < 0x80:
length, header = int(lenByte), 2
case back == 3 && lenByte == 0x81:
length, header = int(der[start+2]), 3
case back == 4 && lenByte == 0x82:
length, header = int(der[start+2])<<8|int(der[start+3]), 4
default:
continue
}
if header == back && offset+length <= len(der) {
return string(der[offset : offset+length]), nil
}
}
return "", fmt.Errorf("the embedded logo could not be located in the certificate")
}
@@ -0,0 +1,234 @@
package bimi
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"strings"
"testing"
"time"
)
// The clock is pinned inside both fixtures' validity periods.
var fixtureNow = time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)
func read(t *testing.T, name string) []byte {
t.Helper()
data, err := os.ReadFile("testdata/" + name)
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
return data
}
func rules(violations []Violation) []string {
out := make([]string, 0, len(violations))
for _, v := range violations {
out = append(out, v.Rule)
}
return out
}
// Real certificates, issued by DigiCert and through Valimail. These are the
// formats the parser has to read; a certificate built by hand would only prove
// the parser reads what it was written to read.
func TestInspectRealCertificates(t *testing.T) {
cases := []struct {
file, logo, domain, issuer string
}{
{"ebay.pem", "ebay.svg", "ebay.com", "DigiCert Verified Mark RSA4096 SHA256 2021 CA1"},
{"cnn.pem", "cnn.svg", "cnn.com", ""},
}
for _, c := range cases {
t.Run(c.file, func(t *testing.T) {
cert, violations := InspectCertificate(read(t, c.file), c.domain, read(t, c.logo), fixtureNow)
if len(violations) != 0 {
t.Fatalf("a genuine certificate was refused: %v", violations)
}
if !cert.MatchesLogo {
t.Error("the logo embedded in the certificate should match the published one")
}
if cert.Kind != KindVMC {
t.Errorf("kind = %q, want vmc (both are registered marks)", cert.Kind)
}
if cert.ChainLength < 2 {
t.Errorf("chain length = %d, want the intermediates too", cert.ChainLength)
}
if c.issuer != "" && cert.Issuer != c.issuer {
t.Errorf("issuer = %q", cert.Issuer)
}
if cert.Organization == "" || cert.NotAfter.IsZero() {
t.Errorf("incomplete description: %+v", cert)
}
})
}
}
func TestInspectCoversSubdomainsOfTheCertifiedDomain(t *testing.T) {
// ebay.com's certificate names ebay.com, and mail from reply.ebay.com is covered
_, violations := InspectCertificate(read(t, "ebay.pem"), "reply.ebay.com", read(t, "ebay.svg"), fixtureNow)
if len(violations) != 0 {
t.Fatalf("a subdomain of the certified domain was refused: %v", violations)
}
}
func TestInspectRefusesTheWrongDomain(t *testing.T) {
_, violations := InspectCertificate(read(t, "ebay.pem"), "example.test", read(t, "ebay.svg"), fixtureNow)
if got := rules(violations); len(got) != 1 || got[0] != "domain" {
t.Fatalf("violations = %v, want only the domain", got)
}
}
func TestInspectRefusesADifferentLogo(t *testing.T) {
// The certificate vouches for exactly one SVG. Anything else published at
// l= makes the receiver ignore BIMI, so it is refused here instead.
_, violations := InspectCertificate(read(t, "ebay.pem"), "ebay.com", read(t, "cnn.svg"), fixtureNow)
if got := rules(violations); len(got) != 1 || got[0] != "logoMismatch" {
t.Fatalf("violations = %v, want only the mismatch", got)
}
// Even one byte
altered := append(read(t, "ebay.svg"), '\n')
_, violations = InspectCertificate(read(t, "ebay.pem"), "ebay.com", altered, fixtureNow)
if got := rules(violations); len(got) != 1 || got[0] != "logoMismatch" {
t.Fatalf("a one-byte change must be a mismatch, got %v", got)
}
}
func TestInspectChecksValidity(t *testing.T) {
logo := read(t, "ebay.svg")
_, violations := InspectCertificate(read(t, "ebay.pem"), "ebay.com", logo, time.Date(2028, 1, 1, 0, 0, 0, 0, time.UTC))
if got := rules(violations); len(got) != 1 || got[0] != "validity" {
t.Fatalf("an expired certificate: violations = %v", got)
}
_, violations = InspectCertificate(read(t, "ebay.pem"), "ebay.com", logo, time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
if got := rules(violations); len(got) != 1 || got[0] != "validity" {
t.Fatalf("a certificate not yet valid: violations = %v", got)
}
// Close to expiry is allowed, with a warning
cert, violations := InspectCertificate(read(t, "ebay.pem"), "ebay.com", logo, time.Date(2027, 7, 10, 0, 0, 0, 0, time.UTC))
if len(violations) != 0 || len(cert.Warnings) == 0 {
t.Fatalf("near expiry: violations=%v warnings=%v", violations, cert)
}
}
func TestInspectNeverAcceptsAPrivateKey(t *testing.T) {
// A key pasted next to the certificate would be published at a public URL
key := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: []byte("not a real key")})
for _, payload := range [][]byte{key, append(read(t, "ebay.pem"), key...), append(key, read(t, "ebay.pem")...)} {
cert, violations := InspectCertificate(payload, "ebay.com", read(t, "ebay.svg"), fixtureNow)
if cert != nil || len(violations) != 1 || violations[0].Rule != "pemBlock" {
t.Fatalf("a private key block was not refused: cert=%v violations=%v", cert, violations)
}
if strings.Contains(violations[0].Detail, "not a real key") {
t.Error("the refusal must not echo the key")
}
}
}
func TestInspectRefusesGarbage(t *testing.T) {
cases := map[string][]byte{
"empty": nil,
"text": []byte("this is not a certificate"),
"corrupt": pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte("garbage")}),
"huge": make([]byte, MaxCertificateBytes+1),
}
for name, payload := range cases {
if cert, violations := InspectCertificate(payload, "ebay.com", read(t, "ebay.svg"), fixtureNow); cert != nil || len(violations) == 0 {
t.Errorf("%s: cert=%v violations=%v", name, cert, violations)
}
}
}
// An ordinary web certificate is not a BIMI certificate: it has neither the
// brand-indicator usage nor a logo inside.
func TestInspectRefusesAnOrdinaryCertificate(t *testing.T) {
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "example.test", Organization: []string{"Example"}},
NotBefore: fixtureNow.Add(-time.Hour),
NotAfter: fixtureNow.Add(365 * 24 * time.Hour),
DNSNames: []string{"example.test"},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
payload := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
cert, violations := InspectCertificate(payload, "example.test", []byte("<svg/>"), fixtureNow)
got := rules(violations)
want := map[string]bool{"extendedKeyUsage": true, "logotype": true}
if cert == nil || len(got) != 2 || !want[got[0]] || !want[got[1]] {
t.Fatalf("violations = %v, want the missing usage and the missing logo", got)
}
if cert.MatchesLogo {
t.Error("an ordinary certificate cannot match a logo")
}
}
func TestInspectWarnsAboutALeafWithoutItsChain(t *testing.T) {
first, _ := pem.Decode(read(t, "ebay.pem"))
leafOnly := pem.EncodeToMemory(first)
cert, violations := InspectCertificate(leafOnly, "ebay.com", read(t, "ebay.svg"), fixtureNow)
if len(violations) != 0 {
t.Fatalf("a leaf alone is usable, only worth a warning: %v", violations)
}
if len(cert.Warnings) == 0 || !strings.Contains(cert.Warnings[0], "intermediate") {
t.Fatalf("warnings = %v", cert.Warnings)
}
}
func TestInspectWarnsAboutAnOutOfOrderChain(t *testing.T) {
var blocks []*pem.Block
rest := read(t, "ebay.pem")
for {
var b *pem.Block
b, rest = pem.Decode(rest)
if b == nil {
break
}
blocks = append(blocks, b)
}
// leaf first, then the chain reversed: each is no longer signed by the next
swapped := pem.EncodeToMemory(blocks[0])
for i := len(blocks) - 1; i >= 1; i-- {
swapped = append(swapped, pem.EncodeToMemory(blocks[i])...)
}
cert, violations := InspectCertificate(swapped, "ebay.com", read(t, "ebay.svg"), fixtureNow)
if len(violations) != 0 {
t.Fatalf("violations = %v", violations)
}
found := false
for _, w := range cert.Warnings {
found = found || strings.Contains(w, "chain")
}
if !found {
t.Fatalf("an out-of-order chain should warn: %v", cert.Warnings)
}
}
func TestCoversDomain(t *testing.T) {
cert := &x509.Certificate{DNSNames: []string{"example.test", "*.wild.test"}}
for domain, want := range map[string]bool{
"example.test": true,
"EXAMPLE.test": true,
"mail.example.test": true,
"notexample.test": false,
"example.test.evil": false,
"a.wild.test": true,
"wild.test": false, // a wildcard never covers its own apex
"other.test": false,
} {
if got := coversDomain(cert, domain); got != want {
t.Errorf("coversDomain(%q) = %v, want %v", domain, got, want)
}
}
}
@@ -25,6 +25,8 @@ type BimiLogo struct {
SVG []byte
ETag string
VmcURL string
// VmcPEM is the certificate chain held here, when the operator uploaded one.
VmcPEM []byte
}
// Save replaces a domain's logo, returning the digest receivers can cache on.
@@ -50,10 +52,10 @@ func (r *BimiRepository) Load(ctx context.Context, domain string) (*BimiLogo, er
var logo BimiLogo
var vmc *string
err := r.pool.QueryRow(ctx, `
SELECT b.svg, b.etag, b.vmc_url
SELECT b.svg, b.etag, b.vmc_url, b.vmc_pem
FROM domain_bimi b JOIN domains d ON d.id = b.domain_id
WHERE lower(d.name) = lower($1)
`, domain).Scan(&logo.SVG, &logo.ETag, &vmc)
`, domain).Scan(&logo.SVG, &logo.ETag, &vmc, &logo.VmcPEM)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
@@ -77,3 +79,33 @@ func (r *BimiRepository) Delete(ctx context.Context, domain string) error {
}
return nil
}
// SaveCertificate stores the certificate chain for a domain that already has a
// logo. It reports false when there is no logo to attach it to: a certificate
// only means something next to the logo it was issued for.
func (r *BimiRepository) SaveCertificate(ctx context.Context, domain string, pem []byte) (bool, error) {
tag, err := r.pool.Exec(ctx, `
UPDATE domain_bimi b
SET vmc_pem = $2, vmc_uploaded_at = NOW()
FROM domains d
WHERE b.domain_id = d.id AND lower(d.name) = lower($1)
`, domain, pem)
if err != nil {
return false, fmt.Errorf("save BIMI certificate for %s: %w", domain, err)
}
return tag.RowsAffected() > 0, nil
}
// DeleteCertificate removes the stored certificate, leaving the logo.
func (r *BimiRepository) DeleteCertificate(ctx context.Context, domain string) error {
_, err := r.pool.Exec(ctx, `
UPDATE domain_bimi b
SET vmc_pem = NULL, vmc_uploaded_at = NULL
FROM domains d
WHERE b.domain_id = d.id AND lower(d.name) = lower($1)
`, domain)
if err != nil {
return fmt.Errorf("delete BIMI certificate for %s: %w", domain, err)
}
return nil
}
@@ -0,0 +1,74 @@
package postgres
import (
"bytes"
"context"
"testing"
"github.com/google/uuid"
)
func TestBimiRepositoryHoldsALogoAndItsCertificate(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
name := "bimi" + uuid.NewString()[:8] + ".test"
id := uuid.New()
if _, err := pool.Exec(ctx, `INSERT INTO domains (id, name, punycode_name, is_active) VALUES ($1,$2,$3,true)`, id, name, name); err != nil {
t.Fatalf("seed domain: %v", err)
}
t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `DELETE FROM domains WHERE id = $1`, id) })
repo := NewBimiRepository(pool)
svg := []byte(`<svg version="1.2" baseProfile="tiny-ps" viewBox="0 0 1 1"><title>x</title></svg>`)
pem := []byte("-----BEGIN CERTIFICATE-----\nQUJD\n-----END CERTIFICATE-----\n")
// A certificate means something only next to a logo
saved, err := repo.SaveCertificate(ctx, name, pem)
if err != nil || saved {
t.Fatalf("SaveCertificate with no logo = (%v, %v), want (false, nil)", saved, err)
}
if _, err := repo.Save(ctx, name, svg, "https://elsewhere.test/vmc.pem"); err != nil {
t.Fatal(err)
}
logo, err := repo.Load(ctx, name)
if err != nil || logo == nil || len(logo.VmcPEM) != 0 || logo.VmcURL != "https://elsewhere.test/vmc.pem" {
t.Fatalf("a logo with no certificate: %+v, %v", logo, err)
}
if saved, err := repo.SaveCertificate(ctx, name, pem); err != nil || !saved {
t.Fatalf("SaveCertificate = (%v, %v)", saved, err)
}
logo, _ = repo.Load(ctx, name)
if !bytes.Equal(logo.VmcPEM, pem) {
t.Errorf("certificate read back = %q", logo.VmcPEM)
}
// Replacing the logo keeps the certificate: it is the operator who decides
// whether it still applies, and the console says when it does not
if _, err := repo.Save(ctx, name, append(svg, ' '), ""); err != nil {
t.Fatal(err)
}
logo, _ = repo.Load(ctx, name)
if !bytes.Equal(logo.VmcPEM, pem) {
t.Error("replacing the logo dropped the certificate")
}
if err := repo.DeleteCertificate(ctx, name); err != nil {
t.Fatal(err)
}
logo, _ = repo.Load(ctx, name)
if logo == nil || len(logo.VmcPEM) != 0 || len(logo.SVG) == 0 {
t.Errorf("deleting the certificate must leave the logo: %+v", logo)
}
// The certificate goes with the logo
repo.SaveCertificate(ctx, name, pem)
if err := repo.Delete(ctx, name); err != nil {
t.Fatal(err)
}
if logo, _ := repo.Load(ctx, name); logo != nil {
t.Errorf("the logo survived its own deletion: %+v", logo)
}
}
@@ -2,21 +2,28 @@ package httppresentation
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"lambdamail/protocols/internal/domain/bimi"
)
// BimiStore holds the mark a domain publishes.
// BimiStore holds the mark a domain publishes, and the certificate that
// vouches for it.
type BimiStore interface {
Save(ctx context.Context, domain string, svg []byte, vmcURL string) (string, error)
Load(ctx context.Context, domain string) (*BimiLogo, error)
Delete(ctx context.Context, domain string) error
// SaveCertificate reports false when the domain has no logo to attach it to.
SaveCertificate(ctx context.Context, domain string, pem []byte) (bool, error)
DeleteCertificate(ctx context.Context, domain string) error
}
// BimiLogo is a stored mark as this layer sees it.
@@ -24,6 +31,8 @@ type BimiLogo struct {
SVG []byte
ETag string
VmcURL string
// VmcPEM is the certificate chain held here, if the operator uploaded one.
VmcPEM []byte
}
type bimiAPI struct {
@@ -32,14 +41,27 @@ type bimiAPI struct {
// domainOf maps the host a request arrived on to the mail domain whose
// logo should answer, so one deployment can serve several.
defaultDomain string
// mailHost is where the logo and the certificate are served from: the host
// the DNS record names.
mailHost string
now func() time.Time
dns bimi.Resolver
fetch bimi.Fetcher
}
// SetBimiAPI wires the BIMI logo store.
func (r *Router) SetBimiAPI(store BimiStore, sessionSecret, defaultDomain string) {
func (r *Router) SetBimiAPI(store BimiStore, sessionSecret, defaultDomain, mailHost string) {
if store == nil {
return
}
r.bimi = &bimiAPI{store: store, defaultDomain: defaultDomain}
r.bimi = &bimiAPI{
store: store,
defaultDomain: defaultDomain,
mailHost: mailHost,
now: time.Now,
dns: netResolver{},
fetch: newBimiFetcher(),
}
if sessionSecret != "" {
r.bimi.sessions = NewWebSessionVerifier(sessionSecret)
}
@@ -101,27 +123,37 @@ func (a *bimiAPI) domainFor(req *http.Request) string {
return a.defaultDomain
}
// handleBimiAdmin uploads, inspects or removes a domain's mark.
func (r *Router) handleBimiAdmin(w http.ResponseWriter, req *http.Request) {
// requireBimiAdmin answers the admin session and domain checks shared by every
// BIMI console route. It writes the refusal itself and returns false.
func (r *Router) requireBimiAdmin(w http.ResponseWriter, req *http.Request) (string, bool) {
if r.bimi == nil || r.bimi.sessions == nil {
writeError(w, http.StatusServiceUnavailable, "BIMI_DISABLED",
"The BIMI panel needs JWT_SECRET and a database")
return
return "", false
}
token := bearerOrCookie(req, "lm_admin_session")
if token == "" {
writeError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Admin session required")
return
return "", false
}
if _, err := r.bimi.sessions.RequireSurface(token, "admin"); err != nil {
writeError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Admin session required")
return
return "", false
}
domain := strings.TrimSpace(req.URL.Query().Get("domain"))
if domain == "" {
writeError(w, http.StatusBadRequest, "DOMAIN_REQUIRED", "A domain is required")
return "", false
}
return domain, true
}
// handleBimiAdmin uploads, inspects or removes a domain's mark.
func (r *Router) handleBimiAdmin(w http.ResponseWriter, req *http.Request) {
domain, ok := r.requireBimiAdmin(w, req)
if !ok {
return
}
@@ -142,6 +174,120 @@ func (r *Router) handleBimiAdmin(w http.ResponseWriter, req *http.Request) {
}
}
// handleBimiCertificate serves the certificate chain, like the logo: fetched
// by receiving providers that hold no session.
func (r *Router) handleBimiCertificate(w http.ResponseWriter, req *http.Request) {
if r.bimi == nil {
http.NotFound(w, req)
return
}
if req.Method != http.MethodGet && req.Method != http.MethodHead {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
return
}
logo, err := r.bimi.store.Load(req.Context(), r.bimi.domainFor(req))
if err != nil || logo == nil || len(logo.VmcPEM) == 0 {
http.NotFound(w, req)
return
}
sum := sha256.Sum256(logo.VmcPEM)
etag := `"` + hex.EncodeToString(sum[:]) + `"`
if req.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
w.Header().Set("ETag", etag)
// Shorter than the logo: a renewed certificate should reach receivers
// within the hour, not the day.
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Header().Set("X-Content-Type-Options", "nosniff")
if req.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
return
}
_, _ = w.Write(logo.VmcPEM)
}
// handleBimiCertificateAdmin uploads or removes the certificate chain.
func (r *Router) handleBimiCertificateAdmin(w http.ResponseWriter, req *http.Request) {
domain, ok := r.requireBimiAdmin(w, req)
if !ok {
return
}
switch req.Method {
case http.MethodPut:
r.bimi.putCertificate(w, req, domain)
case http.MethodDelete:
if err := r.bimi.store.DeleteCertificate(req.Context(), domain); err != nil {
log.Printf("bimi: could not delete the certificate for %s: %v", domain, err)
writeError(w, http.StatusInternalServerError, "DELETE_FAILED", "Could not remove the certificate")
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
default:
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "PUT or DELETE")
}
}
// handleBimiCheck looks at everything a receiver would look at, and says which
// part is wrong.
func (r *Router) handleBimiCheck(w http.ResponseWriter, req *http.Request) {
domain, ok := r.requireBimiAdmin(w, req)
if !ok {
return
}
if req.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
return
}
logo, err := r.bimi.store.Load(req.Context(), domain)
if err != nil {
writeError(w, http.StatusInternalServerError, "LOAD_FAILED", "Could not read the logo")
return
}
if logo == nil {
writeJSON(w, http.StatusOK, map[string]any{"configured": false, "checks": []any{}})
return
}
ctx, cancel := context.WithTimeout(req.Context(), 25*time.Second)
defer cancel()
in := bimi.Inputs{
Domain: domain,
LogoURL: BimiLogoURL(r.bimi.mailHost),
SVG: logo.SVG,
CertificatePEM: logo.VmcPEM,
ExternalCertificateURL: logo.VmcURL,
Now: r.bimi.now(),
}
// The record must name the certificate that is actually served
switch {
case len(logo.VmcPEM) > 0:
in.CertificateURL = BimiVmcURL(r.bimi.mailHost)
default:
in.CertificateURL = logo.VmcURL
}
results := bimi.RunChecks(ctx, in, r.bimi.dns, r.bimi.fetch)
checks := make([]map[string]string, 0, len(results))
ready := true
for _, c := range results {
checks = append(checks, map[string]string{"id": c.ID, "status": string(c.Status), "detail": c.Detail})
// A missing certificate is a warning, not a fault: some receivers show the logo without one
if c.Status == bimi.StatusFail {
ready = false
}
}
writeJSON(w, http.StatusOK, map[string]any{"configured": true, "ready": ready, "checks": checks})
}
func (a *bimiAPI) get(w http.ResponseWriter, req *http.Request, domain string) {
logo, err := a.store.Load(req.Context(), domain)
if err != nil {
@@ -153,11 +299,101 @@ func (a *bimiAPI) get(w http.ResponseWriter, req *http.Request, domain string) {
return
}
writeJSON(w, http.StatusOK, map[string]any{
"configured": true,
"etag": logo.ETag,
"vmc_url": logo.VmcURL,
"bytes": len(logo.SVG),
"svg": string(logo.SVG),
"configured": true,
"etag": logo.ETag,
"vmc_url": logo.VmcURL,
"bytes": len(logo.SVG),
"svg": string(logo.SVG),
"logo_url": BimiLogoURL(a.mailHost),
"certificate_url": BimiVmcURL(a.mailHost),
"certificate": a.describeCertificate(domain, logo),
})
}
// describeCertificate reports the held certificate as it stands against the
// logo now being served. Read from the file each time, so it cannot drift from
// what receivers get.
func (a *bimiAPI) describeCertificate(domain string, logo *BimiLogo) map[string]any {
if len(logo.VmcPEM) == 0 {
return map[string]any{"present": false}
}
cert, violations := bimi.InspectCertificate(logo.VmcPEM, domain, logo.SVG, a.now())
reasons := make([]string, 0, len(violations))
for _, v := range violations {
reasons = append(reasons, v.String())
}
out := map[string]any{"present": true, "valid": len(violations) == 0, "violations": reasons}
if cert != nil {
out["subject"] = cert.Subject
out["organization"] = cert.Organization
out["issuer"] = cert.Issuer
out["not_before"] = cert.NotBefore.UTC().Format(time.RFC3339)
out["not_after"] = cert.NotAfter.UTC().Format(time.RFC3339)
out["dns_names"] = cert.DNSNames
out["mark_type"] = cert.MarkType
out["kind"] = string(cert.Kind)
out["chain_length"] = cert.ChainLength
out["matches_logo"] = cert.MatchesLogo
out["warnings"] = cert.Warnings
}
return out
}
// putCertificate stores a certificate chain after checking it against the
// logo and the domain. A certificate that does not match is refused rather
// than stored: publishing one makes receivers ignore BIMI without a word.
func (a *bimiAPI) putCertificate(w http.ResponseWriter, req *http.Request, domain string) {
body, err := io.ReadAll(io.LimitReader(req.Body, bimi.MaxCertificateBytes*2))
if err != nil {
writeError(w, http.StatusBadRequest, "READ_FAILED", "Could not read the upload")
return
}
var payload struct {
PEM string `json:"pem"`
}
if err := json.Unmarshal(body, &payload); err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Expected a JSON body with a pem field")
return
}
logo, err := a.store.Load(req.Context(), domain)
if err != nil {
writeError(w, http.StatusInternalServerError, "LOAD_FAILED", "Could not read the logo")
return
}
if logo == nil {
writeError(w, http.StatusConflict, "NO_LOGO", "Save the logo first: a certificate only means something next to the logo it was issued for")
return
}
pem := []byte(strings.TrimSpace(payload.PEM) + "\n")
_, violations := bimi.InspectCertificate(pem, domain, logo.SVG, a.now())
if len(violations) > 0 {
reasons := make([]string, 0, len(violations))
for _, v := range violations {
reasons = append(reasons, v.String())
}
writeJSON(w, http.StatusBadRequest, map[string]any{
"error": "INVALID_CERTIFICATE",
"message": "The certificate cannot be used for this domain and logo",
"violations": reasons,
})
return
}
saved, err := a.store.SaveCertificate(req.Context(), domain, pem)
if err != nil {
log.Printf("bimi: could not save the certificate for %s: %v", domain, err)
writeError(w, http.StatusInternalServerError, "SAVE_FAILED", "Could not store the certificate")
return
}
if !saved {
writeError(w, http.StatusConflict, "NO_LOGO", "Save the logo first")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"certificate": a.describeCertificate(domain, &BimiLogo{SVG: logo.SVG, VmcPEM: pem}),
})
}
@@ -208,10 +444,23 @@ func (a *bimiAPI) put(w http.ResponseWriter, req *http.Request, domain string) {
writeError(w, http.StatusInternalServerError, "SAVE_FAILED", "Could not store the logo")
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "etag": etag})
// A certificate vouches for one exact SVG. Replacing the logo under a held
// certificate breaks it, which receivers do not report; say so now.
response := map[string]any{"ok": true, "etag": etag}
if held, err := a.store.Load(req.Context(), domain); err == nil && held != nil && len(held.VmcPEM) > 0 {
if cert, _ := bimi.InspectCertificate(held.VmcPEM, domain, held.SVG, a.now()); cert != nil && !cert.MatchesLogo {
response["certificate_mismatch"] = true
}
}
writeJSON(w, http.StatusOK, response)
}
// BimiLogoURL is where a domain's mark is served from.
func BimiLogoURL(mailHost string) string {
return fmt.Sprintf("https://%s/.well-known/bimi/default.svg", mailHost)
}
// BimiVmcURL is where the certificate held here is served from.
func BimiVmcURL(mailHost string) string {
return fmt.Sprintf("https://%s/.well-known/bimi/vmc.pem", mailHost)
}
@@ -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)
}
}
}
@@ -0,0 +1,95 @@
package httppresentation
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"syscall"
"time"
"lambdamail/protocols/internal/domain/bimi"
)
// netResolver reads TXT records from the system resolver.
type netResolver struct{}
func (netResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
return net.DefaultResolver.LookupTXT(ctx, name)
}
// maxFetchBytes bounds what a check reads. A logo is at most 32 KB and a
// certificate chain about 10 KB.
const maxFetchBytes = 256 * 1024
// newBimiFetcher returns the HTTP client the console's checks use.
//
// The URLs it is given come from DNS and from a field an operator filled in,
// so it must not become a way to make this server call its own network: HTTPS
// only, and the address actually connected to (after DNS, so a name that
// rebinds to a private address is caught too) must be a public one.
func newBimiFetcher() bimi.Fetcher {
dialer := &net.Dialer{
Timeout: 6 * time.Second,
Control: func(_, address string, _ syscall.RawConn) error {
host, _, err := net.SplitHostPort(address)
if err != nil {
return err
}
if ip := net.ParseIP(host); ip == nil || !publicAddress(ip) {
return errors.New("refusing to connect to a non-public address")
}
return nil
},
}
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{DialContext: dialer.DialContext, TLSHandshakeTimeout: 6 * time.Second},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return errors.New("too many redirects")
}
if req.URL.Scheme != "https" {
return errors.New("redirect to a non-HTTPS URL")
}
return nil
},
}
return func(ctx context.Context, url string) (bimi.Response, error) {
if !strings.HasPrefix(url, "https://") {
return bimi.Response{}, fmt.Errorf("%q is not an https URL", url)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return bimi.Response{}, err
}
resp, err := client.Do(req)
if err != nil {
return bimi.Response{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, maxFetchBytes))
if err != nil {
return bimi.Response{}, err
}
return bimi.Response{Status: resp.StatusCode, ContentType: resp.Header.Get("Content-Type"), Body: body}, nil
}
}
// publicAddress is false for anything that is not a routable public address.
func publicAddress(ip net.IP) bool {
switch {
case ip.IsLoopback(), ip.IsPrivate(), ip.IsLinkLocalUnicast(), ip.IsLinkLocalMulticast(),
ip.IsMulticast(), ip.IsUnspecified(), ip.IsInterfaceLocalMulticast():
return false
}
// Carrier-grade NAT, 100.64.0.0/10, which IsPrivate does not cover
if v4 := ip.To4(); v4 != nil && v4[0] == 100 && v4[1]&0xC0 == 64 {
return false
}
return true
}
@@ -80,7 +80,10 @@ func (r *Router) registerRoutes() {
r.mux.HandleFunc("/.well-known/mta-sts.txt", r.handleMtaSts)
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/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)
r.mux.HandleFunc("/mail/config-v1.1.xml", r.handleThunderbirdAutoconfig)
r.mux.HandleFunc("/.well-known/autoconfig/mail/config-v1.1.xml", r.handleThunderbirdAutoconfig)
r.mux.HandleFunc("/autodiscover/autodiscover.xml", r.handleOutlookAutodiscover)
+6
View File
@@ -36,6 +36,12 @@ const nextConfig: NextConfig = {
source: "/.well-known/bimi/default.svg",
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/default.svg`,
},
{
// The BIMI certificate (VMC/CMC) the DNS record's a= tag points at,
// fetched the same way as the logo above.
source: "/.well-known/bimi/vmc.pem",
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/vmc.pem`,
},
{
source: "/.well-known/security.txt",
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/security.txt`,
@@ -58,4 +58,12 @@ describe("the routes the protocols service owns", () => {
expect(config).toContain("/.well-known/bimi/default.svg");
expect(config).toMatch(/bimi\/default\.svg[\s\S]{0,200}PROTOCOLS_SERVICE_URL/);
});
// The a= tag of the BIMI record points here when the certificate is held by
// the service; without the rewrite the Next 404 page is served instead, and
// the receiver sees a record whose certificate cannot be fetched.
it("routes the BIMI certificate to the service that stores it", () => {
expect(config).toContain("/.well-known/bimi/vmc.pem");
expect(config).toMatch(/bimi\/vmc\.pem[\s\S]{0,200}PROTOCOLS_SERVICE_URL/);
});
});