faf436997c978158462a269b8b983e5ca8510ae0
[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             switch (result) {
247             case SecurityStatus.BOGUS:
248             case SecurityStatus.INVALID:
249                 errorStream.println("BOGUS Answer:");
250                 errorStream.println("Query: " + queryToString(query));
251                 errorStream.println("Response:\n" + response);
252                 for (String err : validator.getErrorList()) {
253                     errorStream.println("Error: " + err);
254                 }
255                 errorStream.println("");
256                 errorCount++;
257                 break;
258             case SecurityStatus.INSECURE:
259             case SecurityStatus.INDETERMINATE:
260             case SecurityStatus.UNCHECKED:
261                 errorStream.println("Insecure Answer:");
262                 errorStream.println("Query: " + queryToString(query));
263                 errorStream.println("Response:\n" + response);
264                 for (String err : validator.getErrorList()) {
265                     errorStream.println("Error: " + err);
266                 }
267                 errorCount++;
268                 break;
269             case SecurityStatus.SECURE:
270                 if (debug) {
271                     System.out.println("DEBUG: response for " + queryToString(query) + " was valid.");
272                     System.out.println("Response:\n" + response);
273                 }
274                 validCount++;
275                 break;
276             }
277
278             if (++total % 1000 == 0) {
279                 System.out.println("Completed " + total + " queries: " +
280                                    validCount + " valid, " + errorCount + " errors.");
281             }
282
283             if (count > 0 && total >= count) {
284                 if (debug) {
285                     System.out.println("DEBUG: reached maximum number of queries, exiting");
286                 }
287                 break;
288             }
289
290             query = nextQuery();
291         }
292
293         System.out.println("Completed " + total + (total > 1 ? " queries" : " query") +
294                            ": " + validCount + " valid, " + errorCount + " errors.");
295     }
296
297     private static void usage() {
298         System.err.println("usage: java -jar dnssecvaltool.jar [..options..]");
299         System.err.println("       server:       the DNS server to query.");
300         System.err.println("       query:        a name [type [flags]] string.");
301         System.err.println("       query_file:   a list of queries, one query per line.");
302         System.err.println("       count:        send up to'count' queries, then stop.");
303         System.err.println("       dnskey_file:  a file containing DNSKEY RRs to trust.");
304         System.err.println("       dnskey_query: query 'server' for DNSKEY at given name to trust, may repeat.");
305         System.err.println("       error_file:   write DNSSEC validation failure details to this file.");
306     }
307
308     public static void main(String[] argv) {
309
310         // Set up Log4J to just log to console.
311         BasicConfigurator.configure();
312         // And raise the log level quite high
313         Logger rootLogger = Logger.getRootLogger();
314         rootLogger.setLevel(Level.FATAL);
315
316         DNSSECValTool dr = new DNSSECValTool();
317
318         try {
319             // Parse the command line options
320             for (String arg : argv) {
321
322                 if (arg.indexOf('=') < 0) {
323                     System.err.println("Unrecognized option: " + arg);
324                     usage();
325                     System.exit(1);
326                 }
327
328                 String[] split_arg = arg.split("=", 2);
329                 String opt         = split_arg[0];
330                 String optarg      = split_arg[1];
331
332                 if (opt.equals("server")) {
333                     dr.server = optarg;
334                 } else if (opt.equals("query")) {
335                     dr.query = optarg;
336                 } else if (opt.equals("query_file")) {
337                     dr.queryFile = optarg;
338                 } else if (opt.equals("count")) {
339                     dr.count = Util.parseInt(optarg, 0);
340                 } else if (opt.equals("error_file")) {
341                     dr.errorFile = optarg;
342                 } else if (opt.equals("dnskey_file")) {
343                     dr.dnskeyFile = optarg;
344                 } else if (opt.equals("dnskey_query")) {
345                     if (dr.dnskeyNames == null) {
346                         dr.dnskeyNames = new ArrayList<String>();
347                     }
348                     dr.dnskeyNames.add(optarg);
349                 } else if (opt.equals("debug")) {
350                     dr.debug = Boolean.parseBoolean(optarg);
351                 } else if (opt.equals("trace")) {
352                     dr.debug = Boolean.parseBoolean(optarg);
353                     if (dr.debug) {
354                         rootLogger.setLevel(Level.TRACE);
355                     }
356                 } else {
357                     System.err.println("Unrecognized option: " + opt);
358                     usage();
359                     System.exit(1);
360                 }
361             }
362
363             // Check for minimum usage
364             if (dr.server == null) {
365                 System.err.println("'server' must be specified");
366                 usage();
367                 System.exit(1);
368             }
369             if (dr.query == null && dr.queryFile == null) {
370                 System.err.println("Either 'query' or 'query_file' must be specified");
371                 usage();
372                 System.exit(1);
373             }
374             if (dr.dnskeyFile == null && dr.dnskeyNames == null) {
375                 System.err.println("Either 'dnskey_file' or 'dnskey_query' must be specified");
376                 usage();
377                 System.exit(1);
378             }
379
380             // Execute the job
381             dr.execute();
382
383         } catch (Exception e) {
384             e.printStackTrace();
385             System.exit(1);
386         }
387     }
388 }