Skip to content

Commit 584dead

Browse files
author
Shumon Huque
committed
Redo usage=3 checks, set SNI, omit hostname mismatch checks per spec.
1 parent ca7c70f commit 584dead

File tree

1 file changed

+91
-38
lines changed

1 file changed

+91
-38
lines changed

examples/checkdanecert.py

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,49 @@
11
#!/usr/bin/env python
22
#
3+
# Validate a TLS certificate with DANE-EE usage.
34
# Get a TLS certificate from a HTTP server and verify it with
4-
# DANE/DNSSEC. Only supports TLSA usage type 3 (DANE-EE).
5+
# DANE/DNSSEC. Only supports TLSA usage=3 (DANE-EE)
56
#
67

7-
import sys, socket, hashlib
8+
import os.path, sys, socket, hashlib
89
from M2Crypto import SSL, X509
910
import getdns
1011

1112

13+
def usage():
14+
print """\
15+
Usage: %s [hostname] [port]\
16+
""" % os.path.basename(sys.argv[0])
17+
sys.exit(1)
18+
19+
1220
def compute_hash(func, string):
1321
"""compute hash of string using given hash function"""
1422
h = func()
1523
h.update(string)
1624
return h.hexdigest()
1725

1826

19-
def get_tlsa_rdata_set(replies):
27+
def get_addresses(hostname):
28+
29+
extensions = {
30+
"return_both_v4_and_v6" : getdns.GETDNS_EXTENSION_TRUE
31+
}
32+
ctx = getdns.Context()
33+
results = ctx.address(name=hostname, extensions=extensions)
34+
status = results['status']
35+
36+
address_list = []
37+
if status == getdns.GETDNS_RESPSTATUS_GOOD:
38+
for addr in results['just_address_answers']:
39+
address_list.append((addr['address_type'], addr['address_data']))
40+
else:
41+
print "getdns.address(): failed, return code: %d" % status
42+
43+
return address_list
44+
45+
46+
def get_tlsa_rdata_set(replies, requested_usage=None):
2047
tlsa_rdata_set = []
2148
for reply in replies:
2249
for rr in reply['answer']:
@@ -27,34 +54,33 @@ def get_tlsa_rdata_set(replies):
2754
matching_type = rdata['matching_type']
2855
cadata = rdata['certificate_association_data']
2956
cadata = str(cadata).encode('hex')
30-
tlsa_rdata_set.append(
31-
(usage, selector, matching_type, cadata) )
57+
if usage == requested_usage:
58+
tlsa_rdata_set.append(
59+
(usage, selector, matching_type, cadata) )
3260
return tlsa_rdata_set
3361

3462

3563
def get_tlsa(port, proto, hostname):
3664

65+
extensions = {
66+
"dnssec_return_only_secure" : getdns.GETDNS_EXTENSION_TRUE,
67+
}
3768
qname = "_%d._%s.%s" % (port, proto, hostname)
3869
ctx = getdns.Context()
39-
extensions = { "dnssec_return_only_secure": getdns.GETDNS_EXTENSION_TRUE }
4070
results = ctx.general(name=qname,
4171
request_type=getdns.GETDNS_RRTYPE_TLSA,
4272
extensions=extensions)
4373
status = results['status']
4474

4575
if status == getdns.GETDNS_RESPSTATUS_GOOD:
46-
return get_tlsa_rdata_set(results['replies_tree'])
76+
return get_tlsa_rdata_set(results['replies_tree'], requested_usage=3)
4777
else:
48-
print "getdns: failed looking up TLSA record, code: %d" % status
78+
print "getdns.general(): failed, return code: %d" % status
4979
return None
5080

5181

5282
def verify_tlsa(cert, usage, selector, matchtype, hexdata1):
5383

54-
if usage != 3:
55-
print "Only TLSA usage type 3 is currently supported"
56-
return
57-
5884
if selector == 0:
5985
certdata = cert.as_der()
6086
elif selector == 1:
@@ -79,35 +105,62 @@ def verify_tlsa(cert, usage, selector, matchtype, hexdata1):
79105

80106
if __name__ == '__main__':
81107

82-
hostname, port = sys.argv[1:]
83-
port = int(port)
84-
tlsa_rdata_set = get_tlsa(port, "tcp", hostname)
85-
86-
ctx = SSL.Context()
108+
try:
109+
hostname, port = sys.argv[1:]
110+
port = int(port)
111+
except:
112+
usage()
87113

88-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
89-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
114+
tlsa_rdata_set = get_tlsa(port, "tcp", hostname)
90115

91-
connection = SSL.Connection(ctx, sock=sock)
92-
connection.connect((hostname, port))
116+
for (iptype, ipaddr) in get_addresses(hostname):
93117

94-
chain = connection.get_peer_cert_chain()
95-
# Get the first certificate from the chain (which will be the EE cert)
96-
cert = chain[0]
118+
print "Connecting to %s at address %s ..." % (hostname, ipaddr)
119+
ctx = SSL.Context()
97120

98-
# find a matching TLSA record entry for the certificate
99-
tlsa_match = False
100-
for (usage, selector, matchtype, hexdata) in tlsa_rdata_set:
101-
if verify_tlsa(cert, usage, selector, matchtype, hexdata):
102-
tlsa_match = True
103-
print "Certificate matched TLSA record %d %d %d %s" % \
104-
(usage, selector, matchtype, hexdata)
121+
if iptype == "IPv4":
122+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
123+
elif iptype == "IPv6":
124+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
105125
else:
106-
print "Certificate did not match TLSA record %d %d %d %s"% \
107-
(usage, selector, matchtype, hexdata)
108-
if tlsa_match:
109-
print "Found at least one matching TLSA record"
110-
111-
connection.close()
112-
ctx.close()
126+
raise ValueError, "Unknown address type: %s" % iptype
127+
128+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
129+
connection = SSL.Connection(ctx, sock=sock)
130+
131+
# set TLS SNI extension if available in M2Crypto on this platform
132+
# Note: the official M2Crypto release does not yet (as of late 2014)
133+
# have support for SNI, sigh, but patches exist.
134+
try:
135+
connection.set_tlsext_host_name(hostname)
136+
except AttributeError:
137+
pass
138+
139+
# Per https://tools.ietf.org/html/draft-ietf-dane-ops, for DANE-EE
140+
# usage, certificate identity checks are based solely on the TLSA
141+
# record, so we ignore name mismatch conditions in the certificate.
142+
try:
143+
connection.connect((ipaddr, port))
144+
except SSL.Checker.WrongHost:
145+
pass
146+
147+
chain = connection.get_peer_cert_chain()
148+
cert = chain[0]
149+
150+
# find a matching TLSA record entry for the certificate
151+
tlsa_match = False
152+
for (usage, selector, matchtype, hexdata) in tlsa_rdata_set:
153+
if verify_tlsa(cert, usage, selector, matchtype, hexdata):
154+
tlsa_match = True
155+
print "Matched TLSA record %d %d %d %s" % \
156+
(usage, selector, matchtype, hexdata)
157+
else:
158+
print "Didn't match TLSA record %d %d %d %s"% \
159+
(usage, selector, matchtype, hexdata)
160+
161+
if not tlsa_match:
162+
print "No Matching DANE-EE TLSA record found."
163+
164+
connection.close()
165+
ctx.close()
113166

0 commit comments

Comments
 (0)