# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0.  If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYRIGHT file distributed with this work for additional
# information regarding copyright ownership.

"""
Regression test for issue #5877: a CD=1 (checking-disabled) NXDOMAIN response
is cached at dns_trust_pending_answer and must not evict a DNSSEC-validated
RRset cached at dns_trust_secure for the same name.

ns1 is a validating resolver that forwards "example" to the signed ns2.  We
prime the cache with a validated a.example/A (trust=secure), flip ns2 to a
version of the zone where a.example no longer exists, and send a CD=1 query
that makes the resolver cache an unvalidated NXDOMAIN for a.example.  The
originally cached, more-trusted A record must survive.
"""

import os
import shutil

import dns.zone

import isctest

RESOLVER = "10.53.0.1"
AUTH = "10.53.0.2"
A_RDATA = "10.53.0.99"


def bootstrap():
    alg = os.environ["DEFAULT_ALGORITHM"]
    bits = os.environ["DEFAULT_BITS"]
    keygen = isctest.run.EnvCmd("KEYGEN", f"-a {alg} -b {bits} -Kns2 -q")
    signer = isctest.run.EnvCmd("SIGNER", "-S -g")

    name = "example"

    zsk_name = keygen(name).out.strip()
    isctest.kasp.Key(zsk_name, keydir="ns2")
    ksk_name = keygen(f"-f KSK {name}").out.strip()
    ksk = isctest.kasp.Key(ksk_name, keydir="ns2")

    zonetext = """
@ 300 IN SOA ns2.example. admin.example. 0 3600 1800 604800 300
@ NS ns2.example.
ns2 A 10.53.0.2
"""
    zone = dns.zone.from_text(zonetext, origin=name)
    zone.to_file(f"ns2/{name}-empty.db", sorted=True)
    signer(
        f"-P -x -O full -o {name} -f {name}-empty.db.signed {name}-empty.db", cwd="ns2"
    )

    zonetext += "a A 10.53.0.99"
    zone = dns.zone.from_text(zonetext, origin=name)
    zone.to_file(f"ns2/{name}-full.db", sorted=True)
    signer(
        f"-P -x -O full -o {name} -f {name}-full.db.signed {name}-full.db", cwd="ns2"
    )

    shutil.copyfile(f"ns2/{name}-full.db.signed", f"ns2/{name}.db.signed")

    return {
        "trust_anchors": [
            ksk.into_ta("static-key"),
        ],
    }


def _serve(ns2, system_test_dir, variant):
    """Make ns2 serve the 'full' or 'empty' (a.example-less) signed zone."""
    src = system_test_dir / "ns2" / f"example-{variant}.db.signed"
    shutil.copyfile(src, system_test_dir / "ns2" / "example.db.signed")
    ns2.reload()


def _prime_secure_a(ns1):
    """Cache a.example/A at trust=secure and confirm it validated (AD=1)."""
    ns1.rndc("flush")
    res = isctest.query.tcp(isctest.query.create("a.example", "A"), RESOLVER)
    isctest.check.noerror(res)
    isctest.check.adflag(res)
    assert A_RDATA in str(res.answer), res.answer


def test_cd1_nxdomain_keeps_secure_rrset(ns1, ns2, system_test_dir):
    # Prime the resolver with a DNSSEC-validated a.example/A (trust=secure).
    _serve(ns2, system_test_dir, "full")
    _prime_secure_a(ns1)

    # Flip the authoritative zone to a (still signed) version without
    # a.example, so the name now yields a signed NXDOMAIN; confirm at ns2.
    _serve(ns2, system_test_dir, "empty")
    direct = isctest.query.create("a.example", "A", dnssec=False)
    isctest.check.nxdomain(isctest.query.tcp(direct, AUTH))

    # A CD=1 query elicits an unvalidated NXDOMAIN for a.example, cached at
    # trust=pending_answer.  The the secure A record should not be evicted.
    cd_msg = isctest.query.create("a.example", "TXT", cd=True)
    isctest.query.tcp(cd_msg, RESOLVER)

    # The resolver should not refetch from the zone; the validated A
    # record should still be served from cache.
    res = isctest.query.tcp(isctest.query.create("a.example", "A"), RESOLVER)
    isctest.check.noerror(res)
    isctest.check.adflag(res)
    assert A_RDATA in str(res.answer), res.answer


def test_cd1_nxdomain_uncached_type_answer(ns1, ns2, system_test_dir):
    # Prime a.example/A at trust=secure, then make a.example yield NXDOMAIN.
    _serve(ns2, system_test_dir, "full")
    _prime_secure_a(ns1)
    _serve(ns2, system_test_dir, "empty")

    # CD=1 query for an UNCACHED type (AAAA): the unvalidated NXDOMAIN is
    # rejected in favour of the secure A.  Returning the cached A record (the
    # wrong type) in the answer section is incorrect; the answer must be empty.
    res = isctest.query.tcp(
        isctest.query.create("a.example", "AAAA", cd=True), RESOLVER
    )
    isctest.check.servfail(res)
    isctest.check.empty_answer(res)

    # a CD=0 query should now validate the NXDOMAIN, ejecting the A.
    res = isctest.query.tcp(isctest.query.create("a.example", "AAAA"), RESOLVER)
    isctest.check.nxdomain(res)
    isctest.check.adflag(res)
    isctest.check.empty_answer(res)

    res = isctest.query.tcp(isctest.query.create("a.example", "A"), RESOLVER)
    isctest.check.nxdomain(res)
    isctest.check.adflag(res)
    isctest.check.empty_answer(res)
