// Copyright (C) 2001-2003, 2009, 2022 Verisign, Inc. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 // USA package com.verisignlabs.dnssec.security; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.Signature; import java.security.interfaces.DSAPublicKey; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ListIterator; import java.util.logging.Logger; import org.xbill.DNS.DNSKEYRecord; import org.xbill.DNS.DNSSEC; import org.xbill.DNS.Name; import org.xbill.DNS.RRSIGRecord; import org.xbill.DNS.RRset; import org.xbill.DNS.Record; import org.xbill.DNS.Type; import org.xbill.DNS.utils.hexdump; /** * This class contains routines for signing DNS zones. * * In particular, it contains both an ability to sign an individual RRset and * the ability to sign an entire zone. It primarily glues together the more * basic primitives found in {@link SignUtils}. * * @author David Blacka */ public class JCEDnsSecSigner { private DnsKeyConverter mKeyConverter; private boolean mVerboseSigning = false; private Logger log = Logger.getLogger(this.getClass().toString()); public JCEDnsSecSigner() { this.mKeyConverter = null; this.mVerboseSigning = false; } public JCEDnsSecSigner(boolean verboseSigning) { this.mKeyConverter = null; this.mVerboseSigning = verboseSigning; } /** * Cryptographically generate a new DNSSEC key. * * @param owner the KEY RR's owner name. * @param ttl the KEY RR's TTL. * @param dclass the KEY RR's DNS class. * @param algorithm the DNSSEC algorithm (RSASHA258, RSASHA512, * ECDSAP256, etc.) * @param flags any flags for the KEY RR. * @param keysize the size of the key to generate. * @param useLargeExponent if generating an RSA key, use the large exponent. * @return a DnsKeyPair with the public and private keys populated. */ public DnsKeyPair generateKey(Name owner, long ttl, int dclass, int algorithm, int flags, int keysize, boolean useLargeExponent) throws NoSuchAlgorithmException { DnsKeyAlgorithm algorithms = DnsKeyAlgorithm.getInstance(); if (ttl < 0) ttl = 86400; // set to a reasonable default. KeyPair pair = algorithms.generateKeyPair(algorithm, keysize, useLargeExponent); if (mKeyConverter == null) { mKeyConverter = new DnsKeyConverter(); } DNSKEYRecord keyrec = mKeyConverter.generateDNSKEYRecord(owner, dclass, ttl, flags, algorithm, pair.getPublic()); DnsKeyPair dnspair = new DnsKeyPair(); dnspair.setDNSKEYRecord(keyrec); dnspair.setPublic(pair.getPublic()); // keep from conv. the keyrec back. dnspair.setPrivate(pair.getPrivate()); return dnspair; } /** * Sign an RRset. * * @param rrset the RRset to sign -- any existing signatures are ignored. * @param keypars a list of DnsKeyPair objects containing private keys. * @param start the inception time for the resulting RRSIG records. * @param expire the expiration time for the resulting RRSIG records. * @return a list of RRSIGRecord objects. */ public List signRRset(RRset rrset, List keypairs, Instant start, Instant expire) throws IOException, GeneralSecurityException { if (rrset == null || keypairs == null) return new ArrayList<>(); // default start to now, expire to start + 1 second. if (start == null) start = Instant.now(); if (expire == null) expire = start.plusSeconds(1); if (keypairs.isEmpty()) return new ArrayList<>(); if (mVerboseSigning) { log.info("Signing RRset:"); log.info(ZoneUtils.rrsetToString(rrset, false)); } // first, pre-calculate the RRset bytes. byte[] rrsetData = SignUtils.generateCanonicalRRsetData(rrset, 0, 0); ArrayList sigs = new ArrayList<>(keypairs.size()); // for each keypair, sign the RRset. for (DnsKeyPair pair : keypairs) { DNSKEYRecord keyrec = pair.getDNSKEYRecord(); if (keyrec == null) continue; RRSIGRecord presig = SignUtils.generatePreRRSIG(rrset, keyrec, start, expire, rrset.getTTL()); byte[] signData = SignUtils.generateSigData(rrsetData, presig); if (mVerboseSigning) { log.info("Canonical pre-signature data to sign with key " + keyrec.getName().toString() + "/" + keyrec.getAlgorithm() + "/" + keyrec.getFootprint() + ":"); log.info(hexdump.dump(null, signData)); } Signature signer = pair.getSigner(); if (signer == null) { // debug log.fine("missing private key that goes with:\n" + pair.getDNSKEYRecord()); throw new GeneralSecurityException("cannot sign without a valid Signer " + "(probably missing private key)"); } // sign the data. signer.update(signData); byte[] sig = signer.sign(); if (mVerboseSigning) { log.info("Raw Signature:"); log.info(hexdump.dump(null, sig)); } DnsKeyAlgorithm algs = DnsKeyAlgorithm.getInstance(); // Convert to RFC 2536 format, if necessary. if (algs.baseType(pair.getDNSKEYAlgorithm()) == DnsKeyAlgorithm.BaseAlgorithm.DSA) { DSAPublicKey pk = (DSAPublicKey) pair.getPublic(); sig = SignUtils.convertDSASignature(pk.getParams(), sig); } // Convert to RFC 6605, etc format if (pair.getDNSKEYAlgorithm() == DNSSEC.Algorithm.ECDSAP256SHA256 || pair.getDNSKEYAlgorithm() == DNSSEC.Algorithm.ECDSAP384SHA384) { sig = SignUtils.convertECDSASignature(pair.getDNSKEYAlgorithm(), sig); } RRSIGRecord sigrec = SignUtils.generateRRSIG(sig, presig); if (mVerboseSigning) { log.info("RRSIG:\n" + sigrec); } sigs.add(sigrec); } return sigs; } /** * Create a completely self-signed DNSKEY RRset. * * @param keypairs the public & private keypairs to use in the keyset. * @param start the RRSIG inception time. * @param expire the RRSIG expiration time. * @return a signed RRset. */ public RRset makeKeySet(List keypairs, Instant start, Instant expire) throws IOException, GeneralSecurityException { // Generate a KEY RR set to sign. RRset keyset = new RRset(); for (DnsKeyPair pair : keypairs) { keyset.addRR(pair.getDNSKEYRecord()); } List records = signRRset(keyset, keypairs, start, expire); for (RRSIGRecord r : records) { keyset.addRR(r); } return keyset; } /** * Conditionally sign an RRset and add it to the toList. * * @param toList the list to which we are adding the processed RRsets. * @param zonename the zone apex name. * @param rrset the RRset under consideration. * @param kskpairs the List of KSKs.. * @param zskpairs the List of zone keys. * @param start the RRSIG inception time. * @param expire the RRSIG expiration time. * @param fullySignKeyset if true, sign the zone apex keyset with both KSKs * and ZSKs. * @param lastCut the name of the last delegation point encountered. * * @return the name of the new last_cut. */ private Name addRRset(List toList, Name zonename, RRset rrset, List kskpairs, List zskpairs, Instant start, Instant expire, boolean fullySignKeyset, Name lastCut, Name lastDname) throws IOException, GeneralSecurityException { // add the records themselves rrset.rrs().forEach(toList::add); int type = SignUtils.recordSecType(zonename, rrset.getName(), rrset.getType(), lastCut, lastDname); // we don't sign non-normal sets (delegations, glue, invalid). if (type == SignUtils.RR_DELEGATION) { return rrset.getName(); } if (type == SignUtils.RR_GLUE || type == SignUtils.RR_INVALID) { return lastCut; } // check for the zone apex keyset. if (rrset.getName().equals(zonename) && rrset.getType() == Type.DNSKEY && kskpairs != null && !kskpairs.isEmpty()) { // if we have ksks, sign the keyset with them, otherwise we will just sign // them with the zsks. List sigs = signRRset(rrset, kskpairs, start, expire); toList.addAll(sigs); // If we aren't going to sign with all the keys, bail out now. if (!fullySignKeyset) return lastCut; } // otherwise, we are OK to sign this set. List sigs = signRRset(rrset, zskpairs, start, expire); toList.addAll(sigs); return lastCut; } // Various NSEC/NSEC3 generation modes private static final int NSEC_MODE = 0; private static final int NSEC3_MODE = 1; private static final int NSEC3_OPTOUT_MODE = 2; private static final int NSEC_EXP_OPT_IN = 3; /** * Master zone signing method. This method handles all of the different zone * signing variants (NSEC with or without Opt-In, NSEC3 with or without * Opt-Out, etc.) External users of this class are expected to use the * appropriate public signZone* methods instead of this. * * @param zonename The name of the zone * @param records The records comprising the zone. They do not have to * be in any particular order, as this method will * order them as necessary. * @param kskpairs The key pairs designated as "key signing keys" * @param zskpairs The key pairs designated as "zone signing keys" * @param start The RRSIG inception time * @param expire The RRSIG expiration time * @param fullySignKeyset If true, all keys (ksk or zsk) will sign the DNSKEY * RRset. If false, only the ksks will sign it. * @param dsDigestAlg The hash algorithm to use for generating DS records * (DSRecord.SHA1_DIGEST_ID, e.g.) * @param mode The NSEC/NSEC3 generation mode: NSEC_MODE, * NSEC3_MODE, NSEC3_OPTOUT_MODE, etc. * @param includedNames When using an Opt-In/Opt-Out mode, the names listed * here will be included in the NSEC/NSEC3 chain * regardless * @param salt When using an NSEC3 mode, use this salt. * @param iterations When using an NSEC3 mode, use this number of * iterations * @param beConservative If true, then only turn on the Opt-In flag when * there are insecure delegations in the span. * Currently this only works for NSEC_EXP_OPT_IN mode. * @param nsec3paramttl The TTL to use for the generated NSEC3PARAM record. * Negative values will use the SOA TTL. * @return an ordered list of {@link org.xbill.DNS.Record} objects, * representing the signed zone. * * @throws IOException * @throws GeneralSecurityException */ private List signZone(Name zonename, List records, List kskpairs, List zskpairs, Instant start, Instant expire, boolean fullySignKeyset, int dsDigestAlg, int mode, List includedNames, byte[] salt, int iterations, long nsec3paramttl, boolean beConservative) throws IOException, GeneralSecurityException { // Remove any existing generated DNSSEC records (NSEC, NSEC3, NSEC3PARAM, // RRSIG) SignUtils.removeGeneratedRecords(zonename, records); RecordComparator rc = new RecordComparator(); // Sort the zone Collections.sort(records, rc); // Remove duplicate records SignUtils.removeDuplicateRecords(records); // Generate DS records. This replaces any non-zone-apex DNSKEY RRs with DS // RRs. SignUtils.generateDSRecords(zonename, records, dsDigestAlg); // Generate the NSEC or NSEC3 records based on 'mode' switch (mode) { case NSEC_MODE: SignUtils.generateNSECRecords(zonename, records); break; case NSEC3_MODE: SignUtils.generateNSEC3Records(zonename, records, salt, iterations, nsec3paramttl); break; case NSEC3_OPTOUT_MODE: SignUtils.generateOptOutNSEC3Records(zonename, records, includedNames, salt, iterations, nsec3paramttl); break; case NSEC_EXP_OPT_IN: SignUtils.generateOptInNSECRecords(zonename, records, includedNames, beConservative); break; default: throw new NoSuchAlgorithmException("Unknown NSEC/NSEC3 mode: " + mode); } // Re-sort so we can assemble into rrsets. Collections.sort(records, rc); // Assemble into RRsets and sign. RRset rrset = new RRset(); ArrayList signedRecords = new ArrayList<>(); Name lastCut = null; Name lastDname = null; for (ListIterator i = records.listIterator(); i.hasNext();) { Record r = i.next(); // First record if (rrset.size() == 0) { rrset.addRR(r); continue; } // Current record is part of the current RRset. if (rrset.getName().equals(r.getName()) && rrset.getDClass() == r.getDClass() && rrset.getType() == r.getType()) { rrset.addRR(r); continue; } // Otherwise, we have completed the RRset // Sign the records // add the RRset to the list of signed_records, regardless of // whether or not we actually end up signing the set. lastCut = addRRset(signedRecords, zonename, rrset, kskpairs, zskpairs, start, expire, fullySignKeyset, lastCut, lastDname); if (rrset.getType() == Type.DNAME) lastDname = rrset.getName(); rrset.clear(); rrset.addRR(r); } // add the last RR set addRRset(signedRecords, zonename, rrset, kskpairs, zskpairs, start, expire, fullySignKeyset, lastCut, lastDname); return signedRecords; } /** * Given a zone, sign it using standard NSEC records. * * @param zonename The name of the zone. * @param records The records comprising the zone. They do not have to * be in any particular order, as this method will * order them as necessary. * @param kskpairs The key pairs that are designated as "key signing * keys". * @param zskpairs This key pairs that are designated as "zone signing * keys". * @param start The RRSIG inception time. * @param expire The RRSIG expiration time. * @param fullySignKeyset Sign the zone apex keyset with all available keys * (instead of just the key signing keys). * @param dsDigestAlg The digest algorithm to use when generating DS * records. * * @return an ordered list of {@link org.xbill.DNS.Record} objects, * representing the signed zone. */ public List signZone(Name zonename, List records, List kskpairs, List zskpairs, Instant start, Instant expire, boolean fullySignKeyset, int dsDigestAlg) throws IOException, GeneralSecurityException { return signZone(zonename, records, kskpairs, zskpairs, start, expire, fullySignKeyset, dsDigestAlg, NSEC_MODE, null, null, 0, 0, false); } /** * Given a zone, sign it using NSEC3 records. * * @param signer A signer (utility) object used to actually sign * stuff. * @param zonename The name of the zone being signed. * @param records The records comprising the zone. They do not have to * be in any particular order, as this method will * order them as necessary. * @param kskpairs The key pairs that are designated as "key signing * keys". * @param zskpairs This key pairs that are designated as "zone signing * keys". * @param start The RRSIG inception time. * @param expire The RRSIG expiration time. * @param fullySignKeyset If true then the DNSKEY RRset will be signed by all * available keys, if false, only the key signing keys. * @param useOptOut If true, insecure delegations will be omitted from * the NSEC3 chain, and all NSEC3 records will have the * Opt-Out flag set. * @param includedNames A list of names to include in the NSEC3 chain * regardless. * @param salt The salt to use for the NSEC3 hashing. null means no * salt. * @param iterations The number of iterations to use for the NSEC3 * hashing. * @param dsDigestAlg The digest algorithm to use when generating DS * records. * @param nsec3paramttl The TTL to use for the generated NSEC3PARAM record. * Negative values will use the SOA TTL. * @return an ordered list of {@link org.xbill.DNS.Record} objects, * representing the signed zone. * * @throws IOException * @throws GeneralSecurityException */ public List signZoneNSEC3(Name zonename, List records, List kskpairs, List zskpairs, Instant start, Instant expire, boolean fullySignKeyset, boolean useOptOut, List includedNames, byte[] salt, int iterations, int dsDigestAlg, long nsec3paramttl) throws IOException, GeneralSecurityException { if (useOptOut) { return signZone(zonename, records, kskpairs, zskpairs, start, expire, fullySignKeyset, dsDigestAlg, NSEC3_OPTOUT_MODE, includedNames, salt, iterations, nsec3paramttl, false); } else { return signZone(zonename, records, kskpairs, zskpairs, start, expire, fullySignKeyset, dsDigestAlg, NSEC3_MODE, null, salt, iterations, nsec3paramttl, false); } } /** * Given a zone, sign it using experimental Opt-In NSEC records (see RFC * 4956). * * @param zonename the name of the zone. * @param records the records comprising the zone. They do not * have to be in any particular order, as this * method will order them as necessary. * @param kskpairs the key pairs that are designated as "key * signing keys". * @param zskpairs this key pairs that are designated as "zone * signing keys". * @param start the RRSIG inception time. * @param expire the RRSIG expiration time. * @param useConservativeOptIn if true, Opt-In NSEC records will only be * generated if there are insecure, unsigned * delegations in the span. * @param fullySignKeyset sign the zone apex keyset with all available * keys. * @param dsDigestAlg The digest algorithm to use when generating DS * records. * @param nsecIncludeNames names that are to be included in the NSEC chain * regardless. This may be null. * @return an ordered list of {@link org.xbill.DNS.Record} objects, * representing the signed zone. */ public List signZoneOptIn(Name zonename, List records, List kskpairs, List zskpairs, Instant start, Instant expire, boolean useConservativeOptIn, boolean fullySignKeyset, List nsecIncludeNames, int dsDigestAlg) throws IOException, GeneralSecurityException { return signZone(zonename, records, kskpairs, zskpairs, start, expire, fullySignKeyset, dsDigestAlg, NSEC_EXP_OPT_IN, nsecIncludeNames, null, 0, 0, useConservativeOptIn); } }