carlos / platform Public

edge: never serve a cached wildcard that doesn't cover its parent

Paul Campbell 307303e7dd5246da1bbb2e2e532bcdbbb2744855 parent a3063a2
2 files changed, +75 −5
  • internal/edge/wildcard.go +26 −5
  • internal/edge/wildcard_test.go +49 −0
diff --git a/internal/edge/wildcard.go b/internal/edge/wildcard.go
index 5bc8d5326..0f14de237 100644
--- a/internal/edge/wildcard.go
+++ b/internal/edge/wildcard.go
@@ -25,6 +25,7 @@ package edge
import (
"bytes"
"context"
+ "crypto"
"crypto/ecdsa"
"crypto/tls"
"crypto/x509"
@@ -185,17 +186,37 @@ func (w *wildcardManager) loadCached(ctx context.Context, parent string) bool {
if err == nil {
var cert *tls.Certificate
if cert, err = parseWildcardPEM(raw); err == nil {
- w.mu.Lock()
- w.certs[parent] = cert
- w.mu.Unlock()
- log.Printf("edge: wildcard *.%s loaded from cache (valid until %s)", parent, cert.Leaf.NotAfter.Format(time.RFC3339))
- return true
+ if !coversParent(cert, parent) {
+ err = errors.New("cached cert does not cover parent or key mismatch")
+ } else {
+ w.mu.Lock()
+ w.certs[parent] = cert
+ w.mu.Unlock()
+ log.Printf("edge: wildcard *.%s loaded from cache (valid until %s)", parent, cert.Leaf.NotAfter.Format(time.RFC3339))
+ return true
+ }
}
}
log.Printf("edge: wildcard *.%s: cached cert unreadable (%v) — will re-obtain", parent, err)
return false
}
+// coversParent reports whether cert is usable as *.<parent>: the leaf must
+// name a one-label host under parent and the private key must match the
+// leaf. A cached entry failing this is treated as unreadable — served
+// certs must never be worse than no cert (per-host issuance covers).
+func coversParent(cert *tls.Certificate, parent string) bool {
+ if cert.Leaf == nil || cert.Leaf.VerifyHostname("acme-probe."+parent) != nil {
+ return false
+ }
+ priv, ok := cert.PrivateKey.(interface{ Public() crypto.PublicKey })
+ if !ok {
+ return false
+ }
+ pub, ok := priv.Public().(interface{ Equal(crypto.PublicKey) bool })
+ return ok && pub.Equal(cert.Leaf.PublicKey)
+}
+
// wildcardPEMKey is the cache key for a parent's wildcard cert+key PEM —
// Eleven's file name, so a dir cache written by that lineage reads with no
// migration, and the keys sit additively beside autocert's per-host entries.
diff --git a/internal/edge/wildcard_test.go b/internal/edge/wildcard_test.go
index e983e4de5..5ac7b2618 100644
--- a/internal/edge/wildcard_test.go
+++ b/internal/edge/wildcard_test.go
@@ -174,3 +174,52 @@ func TestLoadCachedAndRefreshWake(t *testing.T) {
t.Fatal("refresh with missing cert should wake the run loop")
}
}
+
+func TestLoadCachedRejectsWrongCoverage(t *testing.T) {
+ // (a) a cert valid for the WRONG parent, stashed under abs.oncarlos.com's
+ // key — loadCached must refuse it and store nothing.
+ wrongParent, wrongParentKey := selfSignedWildcard(t, "bab.oncarlos.com", time.Now().Add(60*24*time.Hour))
+ rawWrongParent, err := encodeWildcardPEM(wrongParent, wrongParentKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cache := autocert.DirCache(t.TempDir())
+ if err := cache.Put(context.Background(), wildcardPEMKey("abs.oncarlos.com"), rawWrongParent); err != nil {
+ t.Fatal(err)
+ }
+ w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, cache, nil)
+ if w.loadCached(context.Background(), "abs.oncarlos.com") {
+ t.Fatal("loadCached: want false for cert covering the wrong parent")
+ }
+ w.mu.Lock()
+ _, stored := w.certs["abs.oncarlos.com"]
+ w.mu.Unlock()
+ if stored {
+ t.Fatal("loadCached: must not store a cert that doesn't cover the parent")
+ }
+
+ // (b) a cert whose stored key does NOT match the leaf's public key.
+ cert, _ := selfSignedWildcard(t, "abs.oncarlos.com", time.Now().Add(60*24*time.Hour))
+ otherKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rawMismatch, err := encodeWildcardPEM(cert, otherKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cache2 := autocert.DirCache(t.TempDir())
+ if err := cache2.Put(context.Background(), wildcardPEMKey("abs.oncarlos.com"), rawMismatch); err != nil {
+ t.Fatal(err)
+ }
+ w2 := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, cache2, nil)
+ if w2.loadCached(context.Background(), "abs.oncarlos.com") {
+ t.Fatal("loadCached: want false for key/leaf mismatch")
+ }
+ w2.mu.Lock()
+ _, stored2 := w2.certs["abs.oncarlos.com"]
+ w2.mu.Unlock()
+ if stored2 {
+ t.Fatal("loadCached: must not store a cert whose key doesn't match its leaf")
+ }
+}