239316e387322a031cce652bb09723a3238163d5
[captive-validator.git] / src / se / rfc / unbound / DnsSecVerifier.java
1 /*
2  * $Id$
3  *
4  * Copyright (c) 2005 VeriSign, Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  * 3. The name of the author may not be used to endorse or promote products
16  *    derived from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  *
29  */
30
31 package se.rfc.unbound.validator;
32
33 import java.util.*;
34 import java.io.*;
35 import java.security.*;
36
37 import org.apache.log4j.Logger;
38 import org.xbill.DNS.*;
39 import org.xbill.DNS.security.*;
40
41 import se.rfc.unbound.SecurityStatus;
42 import se.rfc.unbound.Util;
43
44 /**
45  * A class for performing basic DNSSEC verification. The DNSJAVA package
46  * contains a similar class. This is a reimplementation that allows us to have
47  * finer control over the validation process.
48  * 
49  * @author davidb
50  * @version $Revision$
51  */
52 public class DnsSecVerifier
53 {
54   public static final int UNKNOWN = 0;
55   public static final int RSA = 1;
56   public static final int DSA = 2;
57
58   /**
59    * This is a mapping of DNSSEC algorithm numbers/private identifiers to JCA
60    * algorithm identifiers.
61    */
62   private HashMap mAlgorithmMap;
63
64   private Logger  log = Logger.getLogger(this.getClass());
65
66   private static class AlgEntry
67   {
68     public String    jcaName;
69     public boolean   isDSA;
70     public int       dnssecAlg;
71
72     public AlgEntry(String name, int dnssecAlg, boolean isDSA)
73     {
74       jcaName = name;
75       this.dnssecAlg = dnssecAlg;
76       this.isDSA = isDSA;
77     }
78   }
79
80   public DnsSecVerifier()
81   {
82     mAlgorithmMap = new HashMap();
83
84     // set the default algorithm map.
85     mAlgorithmMap.put(new Integer(DNSSEC.RSAMD5), new AlgEntry("MD5withRSA",
86         DNSSEC.RSAMD5, false));
87     mAlgorithmMap.put(new Integer(DNSSEC.DSA), new AlgEntry("SHA1withDSA", DNSSEC.DSA,
88         true));
89     mAlgorithmMap.put(new Integer(DNSSEC.RSASHA1), new AlgEntry(
90         "SHA1withRSA", DNSSEC.RSASHA1, false));
91   }
92
93   private boolean isDSA(int algorithm)
94   {
95     // shortcut the standard algorithms
96     if (algorithm == DNSSEC.DSA) return true;
97     if (algorithm == DNSSEC.RSASHA1) return false;
98     if (algorithm == DNSSEC.RSAMD5) return false;
99     
100     AlgEntry entry = (AlgEntry) mAlgorithmMap.get(new Integer(algorithm));
101     if (entry != null) return entry.isDSA;
102     return false;
103   }
104
105   public void init(Properties config)
106   {
107     if (config == null) return;
108
109     // Algorithm configuration
110
111     // For now, we just accept new identifiers for existing algoirthms.
112     // FIXME: handle private identifiers.
113     List aliases = Util.parseConfigPrefix(config, "dns.algorithm.");
114
115     for (Iterator i = aliases.iterator(); i.hasNext();)
116     {
117       Util.ConfigEntry entry = (Util.ConfigEntry) i.next();
118
119       Integer alg_alias = new Integer(Util.parseInt(entry.key, -1));
120       Integer alg_orig = new Integer(Util.parseInt(entry.value, -1));
121
122       if (!mAlgorithmMap.containsKey(alg_orig))
123       {
124         log.warn("Unable to alias " + alg_alias + " to unknown algorithm "
125             + alg_orig);
126         continue;
127       }
128
129       if (mAlgorithmMap.containsKey(alg_alias))
130       {
131         log.warn("Algorithm alias " + alg_alias
132             + " is already defined and cannot be redefined");
133         continue;
134       }
135
136       mAlgorithmMap.put(alg_alias, mAlgorithmMap.get(alg_orig));
137     }
138
139     // for debugging purposes, log the entire algorithm map table.
140     for (Iterator i = mAlgorithmMap.keySet().iterator(); i.hasNext(); )
141     {
142       Integer alg = (Integer) i.next();
143       AlgEntry entry = (AlgEntry) mAlgorithmMap.get(alg);
144       if (entry == null) 
145         log.warn("DNSSEC alg " + alg + " has a null entry!");
146       else
147         log.debug("DNSSEC alg " + alg + " maps to " + entry.jcaName
148             + " (" + entry.dnssecAlg + ")");
149     }
150   }
151
152   /**
153    * Find the matching DNSKEY(s) to an RRSIG within a DNSKEY rrset. Normally
154    * this will only return one DNSKEY. It can return more than one, since
155    * KeyID/Footprints are not guaranteed to be unique.
156    * 
157    * @param dnskey_rrset The DNSKEY rrset to search.
158    * @param signature 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   private List findKey(RRset dnskey_rrset, RRSIGRecord signature)
163   {
164     if (!signature.getSigner().equals(dnskey_rrset.getName()))
165     {
166       log.trace("findKey: could not find appropriate key because "
167           + "incorrect keyset was supplied. Wanted: " + signature.getSigner()
168           + ", got: " + dnskey_rrset.getName());
169       return null;
170     }
171
172     int keyid = signature.getFootprint();
173     int alg = signature.getAlgorithm();
174
175     List res = new ArrayList(dnskey_rrset.size());
176
177     for (Iterator i = dnskey_rrset.rrs(); i.hasNext();)
178     {
179       DNSKEYRecord r = (DNSKEYRecord) i.next();
180       if (r.getAlgorithm() == alg && r.getFootprint() == keyid)
181       {
182         res.add(r);
183       }
184     }
185
186     if (res.size() == 0)
187     {
188       log.trace("findKey: could not find a key matching "
189           + "the algorithm and footprint in supplied keyset. ");
190       return null;
191     }
192     return res;
193   }
194
195   /**
196    * Check to see if a signature looks valid (i.e., matches the rrset in
197    * question, in the validity period, etc.)
198    * 
199    * @param rrset The rrset that the signature belongs to.
200    * @param sigrec The signature record to check.
201    * @return A value of DNSSEC.Secure if it looks OK, DNSSEC.Faile if it looks
202    *         bad.
203    */
204   private byte checkSignature(RRset rrset, RRSIGRecord sigrec)
205   {
206     if (rrset == null || sigrec == null) return DNSSEC.Failed;
207     if (!rrset.getName().equals(sigrec.getName()))
208     {
209       log.debug("Signature name does not match RRset name");
210       return SecurityStatus.BOGUS;
211     }
212     if (rrset.getType() != sigrec.getTypeCovered())
213     {
214       log.debug("Signature type does not match RRset type");
215       return SecurityStatus.BOGUS;
216     }
217
218     Date now = new Date();
219     Date start = sigrec.getTimeSigned();
220     Date expire = sigrec.getExpire();
221     if (now.before(start))
222     {
223       log.debug("Signature is not yet valid");
224       return SecurityStatus.BOGUS;
225     }
226
227     if (now.after(expire))
228     {
229       log.debug("Signature has expired (now = " + now + ", sig expires = "
230           + expire);
231       return SecurityStatus.BOGUS;
232     }
233
234     return SecurityStatus.SECURE;
235   }
236
237   public PublicKey parseDNSKEY(DNSKEYRecord key)
238   {
239     AlgEntry ae = (AlgEntry) mAlgorithmMap
240         .get(new Integer(key.getAlgorithm()));
241     if (key.getAlgorithm() != ae.dnssecAlg)
242     {
243       // Recast the DNSKEYRecord in question as one using the offical
244       // algorithm, to work around the lack of alias support in the underlying
245       // KEYConverter class from DNSjava
246
247       key = new DNSKEYRecord(key.getName(), key.getDClass(), key.getTTL(),
248           key.getFlags(), key.getProtocol(), ae.dnssecAlg, key.getKey());
249     }
250
251     return KEYConverter.parseRecord(key);
252   }
253   
254   
255   /**
256    * Actually cryptographically verify a signature over the rrset. The RRSIG
257    * record must match the rrset being verified (see checkSignature).
258    * 
259    * @param rrset The rrset to verify.
260    * @param sigrec The signature to verify with.
261    * @param key The (public) key associated with the RRSIG record.
262    * @return A security status code: SECURE if it worked, BOGUS if not,
263    *         UNCHECKED if we just couldn't actually do the function.
264    */
265   public byte verifySignature(RRset rrset, RRSIGRecord sigrec,
266       DNSKEYRecord key)
267   {
268     try
269     {
270       PublicKey pk = parseDNSKEY(key);
271
272       if (pk == null)
273       {
274         log.warn("Could not convert DNSKEY record to a JCA public key: "
275             + key);
276         return SecurityStatus.UNCHECKED;
277       }
278
279       byte[] data = SignUtils.generateSigData(rrset, sigrec);
280
281       Signature signer = getSignature(sigrec.getAlgorithm());
282       if (signer == null)
283       {
284         return SecurityStatus.BOGUS;
285       }
286       
287       signer.initVerify(pk);
288       signer.update(data);
289
290       byte[] sig = sigrec.getSignature();
291       if (isDSA(sigrec.getAlgorithm()))
292       {
293         sig = SignUtils.convertDSASignature(sig);
294       }
295       if (!signer.verify(sig))
296       {
297         log.info("Signature failed to verify cryptographically");
298         log.debug("Failed signature: " + sigrec);
299         return SecurityStatus.BOGUS;
300       }
301       log.trace("Signature verified: " + sigrec);
302       return SecurityStatus.SECURE;
303     }
304     catch (IOException e)
305     {
306       log.error("I/O error", e);
307     }
308     catch (GeneralSecurityException e)
309     {
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   /**
321    * Verify an RRset against a particular signature.
322    * 
323    * @return DNSSEC.Secure if the signature verfied, DNSSEC.Failed if it did
324    *         not verify (for any reason), and DNSSEC.Insecure if verification
325    *         could not be completed (usually because the public key was not
326    *         available).
327    */
328   public byte verifySignature(RRset rrset, RRSIGRecord sigrec, RRset key_rrset)
329   {
330     byte result = checkSignature(rrset, sigrec);
331     if (result != SecurityStatus.SECURE) return result;
332
333     List keys = findKey(key_rrset, sigrec);
334
335     if (keys == null)
336     {
337       log.trace("could not find appropriate key");
338       return SecurityStatus.BOGUS;
339     }
340
341     byte status = SecurityStatus.UNCHECKED;
342
343     for (Iterator i = keys.iterator(); i.hasNext();)
344     {
345       DNSKEYRecord key = (DNSKEYRecord) i.next();
346       status = verifySignature(rrset, sigrec, key);
347
348       if (status == SecurityStatus.SECURE) break;
349     }
350
351     return status;
352   }
353
354   /**
355    * Verifies an RRset. This routine does not modify the RRset. This RRset is
356    * presumed to be verifiable, and the correct DNSKEY rrset is presumed to
357    * have been found.
358    * 
359    * @return SecurityStatus.SECURE if the rrest verified positively,
360    *         SecurityStatus.BOGUS otherwise.
361    */
362   public byte verify(RRset rrset, RRset key_rrset)
363   {
364     Iterator i = rrset.sigs();
365
366     if (!i.hasNext())
367     {
368       log.info("RRset failed to verify due to lack of signatures");
369       return SecurityStatus.BOGUS;
370     }
371
372     while (i.hasNext())
373     {
374       RRSIGRecord sigrec = (RRSIGRecord) i.next();
375
376       byte res = verifySignature(rrset, sigrec, key_rrset);
377
378       if (res == SecurityStatus.SECURE) return res;
379     }
380
381     log.info("RRset failed to verify: all signatures were BOGUS");
382     return SecurityStatus.BOGUS;
383   }
384
385   /**
386    * Verify an RRset against a single DNSKEY. Use this when you must be
387    * certain that an RRset signed and verifies with a particular DNSKEY (as
388    * opposed to a particular DNSKEY rrset).
389    * 
390    * @param rrset The rrset to verify.
391    * @param dnskey The DNSKEY to verify with.
392    * @return SecurityStatus.SECURE if the rrset verified, BOGUS otherwise.
393    */
394   public byte verify(RRset rrset, DNSKEYRecord dnskey)
395   {
396     // Iterate over RRSIGS
397
398     Iterator i = rrset.sigs();
399     if (!i.hasNext())
400     {
401       log.info("RRset failed to verify due to lack of signatures");
402       return SecurityStatus.BOGUS;
403     }
404
405     while (i.hasNext())
406     {
407       RRSIGRecord sigrec = (RRSIGRecord) i.next();
408
409       // Skip RRSIGs that do not match our given key's footprint.
410       if (sigrec.getFootprint() != dnskey.getFootprint()) continue;
411
412       byte res = verifySignature(rrset, sigrec, dnskey);
413
414       if (res == SecurityStatus.SECURE) return res;
415     }
416
417     log.info("RRset failed to verify: all signatures were BOGUS");
418     return SecurityStatus.BOGUS;
419   }
420
421   public boolean supportsAlgorithm(int algorithm)
422   {
423     return mAlgorithmMap.containsKey(new Integer(algorithm));
424   }
425
426   public boolean supportsAlgorithm(Name private_id)
427   {
428     return mAlgorithmMap.containsKey(private_id);
429   }
430
431   public int baseAlgorithm(int algorithm)
432   {
433     switch (algorithm)
434     {
435       case DNSSEC.RSAMD5:
436       case DNSSEC.RSASHA1:
437         return RSA;
438       case DNSSEC.DSA:
439         return DSA;
440     }
441     AlgEntry entry = (AlgEntry) mAlgorithmMap.get(new Integer(algorithm));
442     if (entry == null) return UNKNOWN;
443     if (entry.isDSA) return DSA;
444     return RSA;
445   }
446   
447   /** @return the appropriate Signature object for this keypair. */
448   private Signature getSignature(int algorithm)
449   {
450     Signature s = null;
451
452
453     try
454     {
455       AlgEntry entry = (AlgEntry) mAlgorithmMap.get(new Integer(algorithm));
456       if (entry == null)
457       {
458         log.info("DNSSEC algorithm " + algorithm + " not recognized.");
459         return null;
460       }
461       // TODO: should we cache the instance?
462       s = Signature.getInstance(entry.jcaName);
463     }
464     catch (NoSuchAlgorithmException e)
465     {
466       log.error("error getting Signature object", e);
467     }
468
469     return s;
470   }
471
472   // TODO: enable private algorithm support in dnsjava.
473   // Right now, this cannot be used because the DNSKEYRecord object doesn't
474   // give us
475   // the private key name.
476   // private Signature getSignature(Name private_alg)
477   // {
478   // Signature s = null;
479   //
480   // try
481   // {
482   // String alg_id = (String) mAlgorithmMap.get(private_alg);
483   // if (alg_id == null)
484   // {
485   // log.debug("DNSSEC private algorithm '" + private_alg
486   // + "' not recognized.");
487   // return null;
488   // }
489   //
490   // s = Signature.getInstance(alg_id);
491   // }
492   // catch (NoSuchAlgorithmException e)
493   // {
494   // log.error("error getting Signature object", e);
495   // }
496   //
497   // return s;
498   // }
499 }