Core Question
Core Question
What does the smallest object look like that can be handed a name, a type, and a signed answer, and say — with a full trace — whether that answer is provably correct, provably wrong, or provably absent of proof?
Outcome
Outcome
By the end of this session, the learner should be able to:
- name every field on
ToyValidatingResolverand which earlier session's module owns the logic behind it - explain why
resolve_and_validate()checks the answer's name and type *before* calling intovalidate_chain() - read a
ValidationReport'strace_sliceand say which zone and which key produced each line - run the demo tree through all four
ChainOutcomevalues using the same resolver and the same starting tree
Read Order
Read Order
- Read the module comment above
ToyValidatingResolverabout what it trusts and what it computes - Read
ValidationReportandformat_trace() - Read
ToyValidatingResolver's field list - Read
resolve_and_validate()top to bottom - Read
build_demo_tree() - Read
tamper_record(),tamper_ds(),unsign_child() - Run
examples/dnssec/session_05_walkthrough.py
Read It Like Code
Read It Like Code
ToyValidatingResolver(
zones,
trust_anchor,
clock,
trace,
)Parts List
Parts List
Every field and every function validator_loop.py imports was taught by an earlier session in this track. The capstone's only new code is the name/type guard in resolve_and_validate() and the demo-tree wiring below it.
| Import | Session that taught it | What it contributes to ToyValidatingResolver |
|---|---|---|
rrsig.Rrset, Rrsig, sign_rrset | 01 | The signed-answer shape every scenario is built from; verify_rrsig() (called inside validate_chain()) is the first check on every hop. |
dnskey.DnsKey, KeyRole, make_key, sign_key_rrset | 02 | The KSK/ZSK pair every zone in build_demo_tree() holds, and the signature that binds them into one DNSKEY rrset. |
ds.Ds, make_ds | 03 | trust_anchor's type, and the cross-zone fingerprint ds_for_children stores at every parent. |
chain.ChainOutcome, ChainResult, ZoneData, validate_chain | 04 | zones's element type, and the entire walk resolve_and_validate() delegates to after its own guard clause. |
Decision Flow
Decision Flow
resolve_and_validate(name, rtype, rrset, rrsig):
1. rrset.name != name -> BOGUS_RECORD_SIG, trace: "answer name mismatch", stop
2. rrset.rtype != rtype -> BOGUS_RECORD_SIG, trace: "answer type mismatch", stop
3. otherwise -> validate_chain(rrset, rrsig, zones, trust_anchor, clock)
result appended to self.trace, returned as ValidationReportvalidate_chain() itself is Session 04's walk, unchanged: record RRSIG under the ZSK, DNSKEY rrset under the KSK, KSK hash against the parent's DS, repeated to the trust anchor.
Reading Lens
Reading Lens
The important move in this session is to stop reading chain.py and validator_loop.py as two separate things and start asking:
- what does this resolver actually trust on say-so, versus what does it prove by recomputing a hash or a signature?
- which line of
resolve_and_validate()runs *before*validate_chain()is even called, and why does a name/type mismatch reuseBOGUS_RECORD_SIGrather than get its own outcome? - what does advancing
self.clockwithtick()change about a trace that was SECURE a moment ago?
The capstone's thesis, stated directly in the module comment: the resolver trusts exactly one key it was *given* — trust_anchor, pinned out of band, never verified against anything else — and every other fact along the way — each ZSK, each KSK, each DS digest — it *computes* its way up to by recomputing a sha256 and comparing. Nothing else is trusted on say-so. Read every ChainOutcome other than the missing-key/missing-zone branches as evidence of that: SECURE means the arithmetic closed the loop all the way to trust_anchor; every BOGUS variant means one specific piece of arithmetic didn't match; INSECURE means there was no arithmetic to run.
Toy Model Boundary
Toy Model Boundary
ToyValidatingResolver is handed the whole zones tree and the signed answer up front — it does not fetch DNSKEY, DS, or RRSIG records itself over the network. A real validating resolver (RFC 4035 §5) performs that fan-out hop by hop; the DNS track's walk_from_root() and referral machinery already model the transport side of that walk. Keeping the two apart is deliberate: this module is only about what happens once the records are in hand.
There is no authenticated denial of existence here (no NSEC/NSEC3) — see Session 04's Toy Model Boundary for why unsign_child()'s INSECURE result is trusted at face value rather than proven. There are no signature algorithms and no key rollover: a KSK or ZSK in this toy is a single unchanging secret string for the lifetime of the scenario. Expiration is real arithmetic (now >= rrsig.expiration), but there is no mechanism here for a resolver to notice a key is *about* to expire or to fetch a replacement — tick() only advances an integer.
Code Landmarks
Code Landmarks
The module comment above ToyValidatingResolver
States the capstone thesis directly: one key given, everything else computed. Read it before the class.
resolve_and_validate()'s two guard clauses
if rrset.name != name:
...
return ValidationReport(ChainOutcome.BOGUS_RECORD_SIG, (self.trace[-1],))
if rrset.rtype != rtype:
...
return ValidationReport(ChainOutcome.BOGUS_RECORD_SIG, (self.trace[-1],))Both mismatches reuse ChainOutcome.BOGUS_RECORD_SIG rather than a dedicated value — an answer that does not even match the question asked is treated the same as an answer whose signature failed, because in both cases the resolver refuses to trust what it was handed. Neither guard calls into validate_chain() at all; the chain walk never sees a mismatched answer.
self.trace versus a report's trace_slice
self.trace accumulates across every call the resolver makes over its lifetime; ValidationReport.trace_slice is only the lines from the *current* call. format_trace() renders a report's slice, not the resolver's whole history — the walkthrough's demo prints one call's story, not the resolver's full log.
build_demo_tree()'s delegation wiring
root_data.ds_for_children["com"] = make_ds(com_ksk) com_data.ds_for_children["example.com"] = make_ds(example_ksk)
This is the entire tree's trust structure in two lines: root vouches for com's KSK, com vouches for example.com's KSK. trust_anchor itself is make_ds(root_ksk) — the same function, applied one more time, with nothing above it.
The three knob functions
tamper_record() returns a bare Rrset, meant to be fed alongside the *original* rrsig — the signature still covers the old records, so it silently stops matching. tamper_ds() and unsign_child() both return a whole new DemoTree with com's ZoneData swapped out, because both need to change what the parent zone publishes, not the leaf answer.
Failure Questions
Failure Questions
Use the source file to answer these:
resolve_and_validate()checksrrset.name != nameandrrset.rtype != rtypebefore callingvalidate_chain(). WhatChainOutcomedo both of these guard clauses produce, and why does that choice mean the walkthrough cannot distinguish "wrong name" from "signature forged" just by reading the outcome value alone?ToyValidatingResolver.traceandValidationReport.trace_sliceare two different lists holding overlapping content. If you callresolve_and_validate()three times on the same resolver, how many lines doesself.tracehold afterward, compared to any single call'strace_slice?trust_anchoris typed as aDs, built bymake_ds(root_ksk)inbuild_demo_tree(). What doesvalidate_chain()compare it against at the root, and what function performs that comparison?tick()only ever doesself.clock += seconds. Trace what changes insidevalidate_chain()'s first check (verify_rrsig()on the record) iftick()is called with a value larger thanDEFAULT_EXPIRATIONbeforeresolve_and_validate()runs.unsign_child()deletescom_data.ds_for_children["example.com"]and returns a newDemoTree. Why does this function need to rebuildZoneDataforcominstead of mutatingtree.zones["com"].ds_for_childrendirectly?
Walkthrough
Walkthrough
Run this:
PYTHONPATH=src python3 examples/dnssec/session_05_walkthrough.py
The walkthrough builds one ToyValidatingResolver over build_demo_tree()'s zones and trust anchor, resolves example.com A and confirms SECURE with the verified 7-line trace ending on the trust-anchor match, checks that format_trace() carries the per-level lines a doc author would paste under a demo, then runs each knob — tamper_record, tamper_ds, unsign_child — through a fresh resolver built over the knob's tampered tree and confirms each lands on its ChainOutcome. It finishes by advancing a resolver's clock past DEFAULT_EXPIRATION with tick() and confirming the record signature is caught as expired at the very first hop, and by asking for an answer under the wrong name and confirming the guard clause fires before any chain walk begins.
Done When
Done When
The learner can say all of the following without looking at notes:
- "The resolver trusts one Ds it was given — trust_anchor — and arithmetics its way to everything else."
- "resolve_and_validate() checks the answer matches the question before it ever calls validate_chain(); a mismatch never reaches the chain walk."
- "A SECURE trace is always the same shape for the same tree depth: one record-sig line, then a key-sig/DS-match pair per zone, ending on the trust-anchor match."
- "Advancing the clock past a signature's expiration turns SECURE into BOGUS_RECORD_SIG at the very first hop — the record's own zone catches it before the walk ever reaches the parent."
- "Everything in this file was built in Sessions 01 through 04; validator_loop.py only wires build_demo_tree, the three knobs, and the name/type guard around validate_chain()."
Bolt-On Point
Bolt-On Point
ToyValidatingResolver is handed a finished (rrset, rrsig) pair — it never fetches one. The DNS track's ToyResolver (src/protocol_in_code/dns/resolver.py, DNS Session 08) is the component that actually walks the tree over a transport and produces records; bolting the two together means DNS Session 08's resolver would fetch the RRSIG alongside the record it already fetches, then hand both to resolve_and_validate() before trusting the answer. Neither file currently does this wiring — it is the natural next exercise once both capstones are read, not something either capstone builds for you.
References
References
- RFC 4033 (DNS Security Introduction and Requirements — the "resolver trusts one key" framing this session's thesis is built on)
- RFC 4035 Section 5 (Authenticating DNS Responses — the validation algorithm this module's
resolve_and_validate()is a toy version of)
Continue