feat: Add temporary frontend and started on dnssec

This commit is contained in:
Nathan Woodburn 2025-02-25 18:30:29 +11:00
parent af89fca45d
commit 12263dc7b5
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
4 changed files with 344 additions and 27 deletions

View File

@ -1,20 +1,7 @@
body {
background-color: #000000;
color: #ffffff;
}
h1 {
font-size: 50px;
margin: 0;
padding: 0;
.spacer {
margin-top: 20px;
}
.centre {
margin-top: 10%;
margin-top: 50px;
text-align: center;
}
a {
color: #ffffff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,59 @@
// Add event listener to button
document.getElementById("ssl").addEventListener("click", function () {
getSSL();
});
// Add enter key listener to input
document.getElementById("domain").addEventListener("keyup", function (event) {
if (event.key === "Enter") {
getSSL();
}
});
function getSSL() {
// Get the input value
const domain = document.getElementById("domain").value;
// Send a GET request to the API
fetch(`/api/v1/ssl/${domain}`)
.then(response => response.json())
.then(data => {
// Check if the request was successful
if (data.success) {
// Display the results
// document.getElementById("results").innerHTML = `<h2>SSL Certificate Details</h2>
// <p>IP Address: ${data.ip}</p>
// <p>Webserver TLSA: <code>${data.tlsa.server}</code></p>
// <p>Nameserver TLSA: <code>${data.tlsa.nameserver}</code> ${data.tlsa.match ? "(Match)" : "(No Match)"}</p>
// <p>Certificate Names: ${data.cert.domains.join(", ")}</p>
// <p>Expiry Date: ${data.cert.expiry_date} UTC</p>
// <p>Valid: ${data.cert.valid ? "Yes" : "No"}</p>`;
document.getElementById("results").innerHTML = `
<div class="card shadow-sm p-4" style="max-width: 950px;margin: auto;">
<h2 class="mb-3">SSL Certificate Details</h2>
<ul class="list-group">
<li class="list-group-item"><strong>IP Address:</strong> ${data.ip}</li>
<li class="list-group-item"><strong>Webserver TLSA:</strong> <code>${data.tlsa.server}</code></li>
<li class="list-group-item"><strong>Nameserver TLSA:</strong> <code>${data.tlsa.nameserver}</code>
${data.tlsa.match ? '<span class="badge bg-success">Match</span>' : '<span class="badge bg-danger">No Match</span>'}
</li>
<li class="list-group-item"><strong>Certificate Names:</strong> ${data.cert.domains.join(", ")}</li>
<li class="list-group-item"><strong>Expiry Date:</strong> ${data.cert.expiry_date} UTC</li>
<li class="list-group-item"><strong>Valid:</strong>
${data.valid ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-danger">No</span>'}
</li>
</ul>
</div>`;
} else {
// Display an error message
document.getElementById("results").innerHTML = `<h2>Error</h2>
<p>${data.message}</p>`;
}
})
.catch(error => {
// Display an error message
document.getElementById("results").innerHTML = `<h2>Error</h2>
<p>${error.message}</p>`;
});
}

View File

@ -1,18 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nathan.Woodburn/</title>
<title>HNS Tools | Nathan.Woodburn/</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/index.css">
<script src="/assets/js/index.js" defer></script>
</head>
<body>
<div class="spacer"></div>
<div class="centre" style="margin-top: 20px;">
<h1>HNS Tools</h1>
<h2>Nathan.Woodburn/</h2>
</div>
<div class="centre">
<h1>Nathan.Woodburn/</h1>
<h2>SSL/DANE Checker</h2>
<input type="text" id="domain" placeholder="Domain to check">
<button id="ssl">Validate SSL</button>
<div id="results" style="margin-top: 20px;"></div>
</div>
</body>

277
tools.py
View File

@ -5,27 +5,37 @@ import binascii
from cryptography import x509
from cryptography.hazmat.backends import default_backend
import datetime
from dns import resolver, dnssec, name, exception
import time
resolver = dns.resolver.Resolver()
resolver.nameservers = ["194.50.5.28","194.50.5.27","194.50.5.26"]
resolver.port = 53
domain_check = False
def check_ssl(domain: str):
domain_check = False
returns = {"success": False,"valid":False}
try:
# Query the DNS record
response = resolver.resolve(domain, "A")
records = []
records = []
for record in response:
records.append(str(record))
if not records:
return {"success": False, "message": "No A record found for " + domain}
returns["IP"] = records[0]
returns["IPS"] = records
# Get the first A record
returns["ip"] = records[0]
if len(records) > 1:
returns["other_ips"] = records[1:]
result, details = validate_dnssec(domain, trace=True)
returns["dnssec"] = details
returns["dnssec"]["valid"] = result
# Get the first A record
ip = records[0]
# Run the openssl s_client command
@ -144,4 +154,257 @@ def check_ssl(domain: str):
# Catch all exceptions
except Exception as e:
returns["message"] = f"An error occurred: {e}"
return returns
return returns
def validate_dnssec(domain, trace=False):
"""
Validate DNSSEC for a given domain by following the chain of trust.
Args:
domain (str): The domain to validate
trace (bool): Enable detailed tracing of validation steps
Returns:
bool: True if DNSSEC validation succeeds, False otherwise
dict: Details about the validation process
"""
domain = dns.name.from_text(domain)
if not domain.is_absolute():
domain = domain.concatenate(dns.name.root)
# Start with root servers
trusted_keys = get_root_trust_anchors()
result = {
'domain': str(domain),
'validation_time': time.time(),
'validated': False,
'validation_chain': [],
'errors': []
}
try:
# Walk the DNS hierarchy to validate the chain
current = dns.name.root
while current != domain and current.is_subdomain(domain):
# Determine the next label in the chain
next_part = domain.relativize(current).split()[0]
next_domain = dns.name.Name([next_part]) + current
# Get DNSKEY records for the current zone
dnskey_response = query_dns(current, dns.rdatatype.DNSKEY)
dnskey_rrset = extract_rrset_from_response(dnskey_response, current, dns.rdatatype.DNSKEY)
if not dnskey_rrset:
error = f"No DNSKEY found for {current}"
result['errors'].append(error)
return False, result
# Validate current zone's DNSKEY against trusted keys
if not validate_rrset(dnskey_rrset, trusted_keys):
error = f"Failed to validate DNSKEY for {current}"
result['errors'].append(error)
return False, result
# Get DS records for the next domain
ds_response = query_dns(current, dns.rdatatype.DS, next_domain)
ds_rrset = extract_rrset_from_response(ds_response, next_domain, dns.rdatatype.DS)
if not ds_rrset:
error = f"No DS records found for {next_domain}"
result['errors'].append(error)
return False, result
# Validate DS records with the trusted keys from the parent zone
if not validate_rrset(ds_rrset, trusted_keys):
error = f"Failed to validate DS records for {next_domain}"
result['errors'].append(error)
return False, result
# Now get DNSKEYs for the next domain level
next_dnskey_response = query_dns(next_domain, dns.rdatatype.DNSKEY)
next_dnskey_rrset = extract_rrset_from_response(next_dnskey_response, next_domain, dns.rdatatype.DNSKEY)
if not next_dnskey_rrset:
error = f"No DNSKEY found for {next_domain}"
result['errors'].append(error)
return False, result
# Verify the DNSKEY matches the DS record hash
if not match_ds_to_dnskey(ds_rrset, next_dnskey_rrset):
error = f"DS record does not match DNSKEY for {next_domain}"
result['errors'].append(error)
return False, result
# The next domain's DNSKEYs become our trusted keys
trusted_keys = next_dnskey_rrset
current = next_domain
result['validation_chain'].append({
'zone': str(current),
'validated': True,
'timestamp': time.time()
})
# Finally, validate the actual records we're interested in (A, AAAA, etc)
for rdtype in [dns.rdatatype.A, dns.rdatatype.TLSA]:
if rdtype == dns.rdatatype.TLSA:
record_response = query_dns(f"_443._tcp.{domain}", rdtype)
else:
record_response = query_dns(domain, rdtype)
if record_response.answer:
record_rrset = extract_rrset_from_response(record_response, domain, rdtype)
if record_rrset and not validate_rrset(record_rrset, trusted_keys):
error = f"Failed to validate {dns.rdatatype.to_text(rdtype)} records for {domain}"
result['errors'].append(error)
return False, result
result['validated'] = True
return True, result
except Exception as e:
error = f"Validation error: {str(e)}"
result['errors'].append(error)
return False, result
def get_root_trust_anchors():
"""
Get the root trust anchors (usually the root DNSKEY)
In practice, this would be configured or obtained from a trusted source.
"""
try:
# For production use, you should use properly validated root anchors
# This is a simplified example
response = resolver.query('.', dns.rdatatype.DNSKEY)
return response.rrset
except Exception as e:
# Fallback to hardcoded root KSK if needed
# This should be updated when the root KSK changes
from dns.rdtypes.ANY.DNSKEY import DNSKEY
from dns.rdata import from_text
# Root KSK-2017 (hardcoded as fallback only - you should obtain this securely)
root_ksk = ". IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU="
# Create a DNS rrset
dnskey_rdata = from_text(dns.rdataclass.IN, dns.rdatatype.DNSKEY, root_ksk)
rrset = dns.rrset.RRset(dns.name.root, dns.rdataclass.IN, dns.rdatatype.DNSKEY)
rrset.add(dnskey_rdata)
return rrset
def query_dns(domain, rdtype, qname=None):
"""
Query DNS for specific record types
Args:
domain (dns.name.Name): Domain to query from (name server authority)
rdtype (int): DNS record type
qname (dns.name.Name, optional): Specific name to query. If None, uses domain
Returns:
dns.message.Message: DNS response
"""
if qname is None:
qname = domain
try:
# For more control, you could use dns.query directly to a specific server
response = resolver.query(qname, rdtype, raise_on_no_answer=False)
return response.response
except dns.resolver.NXDOMAIN:
# Create an empty response with NXDOMAIN code
response = dns.message.make_response(dns.message.make_query(qname, rdtype))
response.set_rcode(dns.rcode.NXDOMAIN)
return response
except Exception as e:
# Return empty response
response = dns.message.make_response(dns.message.make_query(qname, rdtype))
response.set_rcode(dns.rcode.SERVFAIL)
return response
def extract_rrset_from_response(response, name, rdtype):
"""
Extract the RRset from a DNS response
Args:
response (dns.message.Message): DNS response
name (dns.name.Name): Name of the RRset
rdtype (int): Record type
Returns:
dns.rrset.RRset or None: The requested RRset or None if not found
"""
if response.rcode() != dns.rcode.NOERROR:
return None
for section in [response.answer, response.authority, response.additional]:
for rrset in section:
if rrset.name == name and rrset.rdtype == rdtype:
return rrset
return None
def validate_rrset(rrset, keys):
"""
Validate a DNS RRset using DNSSEC
Args:
rrset (dns.rrset.RRset): The RRset to validate
keys (dns.rrset.RRset): The DNSKEY rrset to validate against
Returns:
bool: True if the RRset validates, False otherwise
"""
try:
# Get the RRSIG for this RRset
for rrsig in [r for r in rrset.rrsigs]:
# Find the corresponding key
key = None
for dnskey in keys:
if dnskey.key_tag() == rrsig.key_tag:
key = dnskey
break
if key:
# Perform the validation
dns.dnssec.validate_rrsig(rrset, rrsig, {(keys.name, keys.rdtype): keys})
return True
return False
except dns.dnssec.ValidationFailure as e:
return False
except Exception as e:
return False
def match_ds_to_dnskey(ds_rrset, dnskey_rrset):
"""
Verify that a DS record matches a DNSKEY
Args:
ds_rrset (dns.rrset.RRset): DS record set
dnskey_rrset (dns.rrset.RRset): DNSKEY record set
Returns:
bool: True if any DS matches any DNSKEY, False otherwise
"""
for ds in ds_rrset:
for dnskey in dnskey_rrset:
# Calculate the DS from the DNSKEY
calculated_ds = dns.dnssec.make_ds(dnskey_rrset.name, dnskey, ds.digest_type)
# Compare the calculated DS to the actual DS
if (ds.key_tag == calculated_ds.key_tag and
ds.algorithm == calculated_ds.algorithm and
ds.digest_type == calculated_ds.digest_type and
ds.digest == calculated_ds.digest):
return True
return False