feat(tls): o painel diz o digest que o registo TLSA tem de carregar

Uma associacao DANE e o SHA-256 sobre o SubjectPublicKeyInfo do certificado
que esta a ser servido. Se o que esta no DNS e o que o servidor apresenta se
afastarem, qualquer remetente que valide DANE recusa o correio -- e nada no
painel dizia porque.

Passa a mostrar o numero, para poder ser comparado com o registo publicado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5 committed 2026-09-01 12:08:25 -03:00
1 parent c06691a056
commit 712885c091
7 files changed
+86 -3

No files matched your search

@@ -2,8 +2,10 @@ package tlsprovider
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"time"
"lambdamail/protocols/internal/application/port"
@@ -94,3 +96,18 @@ func (s *ProviderStatus) CertificateSummary(host string) (string, bool, bool) {
leaf.CheckSignature(leaf.SignatureAlgorithm, leaf.RawTBSCertificate, leaf.Signature) == nil
return issuer, selfSigned, true
}
// CertificateSpkiDigest is the SHA-256 over the certificate's
// SubjectPublicKeyInfo: the number a TLSA "3 1 1" record has to carry.
//
// Published so an operator can compare what is in DNS against what the server
// is actually serving. When those two disagree, every DANE-validating sender
// refuses the mail, and nothing else on the panel would show why.
func (s *ProviderStatus) CertificateSpkiDigest(host string) (string, bool) {
leaf := s.leafFor(host)
if leaf == nil {
return "", false
}
sum := sha256.Sum256(leaf.RawSubjectPublicKeyInfo)
return hex.EncodeToString(sum[:]), true
}
@@ -1,6 +1,9 @@
package tlsprovider
import (
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
"time"
)
@@ -47,3 +50,37 @@ func TestProviderStatus_ReportsAnEphemeralSelfSignedCertificate(t *testing.T) {
t.Error("no issuer reported")
}
}
// The DANE association an operator publishes has to be the hash of the key the
// server is actually serving. If the two ever drift, mail from a validating
// sender is refused outright - so the panel states the digest of the live
// certificate, which is the number to compare against the TLSA record.
func TestProviderStatus_ReportsTheSpkiDigestOfTheServedCertificate(t *testing.T) {
provider, err := NewEphemeralSelfSignedCertProvider()
if err != nil {
t.Fatalf("build provider: %v", err)
}
status := NewProviderStatus(provider)
digest, ok := status.CertificateSpkiDigest("mail.example.test")
if !ok {
t.Fatal("no digest reported")
}
// TLSA 3 1 1 is a SHA-256 over the SubjectPublicKeyInfo: 64 hex characters.
if len(digest) != 64 {
t.Errorf("digest %q is %d characters, want 64", digest, len(digest))
}
for _, c := range digest {
if !strings.ContainsRune("0123456789abcdef", c) {
t.Fatalf("digest %q is not lowercase hex", digest)
}
}
// It must be the SPKI hash, not a hash of the whole certificate: those are
// different numbers and publishing the wrong one breaks delivery.
leaf := status.leafFor("mail.example.test")
want := sha256.Sum256(leaf.RawSubjectPublicKeyInfo)
if digest != hex.EncodeToString(want[:]) {
t.Errorf("digest is not the SHA-256 of the SubjectPublicKeyInfo")
}
}