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