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

@@ -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")
}