Fix issue when validating a response containing the original wildcard record
[captive-validator.git] / src / com / verisign / cl / DNSSECValTool.java
1 package com.verisign.cl;
2
3 import java.io.*;
4 import java.net.SocketTimeoutException;
5 import java.util.*;
6
7 import org.apache.log4j.BasicConfigurator;
8 import org.apache.log4j.Level;
9 import org.apache.log4j.Logger;
10 import org.xbill.DNS.*;
11
12 import com.verisign.tat.dnssec.CaptiveValidator;
13 import com.verisign.tat.dnssec.SecurityStatus;
14 import com.verisign.tat.dnssec.Util;
15
16 public class DNSSECValTool {
17
18     /**
19      * Invoke with java -jar dnssecreconciler.jar server=127.0.0.1 \
20      * query_file=queries.txt dnskey_query=net dnskey_query=edu
21      */
22     private CaptiveValidator validator;
23     private SimpleResolver resolver;
24
25     private BufferedReader queryStream;
26     private PrintStream errorStream;
27     private Set<Name> zoneNames;
28
29     // Options
30     public String server;
31     public String query;
32     public String queryFile;
33     public String dnskeyFile;
34     public List<String> dnskeyNames;
35     public String errorFile;
36     public long count = 0;
37     public boolean debug = false;
38
39     DNSSECValTool() {
40         validator = new CaptiveValidator();
41     }
42
43     /**
44      * Convert a query line of the form: <qname> <qtype> <flags> to a request
45      * message.
46      * 
47      * @param query_line
48      * @return A query message
49      * @throws TextParseException
50      * @throws NameTooLongException
51      */
52     private Message queryFromString(String query_line)
53             throws TextParseException, NameTooLongException {
54
55         String[] tokens = query_line.split("[ \t]+");
56
57         Name qname = null;
58         int qtype = -1;
59         int qclass = -1;
60
61         if (tokens.length < 1)
62             return null;
63         qname = Name.fromString(tokens[0]);
64         if (!qname.isAbsolute()) {
65             qname = Name.concatenate(qname, Name.root);
66         }
67
68         for (int i = 1; i < tokens.length; i++) {
69             if (tokens[i].startsWith("+")) {
70                 // For now, we ignore flags as uninteresting
71                 // All queries will get the DO bit anyway
72                 continue;
73             }
74
75             int type = Type.value(tokens[i]);
76             if (type > 0) {
77                 qtype = type;
78                 continue;
79             }
80             int cls = DClass.value(tokens[i]);
81             if (cls > 0) {
82                 qclass = cls;
83                 continue;
84             }
85         }
86         if (qtype < 0) {
87             qtype = Type.A;
88         }
89         if (qclass < 0) {
90             qclass = DClass.IN;
91         }
92
93         Message query = Message.newQuery(Record.newRecord(qname, qtype, qclass));
94
95         return query;
96     }
97
98     /**
99      * Fetch the next query from either the command line or the query file
100      * 
101      * @return a query Message, or null if the query list is exhausted
102      * @throws IOException
103      */
104     private Message nextQuery() throws IOException {
105         if (query != null) {
106             Message res = queryFromString(query);
107             query = null;
108             return res;
109         }
110
111         if (queryStream == null && queryFile != null) {
112             queryStream = new BufferedReader(new FileReader(queryFile));
113         }
114
115         if (queryStream != null) {
116             String line = queryStream.readLine();
117
118             if (line == null)
119                 return null;
120
121             return queryFromString(line);
122         }
123
124         return null;
125
126     }
127
128     /**
129      * Figure out the correct zone from the query by comparing the qname to the
130      * list of trusted DNSKEY owner names.
131      * 
132      * @param query
133      * @return a zone name
134      * @throws IOException
135      */
136     private Name zoneFromQuery(Message query) throws IOException {
137
138         if (zoneNames == null) {
139             zoneNames = new HashSet<Name>();
140             for (String key : validator.listTrustedKeys()) {
141                 String[] components = key.split("/");
142                 Name keyname = Name.fromString(components[0]);
143                 if (!keyname.isAbsolute()) {
144                     keyname = Name.concatenate(keyname, Name.root);
145                 }
146                 zoneNames.add(keyname);
147             }
148         }
149
150         Name qname = query.getQuestion().getName();
151         for (Name n : zoneNames) {
152             if (qname.subdomain(n)) {
153                 return n;
154             }
155         }
156
157         return null;
158     }
159
160     private Message resolve(Message query) {
161
162         try {
163             return resolver.send(query);
164         } catch (SocketTimeoutException e) {
165             System.err.println("Error: timed out querying " + server + " for "
166                     + queryToString(query));
167         } catch (IOException e) {
168             System.err.println("Error: error querying " + server + " for "
169                     + queryToString(query) + ":" + e.getMessage());
170         }
171         return null;
172     }
173
174     private String queryToString(Message query) {
175         if (query == null)
176             return null;
177         Record question = query.getQuestion();
178         return question.getName() + "/" + Type.string(question.getType()) + "/"
179                 + DClass.string(question.getDClass());
180     }
181
182     public void execute() throws IOException {
183         // Configure our resolver
184         resolver = new SimpleResolver(server);
185         resolver.setEDNS(0, 4096, Flags.DO, null);
186
187         // Create our DNSSEC error stream
188         if (errorFile != null) {
189             errorStream = new PrintStream(new FileOutputStream(errorFile, true));
190         } else {
191             errorStream = System.out;
192         }
193
194         // Prime the validator
195         if (dnskeyFile != null) {
196             validator.addTrustedKeysFromFile(dnskeyFile);
197         } else {
198             for (String name : dnskeyNames) {
199                 Message query = queryFromString(name + " DNSKEY");
200                 Message response = resolve(query);
201                 validator.addTrustedKeysFromResponse(response);
202             }
203         }
204
205         // Log our set of trusted keys
206         List<String> trustedKeys = validator.listTrustedKeys();
207         if (trustedKeys.size() == 0) {
208             System.err.println("ERROR: no trusted keys found/provided.");
209             return;
210         }
211
212         for (String key : validator.listTrustedKeys()) {
213             System.out.println("Trusted Key: " + key);
214         }
215
216         // Iterate over all queries
217         Message query = nextQuery();
218         long total = 0;
219         long validCount = 0;
220         long errorCount = 0;
221
222         while (query != null) {
223
224             Name zone = zoneFromQuery(query);
225             // Skip queries in zones that we don't have keys for
226             if (zone == null) {
227                 if (debug) {
228                     System.out.println("DEBUG: skipping query " + queryToString(query));
229                 }
230                 query = nextQuery();
231                 continue;
232             }
233
234             if (debug) {
235                 System.out.println("DEBUG: querying for: " + queryToString(query));
236             }
237
238             Message response = resolve(query);
239             if (response == null) {
240                 System.out.println("ERROR: No response for query: " + queryToString(query));
241                 continue;
242             }
243             byte result = validator.validateMessage(response, zone.toString());
244
245             switch (result) {
246             case SecurityStatus.BOGUS:
247             case SecurityStatus.INVALID:
248                 errorStream.println("BOGUS Answer:");
249                 errorStream.println("Query: " + queryToString(query));
250                 errorStream.println("Response:\n" + response);
251                 for (String err : validator.getErrorList()) {
252                     errorStream.println("Error: " + err);
253                 }
254                 errorStream.println("");
255                 errorCount++;
256                 break;
257             case SecurityStatus.INSECURE:
258             case SecurityStatus.INDETERMINATE:
259             case SecurityStatus.UNCHECKED:
260                 errorStream.println("Insecure Answer:");
261                 errorStream.println("Query: " + queryToString(query));
262                 errorStream.println("Response:\n" + response);
263                 for (String err : validator.getErrorList()) {
264                     errorStream.println("Error: " + err);
265                 }
266                 errorCount++;
267                 break;
268             case SecurityStatus.SECURE:
269                 if (debug) System.out.println("DEBUG: response for " + queryToString(query) + " was valid.");
270                 validCount++;
271                 break;
272             }
273
274             if (++total % 1000 == 0) {
275                 System.out.println("Completed " + total + " queries: "
276                         + validCount + " valid, " + errorCount + " errors.");
277             }
278          
279             if (count > 0 && total >= count) {
280                 if (debug) System.out.println("DEBUG: reached maximum number of queries, exiting");
281                 break;
282             }
283             
284             query = nextQuery();
285         }
286
287         System.out.println("Completed " + total
288                 + (total > 1 ? " queries" : " query") +
289                 ": " + validCount + " valid, " + errorCount + " errors.");
290     }
291
292     private static void usage() {
293         System.err.println("usage: java -jar dnssecvaltool.jar [..options..]");
294         System.err.println("       server:       the DNS server to query.");
295         System.err.println("       query:        a name [type [flags]] string.");
296         System.err.println("       query_file:   a list of queries, one query per line.");
297         System.err.println("       count:        send up to'count' queries, then stop.");
298         System.err.println("       dnskey_file:  a file containing DNSKEY RRs to trust.");
299         System.err.println("       dnskey_query: query 'server' for DNSKEY at given name to trust, may repeat.");
300         System.err.println("       error_file:   write DNSSEC validation failure details to this file.");
301     }
302
303     public static void main(String[] argv) {
304
305         // Set up Log4J to just log to console.
306         BasicConfigurator.configure();
307         // And raise the log level quite high
308         Logger rootLogger = Logger.getRootLogger();
309         rootLogger.setLevel(Level.FATAL);
310
311         DNSSECValTool dr = new DNSSECValTool();
312
313         try {
314             // Parse the command line options
315             for (String arg : argv) {
316
317                 if (arg.indexOf('=') < 0) {
318                     System.err.println("Unrecognized option: " + arg);
319                     usage();
320                     System.exit(1);
321                 }
322
323                 String[] split_arg = arg.split("=", 2);
324                 String opt = split_arg[0];
325                 String optarg = split_arg[1];
326
327                 if (opt.equals("server")) {
328                     dr.server = optarg;
329                 } else if (opt.equals("query")) {
330                     dr.query = optarg;
331                 } else if (opt.equals("query_file")) {
332                     dr.queryFile = optarg;
333                 } else if (opt.equals("count")) {
334                     dr.count = Util.parseInt(optarg, 0);
335                 } else if (opt.equals("error_file")) {
336                     dr.errorFile = optarg;
337                 } else if (opt.equals("dnskey_file")) {
338                     dr.dnskeyFile = optarg;
339                 } else if (opt.equals("dnskey_query")) {
340                     if (dr.dnskeyNames == null) {
341                         dr.dnskeyNames = new ArrayList<String>();
342                     }
343                     dr.dnskeyNames.add(optarg);
344                 } else if (opt.equals("debug")) {
345                     dr.debug = Boolean.parseBoolean(optarg);
346                     rootLogger.setLevel(Level.TRACE);
347                 } else {
348                     System.err.println("Unrecognized option: " + opt);
349                     usage();
350                     System.exit(1);
351                 }
352             }
353
354             // Check for minimum usage
355             if (dr.server == null) {
356                 System.err.println("'server' must be specified");
357                 usage();
358                 System.exit(1);
359             }
360             if (dr.query == null && dr.queryFile == null) {
361                 System.err.println("Either 'query' or 'query_file' must be specified");
362                 usage();
363                 System.exit(1);
364             }
365             if (dr.dnskeyFile == null && dr.dnskeyNames == null) {
366                 System.err.println("Either 'dnskey_file' or 'dnskey_query' must be specified");
367                 usage();
368                 System.exit(1);
369             }
370
371             // Execute the job
372             dr.execute();
373
374         } catch (Exception e) {
375             e.printStackTrace();
376             System.exit(1);
377         }
378     }
379 }