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