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