Merge with backout
[prosody.git] / util / x509.lua
1 -- Prosody IM
2 -- Copyright (C) 2010 Matthew Wild
3 -- Copyright (C) 2010 Paul Aurich
4 --
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
8
9 -- TODO: I feel a fair amount of this logic should be integrated into Luasec,
10 -- so that everyone isn't re-inventing the wheel.  Dependencies on
11 -- IDN libraries complicate that.
12
13
14 -- [TLS-CERTS] - http://tools.ietf.org/html/rfc6125
15 -- [XMPP-CORE] - http://tools.ietf.org/html/rfc6120
16 -- [SRV-ID]    - http://tools.ietf.org/html/rfc4985
17 -- [IDNA]      - http://tools.ietf.org/html/rfc5890
18 -- [LDAP]      - http://tools.ietf.org/html/rfc4519
19 -- [PKIX]      - http://tools.ietf.org/html/rfc5280
20
21 local nameprep = require "util.encodings".stringprep.nameprep;
22 local idna_to_ascii = require "util.encodings".idna.to_ascii;
23 local log = require "util.logger".init("x509");
24 local pairs, ipairs = pairs, ipairs;
25 local s_format = string.format;
26 local t_insert = table.insert;
27 local t_concat = table.concat;
28
29 module "x509"
30
31 local oid_commonname = "2.5.4.3"; -- [LDAP] 2.3
32 local oid_subjectaltname = "2.5.29.17"; -- [PKIX] 4.2.1.6
33 local oid_xmppaddr = "1.3.6.1.5.5.7.8.5"; -- [XMPP-CORE]
34 local oid_dnssrv   = "1.3.6.1.5.5.7.8.7"; -- [SRV-ID]
35
36 -- Compare a hostname (possibly international) with asserted names
37 -- extracted from a certificate.
38 -- This function follows the rules laid out in
39 -- sections 6.4.1 and 6.4.2 of [TLS-CERTS]
40 --
41 -- A wildcard ("*") all by itself is allowed only as the left-most label
42 local function compare_dnsname(host, asserted_names)
43         -- TODO: Sufficient normalization?  Review relevant specs.
44         local norm_host = idna_to_ascii(host)
45         if norm_host == nil then
46                 log("info", "Host %s failed IDNA ToASCII operation", host)
47                 return false
48         end
49
50         norm_host = norm_host:lower()
51
52         local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label
53
54         for i=1,#asserted_names do
55                 local name = asserted_names[i]
56                 if norm_host == name:lower() then
57                         log("debug", "Cert dNSName %s matched hostname", name);
58                         return true
59                 end
60
61                 -- Allow the left most label to be a "*"
62                 if name:match("^%*%.") then
63                         local rest_name = name:gsub("^[^.]+%.", "")
64                         if host_chopped == rest_name:lower() then
65                                 log("debug", "Cert dNSName %s matched hostname", name);
66                                 return true
67                         end
68                 end
69         end
70
71         return false
72 end
73
74 -- Compare an XMPP domain name with the asserted id-on-xmppAddr
75 -- identities extracted from a certificate.  Both are UTF8 strings.
76 --
77 -- Per [XMPP-CORE], matches against asserted identities don't include
78 -- wildcards, so we just do a normalize on both and then a string comparison
79 --
80 -- TODO: Support for full JIDs?
81 local function compare_xmppaddr(host, asserted_names)
82         local norm_host = nameprep(host)
83
84         for i=1,#asserted_names do
85                 local name = asserted_names[i]
86
87                 -- We only want to match against bare domains right now, not
88                 -- those crazy full-er JIDs.
89                 if name:match("[@/]") then
90                         log("debug", "Ignoring xmppAddr %s because it's not a bare domain", name)
91                 else
92                         local norm_name = nameprep(name)
93                         if norm_name == nil then
94                                 log("info", "Ignoring xmppAddr %s, failed nameprep!", name)
95                         else
96                                 if norm_host == norm_name then
97                                         log("debug", "Cert xmppAddr %s matched hostname", name)
98                                         return true
99                                 end
100                         end
101                 end
102         end
103
104         return false
105 end
106
107 -- Compare a host + service against the asserted id-on-dnsSRV (SRV-ID)
108 -- identities extracted from a certificate.
109 --
110 -- Per [SRV-ID], the asserted identities will be encoded in ASCII via ToASCII.
111 -- Comparison is done case-insensitively, and a wildcard ("*") all by itself
112 -- is allowed only as the left-most non-service label.
113 local function compare_srvname(host, service, asserted_names)
114         local norm_host = idna_to_ascii(host)
115         if norm_host == nil then
116                 log("info", "Host %s failed IDNA ToASCII operation", host);
117                 return false
118         end
119
120         -- Service names start with a "_"
121         if service:match("^_") == nil then service = "_"..service end
122
123         norm_host = norm_host:lower();
124         local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label
125
126         for i=1,#asserted_names do
127                 local asserted_service, name = asserted_names[i]:match("^(_[^.]+)%.(.*)");
128                 if service == asserted_service then
129                         if norm_host == name:lower() then
130                                 log("debug", "Cert SRVName %s matched hostname", name);
131                                 return true;
132                         end
133
134                         -- Allow the left most label to be a "*"
135                         if name:match("^%*%.") then
136                                 local rest_name = name:gsub("^[^.]+%.", "")
137                                 if host_chopped == rest_name:lower() then
138                                         log("debug", "Cert SRVName %s matched hostname", name)
139                                         return true
140                                 end
141                         end
142                         if norm_host == name:lower() then
143                                 log("debug", "Cert SRVName %s matched hostname", name);
144                                 return true
145                         end
146                 end
147         end
148
149         return false
150 end
151
152 function verify_identity(host, service, cert)
153         local ext = cert:extensions()
154         if ext[oid_subjectaltname] then
155                 local sans = ext[oid_subjectaltname];
156
157                 -- Per [TLS-CERTS] 6.3, 6.4.4, "a client MUST NOT seek a match for a
158                 -- reference identifier if the presented identifiers include a DNS-ID
159                 -- SRV-ID, URI-ID, or any application-specific identifier types"
160                 local had_supported_altnames = false
161
162                 if sans[oid_xmppaddr] then
163                         had_supported_altnames = true
164                         if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
165                 end
166
167                 if sans[oid_dnssrv] then
168                         had_supported_altnames = true
169                         -- Only check srvNames if the caller specified a service
170                         if service and compare_srvname(host, service, sans[oid_dnssrv]) then return true end
171                 end
172
173                 if sans["dNSName"] then
174                         had_supported_altnames = true
175                         if compare_dnsname(host, sans["dNSName"]) then return true end
176                 end
177
178                 -- We don't need URIs, but [TLS-CERTS] is clear.
179                 if sans["uniformResourceIdentifier"] then
180                         had_supported_altnames = true
181                 end
182
183                 if had_supported_altnames then return false end
184         end
185
186         -- Extract a common name from the certificate, and check it as if it were
187         -- a dNSName subjectAltName (wildcards may apply for, and receive,
188         -- cat treats)
189         --
190         -- Per [TLS-CERTS] 1.8, a CN-ID is the Common Name from a cert subject
191         -- which has one and only one Common Name
192         local subject = cert:subject()
193         local cn = nil
194         for i=1,#subject do
195                 local dn = subject[i]
196                 if dn["oid"] == oid_commonname then
197                         if cn then
198                                 log("info", "Certificate has multiple common names")
199                                 return false
200                         end
201
202                         cn = dn["value"];
203                 end
204         end
205
206         if cn then
207                 -- Per [TLS-CERTS] 6.4.4, follow the comparison rules for dNSName SANs.
208                 return compare_dnsname(host, { cn })
209         end
210
211         -- If all else fails, well, why should we be any different?
212         return false
213 end
214
215 return _M;