fix warnings and findbugs hints
[captive-validator.git] / src / com / verisign / tat / dnssec / DnsSecVerifier.java
1 /***************************** -*- Java -*- ********************************\
2  *                                                                         *
3  *   Copyright (c) 2009 VeriSign, Inc. All rights reserved.                *
4  *                                                                         *
5  * This software is provided solely in connection with the terms of the    *
6  * license agreement.  Any other use without the prior express written     *
7  * permission of VeriSign is completely prohibited.  The software and      *
8  * documentation are "Commercial Items", as that term is defined in 48     *
9  * C.F.R.  section 2.101, consisting of "Commercial Computer Software" and *
10  * "Commercial Computer Software Documentation" as such terms are defined  *
11  * in 48 C.F.R. section 252.227-7014(a)(5) and 48 C.F.R. section           *
12  * 252.227-7014(a)(1), and used in 48 C.F.R. section 12.212 and 48 C.F.R.  *
13  * section 227.7202, as applicable.  Pursuant to the above and other       *
14  * relevant sections of the Code of Federal Regulations, as applicable,    *
15  * VeriSign's publications, commercial computer software, and commercial   *
16  * computer software documentation are distributed and licensed to United  *
17  * States Government end users with only those rights as granted to all    *
18  * other end users, according to the terms and conditions contained in the *
19  * license agreement(s) that accompany the products and software           *
20  * documentation.                                                          *
21  *                                                                         *
22 \***************************************************************************/
23
24 package com.verisign.tat.dnssec;
25
26 import org.apache.log4j.Logger;
27
28 import org.xbill.DNS.*;
29 import org.xbill.DNS.security.*;
30
31 import java.io.*;
32
33 import java.security.*;
34
35 import java.util.*;
36
37 /**
38  * A class for performing basic DNSSEC verification. The DNSJAVA package
39  * contains a similar class. This is a re-implementation that allows us to have
40  * finer control over the validation process.
41  */
42 public class DnsSecVerifier {
43     public static final int UNKNOWN = 0;
44     public static final int RSA = 1;
45     public static final int DSA = 2;
46     private Logger log = Logger.getLogger(this.getClass());
47
48     /**
49      * This is a mapping of DNSSEC algorithm numbers to JCA algorithm
50      * identifiers.
51      */
52     private HashMap<Integer, AlgEntry> mAlgorithmMap;
53
54     /**
55      * This is a mapping of DNSSEC private (DNS name) identifiers to JCA
56      * algorithm identifiers.
57      */
58     private HashMap<Name, AlgEntry> mPrivateAlgorithmMap;
59
60     public DnsSecVerifier() {
61         mAlgorithmMap = new HashMap<Integer, AlgEntry>();
62         mPrivateAlgorithmMap = new HashMap<Name, AlgEntry>();
63
64         // set the default algorithm map.
65         mAlgorithmMap.put(Integer.valueOf(DNSSEC.RSAMD5), new AlgEntry(
66                 "MD5withRSA", DNSSEC.RSAMD5, false));
67         mAlgorithmMap.put(Integer.valueOf(DNSSEC.DSA), new AlgEntry("SHA1withDSA",
68                 DNSSEC.DSA, true));
69         mAlgorithmMap.put(Integer.valueOf(DNSSEC.RSASHA1), new AlgEntry(
70                 "SHA1withRSA", DNSSEC.RSASHA1, false));
71         mAlgorithmMap.put(Integer.valueOf(DNSSEC.DSA_NSEC3_SHA1), new AlgEntry(
72                 "SHA1withDSA", DNSSEC.DSA, true));
73         mAlgorithmMap.put(Integer.valueOf(DNSSEC.RSA_NSEC3_SHA1), new AlgEntry(
74                 "SHA1withRSA", DNSSEC.RSASHA1, false));
75         mAlgorithmMap.put(Integer.valueOf(DNSSEC.RSASHA256), new AlgEntry(
76                 "SHA256withRSA", DNSSEC.RSASHA256, false));
77         mAlgorithmMap.put(Integer.valueOf(DNSSEC.RSASHA512), new AlgEntry(
78                 "SHA512withRSA", DNSSEC.RSASHA512, false));
79     }
80
81     private boolean isDSA(int algorithm) {
82         // shortcut the standard algorithms
83         if (algorithm == DNSSEC.DSA) {
84             return true;
85         }
86
87         if (algorithm == DNSSEC.RSASHA1) {
88             return false;
89         }
90
91         if (algorithm == DNSSEC.RSAMD5) {
92             return false;
93         }
94
95         AlgEntry entry = (AlgEntry) mAlgorithmMap.get(Integer.valueOf(algorithm));
96
97         if (entry != null) {
98             return entry.isDSA;
99         }
100
101         return false;
102     }
103
104     public void init(Properties config) {
105         if (config == null) {
106             return;
107         }
108
109         // Algorithm configuration
110
111         // For now, we just accept new identifiers for existing algorithms.
112         // FIXME: handle private identifiers.
113         List<Util.ConfigEntry> aliases = Util.parseConfigPrefix(config,
114                 "dns.algorithm.");
115
116         for (Util.ConfigEntry entry : aliases) {
117             Integer alg_alias = Integer.valueOf(Util.parseInt(entry.key, -1));
118             Integer alg_orig = Integer.valueOf(Util.parseInt(entry.value, -1));
119
120             if (!mAlgorithmMap.containsKey(alg_orig)) {
121                 log.warn("Unable to alias " + alg_alias
122                         + " to unknown algorithm " + alg_orig);
123
124                 continue;
125             }
126
127             if (mAlgorithmMap.containsKey(alg_alias)) {
128                 log.warn("Algorithm alias " + alg_alias
129                         + " is already defined and cannot be redefined");
130
131                 continue;
132             }
133
134             mAlgorithmMap.put(alg_alias, mAlgorithmMap.get(alg_orig));
135         }
136
137         // for debugging purposes, log the entire algorithm map table.
138         for (Integer alg : mAlgorithmMap.keySet()) {
139             AlgEntry entry = mAlgorithmMap.get(alg);
140
141             if (entry == null) {
142                 log.warn("DNSSEC alg " + alg + " has a null entry!");
143             } else {
144                 log.debug("DNSSEC alg " + alg + " maps to " + entry.jcaName
145                         + " (" + entry.dnssecAlg + ")");
146             }
147         }
148     }
149
150     /**
151      * Find the matching DNSKEY(s) to an RRSIG within a DNSKEY rrset. Normally
152      * this will only return one DNSKEY. It can return more than one, since
153      * KeyID/Footprints are not guaranteed to be unique.
154      * 
155      * @param dnskey_rrset
156      *            The DNSKEY rrset to search.
157      * @param signature
158      *            The RRSIG to match against.
159      * @return A List contains a one or more DNSKEYRecord objects, or null if a
160      *         matching DNSKEY could not be found.
161      */
162     @SuppressWarnings("rawtypes")
163     private List<DNSKEYRecord> findKey(RRset dnskey_rrset, RRSIGRecord signature) {
164         if (!signature.getSigner().equals(dnskey_rrset.getName())) {
165             log.trace("findKey: could not find appropriate key because "
166                     + "incorrect keyset was supplied. Wanted: "
167                     + signature.getSigner() + ", got: "
168                     + dnskey_rrset.getName());
169
170             return null;
171         }
172
173         int keyid = signature.getFootprint();
174         int alg = signature.getAlgorithm();
175
176         List<DNSKEYRecord> res = new ArrayList<DNSKEYRecord>(dnskey_rrset
177                 .size());
178
179         for (Iterator i = dnskey_rrset.rrs(); i.hasNext();) {
180             DNSKEYRecord r = (DNSKEYRecord) i.next();
181
182             if ((r.getAlgorithm() == alg) && (r.getFootprint() == keyid)) {
183                 res.add(r);
184             }
185         }
186
187         if (res.size() == 0) {
188             log.trace("findKey: could not find a key matching "
189                     + "the algorithm and footprint in supplied keyset. ");
190
191             return null;
192         }
193
194         return res;
195     }
196
197     /**
198      * Check to see if a signature looks valid (i.e., matches the rrset in
199      * question, in the validity period, etc.)
200      * 
201      * @param rrset
202      *            The rrset that the signature belongs to.
203      * @param sigrec
204      *            The signature record to check.
205      * @return A value of DNSSEC.Secure if it looks OK, DNSSEC.Faile if it looks
206      *         bad.
207      */
208     private byte checkSignature(RRset rrset, RRSIGRecord sigrec) {
209         if ((rrset == null) || (sigrec == null)) {
210             return DNSSEC.Failed;
211         }
212
213         if (!rrset.getName().equals(sigrec.getName())) {
214             log.debug("Signature name does not match RRset name");
215
216             return SecurityStatus.BOGUS;
217         }
218
219         if (rrset.getType() != sigrec.getTypeCovered()) {
220             log.debug("Signature type does not match RRset type");
221
222             return SecurityStatus.BOGUS;
223         }
224
225         Date now = new Date();
226         Date start = sigrec.getTimeSigned();
227         Date expire = sigrec.getExpire();
228
229         if (now.before(start)) {
230             log.debug("Signature is not yet valid");
231
232             return SecurityStatus.BOGUS;
233         }
234
235         if (now.after(expire)) {
236             log.debug("Signature has expired (now = " + now
237                     + ", sig expires = " + expire);
238
239             return SecurityStatus.BOGUS;
240         }
241
242         return SecurityStatus.SECURE;
243     }
244
245     public PublicKey parseDNSKEY(DNSKEYRecord key) {
246         AlgEntry ae = (AlgEntry) mAlgorithmMap.get(Integer.valueOf(key
247                 .getAlgorithm()));
248
249         if (key.getAlgorithm() != ae.dnssecAlg) {
250             // Recast the DNSKEYRecord in question as one using the offical
251             // algorithm, to work around the lack of alias support in the
252             // underlying
253             // KEYConverter class from DNSjava
254             key = new DNSKEYRecord(key.getName(), key.getDClass(),
255                     key.getTTL(), key.getFlags(), key.getProtocol(),
256                     ae.dnssecAlg, key.getKey());
257         }
258
259         return KEYConverter.parseRecord(key);
260     }
261
262     /**
263      * Actually cryptographically verify a signature over the rrset. The RRSIG
264      * record must match the rrset being verified (see checkSignature).
265      * 
266      * @param rrset
267      *            The rrset to verify.
268      * @param sigrec
269      *            The signature to verify with.
270      * @param key
271      *            The (public) key associated with the RRSIG record.
272      * @return A security status code: SECURE if it worked, BOGUS if not,
273      *         UNCHECKED if we just couldn't actually do the function.
274      */
275     public byte verifySignature(RRset rrset, RRSIGRecord sigrec,
276             DNSKEYRecord key) {
277         try {
278             PublicKey pk = parseDNSKEY(key);
279
280             if (pk == null) {
281                 log
282                         .warn("Could not convert DNSKEY record to a JCA public key: "
283                                 + key);
284
285                 return SecurityStatus.UNCHECKED;
286             }
287
288             byte[] data = SignUtils.generateSigData(rrset, sigrec);
289
290             Signature signer = getSignature(sigrec.getAlgorithm());
291
292             if (signer == null) {
293                 return SecurityStatus.BOGUS;
294             }
295
296             signer.initVerify(pk);
297             signer.update(data);
298
299             byte[] sig = sigrec.getSignature();
300
301             if (isDSA(sigrec.getAlgorithm())) {
302                 sig = SignUtils.convertDSASignature(sig);
303             }
304
305             if (!signer.verify(sig)) {
306                 log.info("Signature failed to verify cryptographically");
307                 log.debug("Failed signature: " + sigrec);
308
309                 return SecurityStatus.BOGUS;
310             }
311
312             log.trace("Signature verified: " + sigrec);
313
314             return SecurityStatus.SECURE;
315         } catch (IOException e) {
316             log.error("I/O error", e);
317         } catch (GeneralSecurityException e) {
318             log.error("Security error", e);
319         }
320
321         // FIXME: Since I'm not sure what would cause an exception here (failure
322         // to have the required crypto?)
323         // We default to UNCHECKED instead of BOGUS. This could be wrong.
324         return SecurityStatus.UNCHECKED;
325     }
326
327     /**
328      * Verify an RRset against a particular signature.
329      * 
330      * @return DNSSEC.Secure if the signature verfied, DNSSEC.Failed if it did
331      *         not verify (for any reason), and DNSSEC.Insecure if verification
332      *         could not be completed (usually because the public key was not
333      *         available).
334      */
335     public byte verifySignature(RRset rrset, RRSIGRecord sigrec, RRset key_rrset) {
336         byte result = checkSignature(rrset, sigrec);
337
338         if (result != SecurityStatus.SECURE) {
339             return result;
340         }
341
342         List<DNSKEYRecord> keys = findKey(key_rrset, sigrec);
343
344         if (keys == null) {
345             log.trace("could not find appropriate key");
346
347             return SecurityStatus.BOGUS;
348         }
349
350         byte status = SecurityStatus.UNCHECKED;
351
352         for (DNSKEYRecord key : keys) {
353             status = verifySignature(rrset, sigrec, key);
354
355             if (status == SecurityStatus.SECURE) {
356                 break;
357             }
358         }
359
360         return status;
361     }
362
363     /**
364      * Verifies an RRset. This routine does not modify the RRset. This RRset is
365      * presumed to be verifiable, and the correct DNSKEY rrset is presumed to
366      * have been found.
367      * 
368      * @return SecurityStatus.SECURE if the rrest verified positively,
369      *         SecurityStatus.BOGUS otherwise.
370      */
371     @SuppressWarnings("rawtypes")
372     public byte verify(RRset rrset, RRset key_rrset) {
373         Iterator i = rrset.sigs();
374
375         if (!i.hasNext()) {
376             log.info("RRset failed to verify due to lack of signatures");
377
378             return SecurityStatus.BOGUS;
379         }
380
381         while (i.hasNext()) {
382             RRSIGRecord sigrec = (RRSIGRecord) i.next();
383
384             byte res = verifySignature(rrset, sigrec, key_rrset);
385
386             if (res == SecurityStatus.SECURE) {
387                 return res;
388             }
389         }
390
391         log.info("RRset failed to verify: all signatures were BOGUS");
392
393         return SecurityStatus.BOGUS;
394     }
395
396     /**
397      * Verify an RRset against a single DNSKEY. Use this when you must be
398      * certain that an RRset signed and verifies with a particular DNSKEY (as
399      * opposed to a particular DNSKEY rrset).
400      * 
401      * @param rrset
402      *            The rrset to verify.
403      * @param dnskey
404      *            The DNSKEY to verify with.
405      * @return SecurityStatus.SECURE if the rrset verified, BOGUS otherwise.
406      */
407     @SuppressWarnings("rawtypes")
408     public byte verify(RRset rrset, DNSKEYRecord dnskey) {
409         // Iterate over RRSIGS
410         Iterator i = rrset.sigs();
411
412         if (!i.hasNext()) {
413             log.info("RRset failed to verify due to lack of signatures");
414
415             return SecurityStatus.BOGUS;
416         }
417
418         while (i.hasNext()) {
419             RRSIGRecord sigrec = (RRSIGRecord) i.next();
420
421             // Skip RRSIGs that do not match our given key's footprint.
422             if (sigrec.getFootprint() != dnskey.getFootprint()) {
423                 continue;
424             }
425
426             byte res = verifySignature(rrset, sigrec, dnskey);
427
428             if (res == SecurityStatus.SECURE) {
429                 return res;
430             }
431         }
432
433         log.info("RRset failed to verify: all signatures were BOGUS");
434
435         return SecurityStatus.BOGUS;
436     }
437
438     public boolean supportsAlgorithm(int algorithm) {
439         return mAlgorithmMap.containsKey(Integer.valueOf(algorithm));
440     }
441
442     public boolean supportsAlgorithm(Name private_id) {
443         return mPrivateAlgorithmMap.containsKey(private_id);
444     }
445
446     public int baseAlgorithm(int algorithm) {
447         switch (algorithm) {
448         case DNSSEC.RSAMD5:
449         case DNSSEC.RSASHA1:
450             return RSA;
451
452         case DNSSEC.DSA:
453             return DSA;
454         }
455
456         AlgEntry entry = (AlgEntry) mAlgorithmMap.get(Integer.valueOf(algorithm));
457
458         if (entry == null) {
459             return UNKNOWN;
460         }
461
462         if (entry.isDSA) {
463             return DSA;
464         }
465
466         return RSA;
467     }
468
469     /** @return the appropriate Signature object for this keypair. */
470     private Signature getSignature(int algorithm) {
471         Signature s = null;
472
473         try {
474             AlgEntry entry = (AlgEntry) mAlgorithmMap
475                     .get(Integer.valueOf(algorithm));
476
477             if (entry == null) {
478                 log.info("DNSSEC algorithm " + algorithm + " not recognized.");
479
480                 return null;
481             }
482
483             // TODO: should we cache the instance?
484             s = Signature.getInstance(entry.jcaName);
485         } catch (NoSuchAlgorithmException e) {
486             log.error("error getting Signature object", e);
487         }
488
489         return s;
490     }
491
492     private static class AlgEntry {
493         public String jcaName;
494         public boolean isDSA;
495         public int dnssecAlg;
496
497         public AlgEntry(String name, int dnssecAlg, boolean isDSA) {
498             jcaName = name;
499             this.dnssecAlg = dnssecAlg;
500             this.isDSA = isDSA;
501         }
502     }
503
504     // TODO: enable private algorithm support in dnsjava.
505     // Right now, this cannot be used because the DNSKEYRecord object doesn't
506     // give us
507     // the private key name.
508     // private Signature getSignature(Name private_alg)
509     // {
510     // Signature s = null;
511     //
512     // try
513     // {
514     // String alg_id = (String) mAlgorithmMap.get(private_alg);
515     // if (alg_id == null)
516     // {
517     // log.debug("DNSSEC private algorithm '" + private_alg
518     // + "' not recognized.");
519     // return null;
520     // }
521     //
522     // s = Signature.getInstance(alg_id);
523     // }
524     // catch (NoSuchAlgorithmException e)
525     // {
526     // log.error("error getting Signature object", e);
527     // }
528     //
529     // return s;
530     // }
531 }