Subject: [dmarc-ietf] DNS library queries for DKIM and DMARC records?

From: Dave Crocker <[email-masked]>
Date: Wed, 10 Apr 2019 12:36:34 -0700

Folks,

Howdy.

I'm trying to get a bit of education about reality.  Always dangerous, 
but I've no choice...

For the software you know about, how are queries to the DNS performed, 
to obtain the TXT records associated with DKIM and/or DMARC?

I'm trying to understand the breadth and limitations of returned 
information that is filtered or passed by the code that is actually in 
use.  Which libraries and which calls from those libraries.

Thanks.

d/

-- 
Dave Crocker
Brandenburg InternetWorking
bbiw.net

---

From: Scott Kitterman <[email-masked]>
Date: Wed, 10 Apr 2019 23:38 -0400

> use.  Which libraries and which calls from those libraries.

I'm most familiar with how it's done with dkimpy (Python DKIM library) [1].  
It uses an internal abstraction layer (we call dnsplug) to that the module 
itself works with either pydns/py3dns [2] or dnspython.  I'm not sure exactly 
what you're after here, but  the code's simple enough, so here it is:

def get_txt_dnspython(name):
    """Return a TXT record associated with a DNS name."""
    try:
      a = dns.resolver.query(name, dns.rdatatype.TXT,raise_on_no_answer=False)
      for r in a.response.answer:
          if r.rdtype == dns.rdatatype.TXT:
              return b"".join(r.items[0].strings)
    except dns.resolver.NXDOMAIN: pass
    return None

def get_txt_pydns(name):
    """Return a TXT record associated with a DNS name."""
    # Older pydns releases don't like a trailing dot.
    if name.endswith('.'):
        name = name[:-1]
    response = DNS.DnsRequest(name, qtype='txt').req()
    if not response.answers:
        return None
    return b''.join(response.answers[0]['data'])

There is a detection process that will decide what to use and then define 
dnsplug.get_txt as the appropriate variant.

Not being sure how far you want to take this, here's an example of a TXT query 
using pydns or py3dns (I picked this one because it has multiple TXT records):

Code:

>>> import DNS
>>> response = DNS.DnsRequest('google.com', qtype='txt').req()
>>> print(response.answers)

Result (whitespace added for clarity):
[
{'classstr': 'IN', 'ttl': 299, 'rdlength': 46, 'class': 1, 'typename': 'TXT', 
'data': [b'docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e'], 'name': 
'google.com', 'type': 16},
 
{'classstr': 'IN', 'ttl': 3599, 'rdlength': 36, 'class': 1, 'typename': 'TXT', 
'data': [b'v=spf1 include:_spf.google.com ~all'], 'name': 'google.com', 
'type': 16}, 

{'classstr': 'IN', 'ttl': 3599, 'rdlength': 60, 'class': 1, 'typename': 'TXT', 
'data': [b'facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95'], 
'name': 'google.com', 'type': 16}, 

{'classstr': 'IN', 'ttl': 3599, 'rdlength': 65, 'class': 1, 'typename': 'TXT', 
'data': [b'globalsign-smime-dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8='], 
'name': 'google.com', 'type': 16}
]

For completeness, here's the balance of what you can pull out of the response 
object for this request:

>>> print(response.header)
{'ra': 1, 'qdcount': 1, 'opcode': 0, 'id': 46730, 'tc': 0, 'nscount': 0, 
'arcount': 0, 'z': 0, 'ancount': 4, 'aa': 0, 'status': 'NOERROR', 'qr': 1, 
'rd': 1, 'opcodestr': 'QUERY', 'rcode': 0}

>>> print(response.questions)
[{'qname': 'google.com', 'qtypestr': 'TXT', 'qclassstr': 'IN', 'qclass': 1, 
'qtype': 16}]

>>> print(response.authority)
[]

>>> print(response.additional)
[]

>>> print(response.args)
{'qtype': 'txt', 'port': 53, 'protocol': 'udp', 'elapsed': 95.10970115661621, 
'opcode': 0, 'server_rotate': 0, 'rd': 1, 'timeout': 30, 'timing': 1, 'name': 
'google.com', 'server': '127.0.1.1'}

What gets returned to the application from dnsplug is a list of the TXT data.  
In this case:

[b'docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e', b'v=spf1 
include:_spf.google.com ~all', b'facebook-domain-
verification=22rm551cu4k0ab0bxsw536tlds4h95', b'globalsign-smime-
dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8=']

Is that the kind of thing you're after?

Scott K

[1] https://git.launchpad.net/dkimpy/tree/dkim/dnsplug.py

[2] For pydns/py3dns there are separate code bases for python2 and python3, 
but the calls are the same.

---

From: "Murray S. Kucherawy" <[email-masked]>
Date: Wed, 10 Apr 2019 21:08:31 -0700

> to obtain the TXT records associated with DKIM and/or DMARC?

By default, libopendkim and libopendmarc use the standard C library
functions (i.e., res_*()) to do these queries and await replies.  The
specific functions are res_query() and res_nquery() depending on which
version of the library is available.

I believe both can be configured to use libunbound instead (certainly
libopendkim can).

-MSK

---

From: Dave Crocker <[email-masked]>
Date: Wed, 10 Apr 2019 21:16:56 -0700

>>>> print(response.additional)

Turns out that's what I was especially hoping to see.

Thanks!

d/

---

From: Scott Kitterman <[email-masked]>
Date: Thu, 11 Apr 2019 00:29:28 -0400

Great.  I checked and dnspython can do that too:

>>> import dns.resolver
>>> a = dns.resolver.query('google.com', dns.rdatatype.TXT)
>>> print(a.response.additional)
[]

Scott K

---

From: Scott Kitterman <[email-masked]>
Date: Thu, 11 Apr 2019 00:42:50 -0400

> >>> print(a.response.additional)

And just for fun python-getdns [1] has an even more interesting result to that 
same query:

{'additional': [{'do': 1, 'extended_rcode': 0, 'rdata': {}, 'type': 41, 
'udp_payload_size': 65535, 'version': 0, 'z': 0}]

As far as I know, the unbound python bindings don't do TXT, so that's all the 
Python libs I know about.

Scott K

[1] https://github.com/getdnsapi/getdns-python-bindings

---

From: "John Levine" <[email-masked]>
Date: 13 Apr 2019 23:20:48 -0400

>Turns out that's what I was especially hoping to see.

As I understand it, your design depends on putting NXDOMAIN signals
in the additional section to show that there aren't any boundaries
between the names it returns.  How do you plan to do that?

R's,
John

---

From: Dave Crocker <[email-masked]>
Date: Sat, 13 Apr 2019 20:40:41 -0700

> between the names it returns.  How do you plan to do that?

John, I don't understand your note.

I don't know what you mean by "NXDomain signals"  and I've no idea what 
you mean by "show that there aren't any boundaries between the names it 
returns."

The latter sound strange enough to make me suspect you read some draft 
other than mine.

And just to add a bit of constraint, my this sub-topic within the draft 
concerns a possible means of getting some efficiency in a search.  It's 
offered as a discussion session in the larger spec.

d/

---

From: "John R Levine" <[email-masked]>
Date: 13 Apr 2019 23:51:16 -0400

> John, I don't understand your note.

In draft-dcrocker-dns-perimeter-00, it says this:

    Another approach is use of the DNS Additional section in the server
    response.  When there is a query for a Perimeter node, the server
    would include the associated Perimeter BEGIN record from earlier in
    the hierarchy, if the queried node is within that hierarchy -- that
    is, is above the actual or virtual END record.

If you asked for _perim.a.b.c.example.com, and the perimeter is actually 
at "c", there, you hope that modified DNS servers will return NXDOMAIN and 
in the additional section add _perim.c.example.com.  But since the 
additional section info is just advisory, that doesn't tell you anything 
about _perim.b.c.example.com, which might exist or might not.  To avoid 
doing a tree walk, you'd need a signal that _perim.b.c.example.com does 
not exist, and there's no way to do that in an additional section.

Regards,
John Levine, [email-masked], Taughannock Networks, Trumansburg NY
Please consider the environment before reading this e-mail. https://jl.ly

---

From: Dave Crocker <[email-masked]>
Date: Sun, 14 Apr 2019 07:05:13 -0700

On 4/13/2019 8:51 PM, John R Levine wrote:
> at "c", there, you hope that modified DNS servers will return NXDOMAIN 
> and in the additional section add _perim.c.example.com.  

Good. That language seems about right.

> But since the 

The rest of your paragraph, again, is confusing and probably misleading.

First, by definition, the fact that NXDomain is returned means that 
_perim.b.c.example.com does not exist.  There is no need or suggestion 
that the Additional section also indicate that that name doesn't exist.

Rather, a query to such a non-existent domain will provide information 
that it doesn't exist by using the usual NXdomain response, except that 
response will /also/ have an Additional section, containing information 
about the node up the branch that contains the Perimeter 'begin'.

My draft doesn't yet offer a detailed specification for this.  It's 
phrased to explore an approach.  So the details of exactly what would go 
into the Additional section for an NXDomain response are tbd.  Let's 
wait to criticize or improve those details until after they've been written.

As for the concern about 'advisory', that merely means that the client 
would need to confirm the information from the Additional section. 
That's one more direct query to the referenced _perim.c.example.com.

Doing exactly one more query is already demonstrated to be acceptable, 
at least for some applications.

d/