From 6d4fab1e6a3ee881782486502d9ace37213ec401 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Fri, 1 Aug 2014 12:15:02 +0000 Subject: [PATCH] whats_next: offer DNSSEC DS parameters rather than the full record and in validation allow for other digests than the one we suggest using fixes #120 (hopefully), in which Gandi generates a SHA1 digest but we were only checking against a SHA256 digest Also see http://discourse.mailinabox.email/t/how-to-set-ds-record-for-gandi-net/24/1 in which a user asks about the DS parameters that Gandi asks for. --- management/dns_update.py | 19 +++++++++----- management/whats_next.py | 56 ++++++++++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index 6d9fbfa2..f83871b4 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -93,7 +93,7 @@ def do_dns_update(env, force=False): # Thus we only sign a zone if write_nsd_zone returned True # indicating the zone changed, and thus it got a new serial number. # write_nsd_zone is smart enough to check if a zone's signature - # is nearing experiation and if so it'll bump the serial number + # is nearing expiration and if so it'll bump the serial number # and return True so we get a chance to re-sign it. sign_zone(domain, zonefile, env) @@ -478,13 +478,18 @@ def sign_zone(domain, zonefile, env): # zone being signed, so we can't use the .ds files generated when we created the keys. # The DS record points to the KSK only. Write this next to the zone file so we can # get it later to give to the user with instructions on what to do with it. - rr_ds = shell('check_output', ["/usr/bin/ldns-key2ds", - "-n", # output to stdout - "-2", # SHA256 - dnssec_keys["KSK"] + ".key" - ]) + # + # We want to be able to validate DS records too, but multiple forms may be valid depending + # on the digest type. So we'll write all (both) valid records. Only one DS record should + # actually be deployed. Preferebly the first. with open("/etc/nsd/zones/" + zonefile + ".ds", "w") as f: - f.write(rr_ds) + for digest_type in ('2', '1'): + rr_ds = shell('check_output', ["/usr/bin/ldns-key2ds", + "-n", # output to stdout + "-" + digest_type, # 1=SHA1, 2=SHA256 + dnssec_keys["KSK"] + ".key" + ]) + f.write(rr_ds) # Remove our temporary file. for fn in files_to_kill: diff --git a/management/whats_next.py b/management/whats_next.py index 1061f8b9..da4c75dd 100755 --- a/management/whats_next.py +++ b/management/whats_next.py @@ -14,7 +14,7 @@ from dns_update import get_dns_zones from web_update import get_web_domains, get_domain_ssl_files from mailconfig import get_mail_domains, get_mail_aliases -from utils import shell, sort_domains +from utils import shell, sort_domains, load_env_vars_from_file def run_checks(env): run_system_checks(env) @@ -125,25 +125,47 @@ def check_dns_zone(domain, env, dns_zonefiles): control panel to set the nameservers to %s.""" % (existing_ns, correct_ns) ) - # See if the domain has a DS record set. + # See if the domain has a DS record set at the registrar. The DS record may have + # several forms. We have to be prepared to check for any valid record. We've + # pre-generated all of the valid digests --- read them in. + ds_correct = open('/etc/nsd/zones/' + dns_zonefiles[domain] + '.ds').read().strip().split("\n") + digests = { } + for rr_ds in ds_correct: + ds_keytag, ds_alg, ds_digalg, ds_digest = rr_ds.split("\t")[4].split(" ") + digests[ds_digalg] = ds_digest + + # Some registrars may want the public key so they can compute the digest. The DS + # record that we suggest using is for the KSK (and that's how the DS records were generated). + dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/keys.conf')) + dnsssec_pubkey = open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key')).read().split("\t")[3].split(" ")[3] + + # Query public DNS for the DS record at the registrar. ds = query_dns(domain, "DS", nxdomain=None) - ds_correct = open('/etc/nsd/zones/' + dns_zonefiles[domain] + '.ds').read().strip() - ds_expected = re.sub(r"\S+\.\s+3600\s+IN\s+DS\s*", "", ds_correct) - if ds == ds_expected: + ds_looks_valid = ds and len(ds.split(" ")) == 4 + if ds_looks_valid: ds = ds.split(" ") + if ds_looks_valid and ds[0] == ds_keytag and ds[1] == '7' and ds[3] == digests.get(ds[2]): print_ok("DNS 'DS' record is set correctly at registrar.") - elif ds == None: - print_error("""This domain's DNS DS record is not set. The DS record is optional. The DS record activates DNSSEC. - To set a DS record, you must follow the instructions provided by your domain name registrar and provide to them this information:""") - print("") - print(" " + ds_correct) - print("") else: - print_error("""This domain's DNS DS record is incorrect. The chain of trust is broken between the public DNS system - and this machine's DNS server. It may take several hours for public DNS to update after a change. If you did not recently - make a change, you must resolve this immediately by following the instructions provided by your domain name registrar and - provide to them this information:""") - print("") - print(" " + ds_correct) + if ds == None: + print_error("""This domain's DNS DS record is not set. The DS record is optional. The DS record activates DNSSEC. + To set a DS record, you must follow the instructions provided by your domain name registrar and provide to them this information:""") + else: + print_error("""This domain's DNS DS record is incorrect. The chain of trust is broken between the public DNS system + and this machine's DNS server. It may take several hours for public DNS to update after a change. If you did not recently + make a change, you must resolve this immediately by following the instructions provided by your domain name registrar and + provide to them this information:""") + print() + print("\tKey Tag: " + ds_keytag + ("" if not ds_looks_valid or ds[0] == ds_keytag else " (Got '%s')" % ds[0])) + print("\tKey Flags: KSK") + print("\tAlgorithm: 7 / RSASHA1-NSEC3-SHA1" + ("" if not ds_looks_valid or ds[1] == '7' else " (Got '%s')" % ds[1])) + print("\tDigest Type: 2 / SHA-256") + print("\tDigest: " + digests['2']) + if ds_looks_valid and ds[3] != digests.get(ds[2]): + print("\t(Got digest type %s and digest %s which do not match.)" % (ds[2], ds[3])) + print("\tPublic Key: " + dnsssec_pubkey) + print() + print("\tBulk/Record Format:") + print("\t" + ds_correct[0]) print("") def check_mail_domain(domain, env):