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