diff --git a/pkg/controller/acmechallenges/BUILD.bazel b/pkg/controller/acmechallenges/BUILD.bazel index 506481187..5983a094b 100644 --- a/pkg/controller/acmechallenges/BUILD.bazel +++ b/pkg/controller/acmechallenges/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "//pkg/controller:go_default_library", "//pkg/controller/acmechallenges/scheduler:go_default_library", "//pkg/issuer/acme/dns:go_default_library", + "//pkg/issuer/acme/dns/util:go_default_library", "//pkg/issuer/acme/http:go_default_library", "//pkg/util:go_default_library", "//third_party/crypto/acme:go_default_library", diff --git a/pkg/controller/acmechallenges/sync.go b/pkg/controller/acmechallenges/sync.go index 4cf857842..9939ae6a1 100644 --- a/pkg/controller/acmechallenges/sync.go +++ b/pkg/controller/acmechallenges/sync.go @@ -31,6 +31,8 @@ import ( cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" controllerpkg "github.com/jetstack/cert-manager/pkg/controller" acmeapi "github.com/jetstack/cert-manager/third_party/crypto/acme" + + dnsutil "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util" ) const ( @@ -131,6 +133,26 @@ func (c *Controller) Sync(ctx context.Context, ch *cmapi.Challenge) (err error) return nil } + // check for CAA records. + // CAA records are static, so we don't have to present anything + // before we check for them. + + // Find out which identity the ACME server says it will use. + dir, err := cl.Discover(ctx) + if err != nil { + return err + } + // TODO(dmo): figure out if missing CAA identity in directory + // means no CAA check is performed by ACME server or if any valid + // CAA would stop issuance (strongly suspect the former) + if len(dir.CAA) != 0 { + err := dnsutil.ValidateCAA(ch.Spec.DNSName, dir.CAA, ch.Spec.Wildcard) + if err != nil { + ch.Status.Reason = fmt.Sprintf("CAA self-check failed: %s", err) + return err + } + } + solver, err := c.solverFor(ch.Spec.Type) if err != nil { return err diff --git a/pkg/issuer/acme/dns/util/wait.go b/pkg/issuer/acme/dns/util/wait.go index 94dc471aa..648d638fd 100644 --- a/pkg/issuer/acme/dns/util/wait.go +++ b/pkg/issuer/acme/dns/util/wait.go @@ -168,6 +168,58 @@ func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) ( return } +func ValidateCAA(domain string, issuerID []string, iswildcard bool) error { + // see https://tools.ietf.org/html/rfc6844#section-4 + // for more information about how CAA lookup is performed + fqdn := ToFqdn(domain) + + issuerSet := make(map[string]bool) + for _, s := range issuerID { + issuerSet[s] = true + } + + //TODO(dmo): figure out if we need these servers to be configurable as well + msg, err := dnsQuery(fqdn, dns.TypeCAA, RecursiveNameservers, true) + if err != nil { + return fmt.Errorf("Could not validate CAA record: %s", err) + } + //TODO(dmo): follow CNAMES + //TODO(dmo): look at labels above this one + caas := make([]*dns.CAA, 0, len(msg.Answer)) + for _, rr := range msg.Answer { + caa, ok := rr.(*dns.CAA) + if !ok { + continue + } + caas = append(caas, caa) + } + if len(caas) == 0 { + // TODO(dmo): work up in the label + return nil + } + if !matchCAA(caas, issuerSet, iswildcard) { + // TODO(dmo): better error message + return fmt.Errorf("CAA record does not match issuer") + } + return nil +} + +func matchCAA(caas []*dns.CAA, issuerIDs map[string]bool, iswildcard bool) bool { + expectedTag := "issue" + if iswildcard { + expectedTag = "issuewild" + } + for _, caa := range caas { + if caa.Tag != expectedTag { + continue + } + if issuerIDs[caa.Value] { + return true + } + } + return false +} + // lookupNameservers returns the authoritative nameservers for the given fqdn. func lookupNameservers(fqdn string, nameservers []string) ([]string, error) { var authoritativeNss []string