581d12e5ffae3bc3159dc4101839f6c1edadc3c6
[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
39  * package contains a similar class. This is a re-implementation that
40  * allows us to have 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
56      * JCA 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.Algorithm.RSAMD5), new AlgEntry(
66                 "MD5withRSA", DNSSEC.Algorithm.RSAMD5, false));
67         mAlgorithmMap.put(Integer.valueOf(DNSSEC.Algorithm.DSA), new AlgEntry("SHA1withDSA",
68                 DNSSEC.Algorithm.DSA, true));
69         mAlgorithmMap.put(Integer.valueOf(DNSSEC.Algorithm.RSASHA1), new AlgEntry(
70                 "SHA1withRSA", DNSSEC.Algorithm.RSASHA1, false));
71         mAlgorithmMap.put(Integer.valueOf(DNSSEC.Algorithm.DSA_NSEC3_SHA1), new AlgEntry(
72                 "SHA1withDSA", DNSSEC.Algorithm.DSA, true));
73         mAlgorithmMap.put(Integer.valueOf(DNSSEC.Algorithm.RSA_NSEC3_SHA1), new AlgEntry(
74                 "SHA1withRSA", DNSSEC.Algorithm.RSASHA1, false));
75         mAlgorithmMap.put(Integer.valueOf(DNSSEC.Algorithm.RSASHA256), new AlgEntry(
76                 "SHA256withRSA", DNSSEC.Algorithm.RSASHA256, false));
77         mAlgorithmMap.put(Integer.valueOf(DNSSEC.Algorithm.RSASHA512), new AlgEntry(
78                 "SHA512withRSA", DNSSEC.Algorithm.RSASHA512, false));
79     }
80
81     private boolean isDSA(int algorithm) {
82         // shortcut the standard algorithms
83         if (algorithm == DNSSEC.Algorithm.DSA) {
84             return true;
85         }
86
87         if (algorithm == DNSSEC.Algorithm.RSASHA1) {
88             return false;
89         }
90
91         if (algorithm == DNSSEC.Algorithm.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, "dns.algorithm.");
114
115         for (Util.ConfigEntry entry : aliases) {
116             Integer alg_alias = Integer.valueOf(Util.parseInt(entry.key, -1));
117             Integer alg_orig  = Integer.valueOf(Util.parseInt(entry.value, -1));
118
119             if (!mAlgorithmMap.containsKey(alg_orig)) {
120                 log.warn("Unable to alias " + alg_alias + " to unknown algorithm " + alg_orig);
121                 continue;
122             }
123
124             if (mAlgorithmMap.containsKey(alg_alias)) {
125                 log.warn("Algorithm alias " + alg_alias + " is already defined and cannot be redefined");
126                 continue;
127             }
128
129             mAlgorithmMap.put(alg_alias, mAlgorithmMap.get(alg_orig));
130         }
131
132         // for debugging purposes, log the entire algorithm map table.
133         for (Integer alg : mAlgorithmMap.keySet()) {
134             AlgEntry entry = mAlgorithmMap.get(alg);
135
136             if (entry == null) {
137                 log.warn("DNSSEC alg " + alg + " has a null entry!");
138             } else {
139                 log.debug("DNSSEC alg " + alg + " maps to " + entry.jcaName
140                           + " (" + entry.dnssecAlg + ")");
141             }
142         }
143     }
144
145     /**
146      * Find the matching DNSKEY(s) to an RRSIG within a DNSKEY
147      * rrset. Normally this will only return one DNSKEY. It can return
148      * more than one, since KeyID/Footprints are not guaranteed to be
149      * unique.
150      *
151      * @param dnskey_rrset
152      *            The DNSKEY rrset to search.
153      * @param signature
154      *            The RRSIG to match against.
155      * @return A List contains a one or more DNSKEYRecord objects, or null if a
156      *         matching DNSKEY could not be found.
157      */
158     @SuppressWarnings("rawtypes")
159     private List<DNSKEYRecord> findKey(RRset dnskey_rrset, RRSIGRecord signature) {
160         if (!signature.getSigner().equals(dnskey_rrset.getName())) {
161             log.trace("findKey: could not find appropriate key because "
162                       + "incorrect keyset was supplied. Wanted: "
163                       + signature.getSigner() + ", got: "
164                       + dnskey_rrset.getName());
165
166             return null;
167         }
168
169         int keyid = signature.getFootprint();
170         int alg   = signature.getAlgorithm();
171
172         List<DNSKEYRecord> res = new ArrayList<DNSKEYRecord>(dnskey_rrset.size());
173
174         for (Iterator i = dnskey_rrset.rrs(); i.hasNext();) {
175             DNSKEYRecord r = (DNSKEYRecord) i.next();
176
177             if ((r.getAlgorithm() == alg) && (r.getFootprint() == keyid)) {
178                 res.add(r);
179             }
180         }
181
182         if (res.size() == 0) {
183             log.trace("findKey: could not find a key matching " +
184                       "the algorithm and footprint in supplied keyset.");
185
186             return null;
187         }
188
189         return res;
190     }
191
192     /**
193      * Check to see if a signature looks valid (i.e., matches the
194      * rrset in question, in the validity period, etc.)
195      *
196      * @param rrset
197      *            The rrset that the signature belongs to.
198      * @param sigrec
199      *            The signature record to check.
200      * @return A value of SecurityStatus.SECURE if it looks OK,
201      *         SecurityStatus.BOGUS if it looks bad.
202      */
203     private byte checkSignature(RRset rrset, RRSIGRecord sigrec) {
204         if ((rrset == null) || (sigrec == null)) {
205             return SecurityStatus.BOGUS;
206         }
207
208         if (!rrset.getName().equals(sigrec.getName())) {
209             log.debug("Signature name does not match RRset name");
210
211             return SecurityStatus.BOGUS;
212         }
213
214         if (rrset.getType() != sigrec.getTypeCovered()) {
215             log.debug("Signature type does not match RRset type");
216
217             return SecurityStatus.BOGUS;
218         }
219
220         Date now    = new Date();
221         Date start  = sigrec.getTimeSigned();
222         Date expire = sigrec.getExpire();
223
224         if (now.before(start)) {
225             log.debug("Signature is not yet valid");
226
227             return SecurityStatus.BOGUS;
228         }
229
230         if (now.after(expire)) {
231             log.debug("Signature has expired (now = " + now +
232                       ", sig expires = " + expire);
233
234             return SecurityStatus.BOGUS;
235         }
236
237         return SecurityStatus.SECURE;
238     }
239
240     public PublicKey parseDNSKEY(DNSKEYRecord key) {
241         AlgEntry ae = (AlgEntry) mAlgorithmMap.get(Integer.valueOf(key.getAlgorithm()));
242
243         if (key.getAlgorithm() != ae.dnssecAlg) {
244             // Recast the DNSKEYRecord in question as one using the offical
245             // algorithm, to work around the lack of alias support in the
246             // underlying
247             // KEYConverter class from DNSjava
248             key = new DNSKEYRecord(key.getName(), key.getDClass(),
249                                    key.getTTL(), key.getFlags(), key.getProtocol(),
250                                    ae.dnssecAlg, key.getKey());
251         }
252
253         return KEYConverter.parseRecord(key);
254     }
255
256     /**
257      * Actually cryptographically verify a signature over the
258      * rrset. The RRSIG record must match the rrset being verified
259      * (see checkSignature).
260      *
261      * @param rrset
262      *            The rrset to verify.
263      * @param sigrec
264      *            The signature to verify with.
265      * @param key
266      *            The (public) key associated with the RRSIG record.
267      * @return A security status code: SECURE if it worked, BOGUS if not,
268      *         UNCHECKED if we just couldn't actually do the function.
269      */
270     public byte verifySignature(RRset rrset, RRSIGRecord sigrec,
271                                 DNSKEYRecord key) {
272         try {
273             PublicKey pk = parseDNSKEY(key);
274
275             if (pk == null) {
276                 log.warn("Could not convert DNSKEY record to a JCA public key: " + key);
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
323      *         if it did not verify (for any reason), and
324      *         DNSSEC.Insecure if verification could not be completed
325      *         (usually because the public key was not 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             return SecurityStatus.BOGUS;
339         }
340
341         byte status = SecurityStatus.UNCHECKED;
342
343         for (DNSKEYRecord key : keys) {
344             status = verifySignature(rrset, sigrec, key);
345
346             if (status == SecurityStatus.SECURE) {
347                 break;
348             }
349         }
350
351         return status;
352     }
353
354     /**
355      * Verifies an RRset. This routine does not modify the RRset. This
356      * RRset is presumed to be verifiable, and the correct DNSKEY
357      * rrset is presumed to have been found.
358      *
359      * @return SecurityStatus.SECURE if the rrest verified positively,
360      *         SecurityStatus.BOGUS otherwise.
361      */
362     @SuppressWarnings("rawtypes")
363     public byte verify(RRset rrset, RRset key_rrset) {
364         Iterator i = rrset.sigs();
365
366         if (!i.hasNext()) {
367             log.info("RRset failed to verify due to lack of signatures");
368
369             return SecurityStatus.BOGUS;
370         }
371
372         while (i.hasNext()) {
373             RRSIGRecord sigrec = (RRSIGRecord) i.next();
374
375             byte res = verifySignature(rrset, sigrec, key_rrset);
376
377             if (res == SecurityStatus.SECURE) {
378                 return res;
379             }
380         }
381
382         log.info("RRset failed to verify: all signatures were BOGUS");
383
384         return SecurityStatus.BOGUS;
385     }
386
387     /**
388      * Verify an RRset against a single DNSKEY. Use this when you must
389      * be certain that an RRset signed and verifies with a particular
390      * DNSKEY (as opposed to a particular DNSKEY rrset).
391      *
392      * @param rrset
393      *            The rrset to verify.
394      * @param dnskey
395      *            The DNSKEY to verify with.
396      * @return SecurityStatus.SECURE if the rrset verified, BOGUS otherwise.
397      */
398     @SuppressWarnings("rawtypes")
399     public byte verify(RRset rrset, DNSKEYRecord dnskey) {
400         // Iterate over RRSIGS
401         Iterator i = rrset.sigs();
402
403         if (!i.hasNext()) {
404             log.info("RRset failed to verify due to lack of signatures");
405
406             return SecurityStatus.BOGUS;
407         }
408
409         while (i.hasNext()) {
410             RRSIGRecord sigrec = (RRSIGRecord) i.next();
411
412             // Skip RRSIGs that do not match our given key's footprint.
413             if (sigrec.getFootprint() != dnskey.getFootprint()) {
414                 continue;
415             }
416
417             byte res = verifySignature(rrset, sigrec, dnskey);
418
419             if (res == SecurityStatus.SECURE) {
420                 return res;
421             }
422         }
423
424         log.info("RRset failed to verify: all signatures were BOGUS");
425
426         return SecurityStatus.BOGUS;
427     }
428
429     public boolean supportsAlgorithm(int algorithm) {
430         return mAlgorithmMap.containsKey(Integer.valueOf(algorithm));
431     }
432
433     public boolean supportsAlgorithm(Name private_id) {
434         return mPrivateAlgorithmMap.containsKey(private_id);
435     }
436
437     public int baseAlgorithm(int algorithm) {
438         switch (algorithm) {
439         case DNSSEC.Algorithm.RSAMD5:
440         case DNSSEC.Algorithm.RSASHA1:
441             return RSA;
442
443         case DNSSEC.Algorithm.DSA:
444             return DSA;
445         }
446
447         AlgEntry entry = (AlgEntry) mAlgorithmMap.get(Integer.valueOf(algorithm));
448
449         if (entry == null) {
450             return UNKNOWN;
451         }
452
453         if (entry.isDSA) {
454             return DSA;
455         }
456
457         return RSA;
458     }
459
460     /** @return the appropriate Signature object for this keypair. */
461     private Signature getSignature(int algorithm) {
462         Signature s = null;
463
464         try {
465             AlgEntry entry = (AlgEntry) mAlgorithmMap.get(Integer.valueOf(algorithm));
466
467             if (entry == null) {
468                 log.info("DNSSEC algorithm " + algorithm + " not recognized.");
469                 return null;
470             }
471
472             // TODO: should we cache the instance?
473             s = Signature.getInstance(entry.jcaName);
474         } catch (NoSuchAlgorithmException e) {
475             log.error("error getting Signature object", e);
476         }
477
478         return s;
479     }
480
481     private static class AlgEntry {
482         public String  jcaName;
483         public boolean isDSA;
484         public int     dnssecAlg;
485
486         public AlgEntry(String name, int dnssecAlg, boolean isDSA) {
487             jcaName = name;
488             this.dnssecAlg = dnssecAlg;
489             this.isDSA = isDSA;
490         }
491     }
492 }