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,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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user