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>
87 lines
2.9 KiB
Go
87 lines
2.9 KiB
Go
package tlsprovider
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The TLS panel answered 503 "the TLS panel needs a certificate watcher" on a
|
|
// real deployment, because the panel was only wired when the certificate came
|
|
// from the Traefik acme.json watcher. With TRAEFIK_ACME_DIR unset the server
|
|
// falls back to an ephemeral self-signed certificate - which is exactly the
|
|
// state an operator most needs the panel to show, and precisely the one it
|
|
// refused to report.
|
|
func TestProviderStatus_ReportsAnEphemeralSelfSignedCertificate(t *testing.T) {
|
|
provider, err := NewEphemeralSelfSignedCertProvider()
|
|
if err != nil {
|
|
t.Fatalf("build provider: %v", err)
|
|
}
|
|
status := NewProviderStatus(provider)
|
|
|
|
if !status.HasCertificateFor("mail.example.test") {
|
|
t.Error("reported no certificate at all; the server is serving one")
|
|
}
|
|
|
|
host, expiry, ok := status.EarliestExpiry()
|
|
if !ok {
|
|
t.Fatal("could not read the expiry, so the panel would show nothing")
|
|
}
|
|
if expiry.Before(time.Now()) {
|
|
t.Errorf("expiry %v is in the past", expiry)
|
|
}
|
|
if host == "" {
|
|
t.Error("no host named for the expiring certificate")
|
|
}
|
|
|
|
// The whole point: an operator must be able to see this is not a real
|
|
// certificate. Serving self-signed on port 25 and 993 means every client
|
|
// that verifies is refusing the connection.
|
|
issuer, selfSigned, ok := status.CertificateSummary("mail.example.test")
|
|
if !ok {
|
|
t.Fatal("no summary available")
|
|
}
|
|
if !selfSigned {
|
|
t.Error("an ephemeral self-signed certificate was not reported as self-signed")
|
|
}
|
|
if issuer == "" {
|
|
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")
|
|
}
|
|
}
|