This is an automated email from the ASF dual-hosted git repository.

zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new d640e91  TR: cache and reuse RRSIG records to avoid unnecessary 
re-signing (#5598)
d640e91 is described below

commit d640e9151fa626a9aa9b3aab8f4a8e2faa1e6ff9
Author: Rawlin Peters <[email protected]>
AuthorDate: Thu Mar 4 14:55:02 2021 -0700

    TR: cache and reuse RRSIG records to avoid unnecessary re-signing (#5598)
    
    * TR: cache and reuse RRSIG records to avoid unnecessary re-signing
    
    Add a new TR profile parameter: 'dnssec.rrsig.cache.enabled'. When set
    to 'true', RRSIG records will be cached and reused when the same RRset
    is re-signed. This heavily reduces the amount of CPU used to re-sign
    zones after a change is made that requires re-signing large numbers of
    zones (e.g. ONLINING/OFFLINING Traffic Routers). Also, this causes
    Traffic Router to startup much quicker, because duplicate work is
    eliminated during the initial zone signing process.
    
    As DNSSEC keys are removed/replaced, the RRSIG cache is cleaned up, and
    entries that are no longer necessary are removed. RRsets that have a
    cached RRSIG record that is more than halfway to expiring will be
    re-signed.
    
    Setting 'dnssec.rrsig.cache.enabled' to 'false' will clear the cache and
    prevent it from being used for new requests until re-enabled again.
    
    * Remove unused getters, collapse lambda expressions
---
 CHANGELOG.md                                       |   1 +
 docs/source/admin/traffic_router.rst               |   5 +
 .../traffic_router/core/dns/RRSIGCacheKey.java     |  48 +++++++
 .../traffic_router/core/dns/RRsetKey.java          |  59 ++++++++
 .../traffic_router/core/dns/SignatureManager.java  |  82 +++++++++--
 .../traffic_router/core/dns/SignedZoneKey.java     |  28 +++-
 .../traffic_router/core/dns/ZoneManager.java       |   7 +
 .../traffic_router/core/dns/ZoneSigner.java        |   8 +-
 .../traffic_router/core/dns/ZoneSignerImpl.java    |  46 +++++-
 .../traffic_router/core/router/TrafficRouter.java  |   1 +
 .../core/dns/ZoneSignerImplTest.java               | 160 +++++++++++++++++++++
 11 files changed, 419 insertions(+), 26 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2e5f5c..5e9bc0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Portal: upgraded delivery service UI tables to use more 
powerful/performant ag-grid component
 - Traffic Ops: added a feature so that the user can specify 
`maxRequestHeaderBytes` on a per delivery service basis
 - Traffic Router: log warnings when requests to Traffic Monitor return a 503 
status code
+- Traffic Router: added new 'dnssec.rrsig.cache.enabled' profile parameter to 
enable new DNSSEC RRSIG caching functionality. Enabling this greatly reduces 
CPU usage during the DNSSEC signing process.
 - [#5316](https://github.com/apache/trafficcontrol/issues/5316) - Add router 
host names and ports on a per interface basis, rather than a per server basis.
 - [#5344](https://github.com/apache/trafficcontrol/issues/5344) - Add a page 
that addresses migrating from Traffic Ops API v1 for each endpoint
 - [#5296](https://github.com/apache/trafficcontrol/issues/5296) - Fixed a bug 
where users couldn't update any regex in Traffic Ops/ Traffic Portal
diff --git a/docs/source/admin/traffic_router.rst 
b/docs/source/admin/traffic_router.rst
index 77acbd4..90002ab 100644
--- a/docs/source/admin/traffic_router.rst
+++ b/docs/source/admin/traffic_router.rst
@@ -256,6 +256,11 @@ Much of a Traffic Router's configuration can be obtained 
through the :term:`Para
        | dnssec.zone.diffing.enabled             | CRConfig.json               
 | If DNSSEC is enabled, enabling this parameter allows Traffic Router to diff 
existing zones with newly generated zones. If the newly   |
        |                                         |                             
 | generated zone is the same as the existing zone, Traffic Router will simply 
reuse the existing signed zone instead of signing the     |
        |                                         |                             
 | same new zone. This reduces the CPU time taken to process new snapshots and 
new DNSSEC keys. Defaults to "false".                     |
+       |                                         |                             
 | NOTE: this may be removed in favor of the ``dnssec.rrsig.cache.enabled`` 
setting in a future release.                                 |
+       
+-----------------------------------------+------------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
+       | dnssec.rrsig.cache.enabled              | CRConfig.json               
 | If DNSSEC is enabled, enabling this parameter allows Traffic Router to cache 
RRSIG records for reuse during DNSSEC signing.           |
+       |                                         |                             
 | This greatly reduces the CPU time taken to sign DNS zones. Defaults to 
"false".                                                       |
+       |                                         |                             
 | NOTE: this may supersede the ``dnssec.zone.diffing.enabled`` setting in a 
future release.                                             |
        
+-----------------------------------------+------------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
        | dnssec.allow.expired.keys               | CRConfig.json               
 | Allow Traffic Router to use expired DNSSEC keys to sign zones; default is 
"true". This helps prevent DNSSEC related outages due to    |
        |                                         |                             
 | failed Traffic Control components or connectivity issues.                    
                                                         |
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/RRSIGCacheKey.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/RRSIGCacheKey.java
new file mode 100644
index 0000000..d266eba
--- /dev/null
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/RRSIGCacheKey.java
@@ -0,0 +1,48 @@
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.comcast.cdn.traffic_control.traffic_router.core.dns;
+
+import java.util.Arrays;
+
+public class RRSIGCacheKey {
+    final private byte[] privateKeyBytes;
+    final private int algorithm;
+
+    public RRSIGCacheKey(final byte[] privateKeyBytes, final int algorithm) {
+        this.privateKeyBytes = privateKeyBytes;
+        this.algorithm = algorithm;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final RRSIGCacheKey that = (RRSIGCacheKey) o;
+        return algorithm == that.algorithm && Arrays.equals(privateKeyBytes, 
that.privateKeyBytes);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = algorithm;
+        result = 31 * result + Arrays.hashCode(privateKeyBytes);
+        return result;
+    }
+
+}
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/RRsetKey.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/RRsetKey.java
new file mode 100644
index 0000000..96db4db
--- /dev/null
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/RRsetKey.java
@@ -0,0 +1,59 @@
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.comcast.cdn.traffic_control.traffic_router.core.dns;
+
+import org.xbill.DNS.RRset;
+import org.xbill.DNS.Record;
+
+import java.util.Iterator;
+
+public class RRsetKey {
+    final private RRset rrset;
+
+    public RRsetKey(final RRset rrset) {
+        this.rrset = rrset;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final RRsetKey that = (RRsetKey) o;
+        final Iterator thisIterator = rrset.rrs(false);
+        final Iterator thatIterator = that.rrset.rrs(false);
+        while (thisIterator.hasNext() && thatIterator.hasNext()) {
+            if (!thisIterator.next().equals(thatIterator.next())) {
+                return false;
+            }
+        }
+        return !thisIterator.hasNext() && !thatIterator.hasNext();
+    }
+
+    @Override
+    public int hashCode() {
+        int hashCode = 1;
+        final Iterator it = rrset.rrs(false);
+        while (it.hasNext()) {
+            final Record r = (Record) it.next();
+            hashCode = 31*hashCode + (r==null? 0 : r.hashCode());
+        }
+        return hashCode;
+    }
+}
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignatureManager.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignatureManager.java
index dc5f75a..1695dd8 100644
--- 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignatureManager.java
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignatureManager.java
@@ -22,9 +22,13 @@ import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -39,6 +43,7 @@ import org.apache.log4j.Logger;
 import org.xbill.DNS.DSRecord;
 import org.xbill.DNS.Name;
 import org.xbill.DNS.Record;
+import org.xbill.DNS.RRSIGRecord;
 import org.xbill.DNS.TextParseException;
 import com.comcast.cdn.traffic_control.traffic_router.core.edge.CacheRegister;
 import 
com.comcast.cdn.traffic_control.traffic_router.core.dns.ZoneManager.ZoneCacheType;
@@ -50,6 +55,9 @@ public final class SignatureManager {
        private static final Logger LOGGER = 
Logger.getLogger(SignatureManager.class);
        private int expirationMultiplier;
        private CacheRegister cacheRegister;
+       private static ConcurrentMap<RRSIGCacheKey, ConcurrentMap<RRsetKey, 
RRSIGRecord>> RRSIGCache = new ConcurrentHashMap<>();
+       private static final Object RRSIGCacheLock = new Object(); // to ensure 
that the RRSIGCache is totally empty if disabled
+       private boolean RRSIGCacheEnabled = false;
        private static ScheduledExecutorService keyMaintenanceExecutor;
        private TrafficOpsUtils trafficOpsUtils;
        private boolean dnssecEnabled = false;
@@ -64,6 +72,7 @@ public final class SignatureManager {
                this.setCacheRegister(cacheRegister);
                this.setTrafficOpsUtils(trafficOpsUtils);
                this.setZoneManager(zoneManager);
+               setRRSIGCacheEnabled(cacheRegister.getConfig());
                initKeyMap();
        }
 
@@ -73,6 +82,19 @@ public final class SignatureManager {
                }
        }
 
+       private void setRRSIGCacheEnabled(final JsonNode config) {
+               RRSIGCacheEnabled = JsonUtils.optBoolean(config, 
TrafficRouter.DNSSEC_RRSIG_CACHE_ENABLED, false);
+               if (!RRSIGCacheEnabled) {
+                       synchronized (RRSIGCacheLock) {
+                               RRSIGCache = new ConcurrentHashMap<>();
+                       }
+               }
+       }
+
+       private boolean isRRSIGCacheEnabled() {
+               return this.RRSIGCacheEnabled;
+       }
+
        private void initKeyMap() {
                synchronized(SignatureManager.class) {
                        final JsonNode config = cacheRegister.getConfig();
@@ -154,6 +176,7 @@ public final class SignatureManager {
                                                                }
                                                        }
                                                }
+                                               cleanRRSIGCache(keyMap, 
newKeyMap);
 
                                                if (keyMap == null) {
                                                        // initial startup
@@ -177,16 +200,44 @@ public final class SignatureManager {
                };
        }
 
-       private boolean hasNewKeys(final Map<String, List<DnsSecKeyPair>> 
keyMap, final Map<String, List<DnsSecKeyPair>> newKeyMap) {
-               for (final String key : newKeyMap.keySet()) {
-                       if (!keyMap.containsKey(key)) {
-                               return true;
+       private void cleanRRSIGCache(final Map<String, List<DnsSecKeyPair>> 
oldKeyMap, final Map<String, List<DnsSecKeyPair>> newKeyMap) {
+               synchronized (RRSIGCacheLock) {
+                       if (RRSIGCache.isEmpty() || oldKeyMap == null || 
getKeyDifferences(oldKeyMap, newKeyMap).isEmpty()) {
+                               return;
+                       }
+                       final int oldKeySize = RRSIGCache.size();
+                       final int oldRRSIGSize = 
RRSIGCache.values().stream().map(Map::size).reduce(0, Integer::sum);
+                       final long now = new Date().getTime();
+                       final ConcurrentMap<RRSIGCacheKey, 
ConcurrentMap<RRsetKey, RRSIGRecord>> newRRSIGCache = new ConcurrentHashMap<>();
+                       newKeyMap.forEach((name, keyPairs) -> 
keyPairs.forEach(keypair -> {
+                               final RRSIGCacheKey cacheKey = new 
RRSIGCacheKey(keypair.getPrivate().getEncoded(), 
keypair.getDNSKEYRecord().getAlgorithm());
+                               final ConcurrentMap<RRsetKey, RRSIGRecord> 
cacheValue = RRSIGCache.get(cacheKey);
+                               if (cacheValue != null) {
+                                       cacheValue.entrySet().removeIf(e -> 
e.getValue().getExpire().getTime() <= now);
+                                       newRRSIGCache.put(cacheKey, cacheValue);
+                               }
+                       }));
+                       RRSIGCache = newRRSIGCache;
+                       final int keySize = RRSIGCache.size();
+                       final int RRSIGSize = 
RRSIGCache.values().stream().map(Map::size).reduce(0, Integer::sum);
+                       LOGGER.info("DNSSEC keys were changed or removed so 
RRSIG cache was cleaned. Old key size: " + oldKeySize +
+                                       ", new key size: " + keySize + ", old 
RRSIG size: " + oldRRSIGSize + ", new RRSIG size: " + RRSIGSize);
+               }
+       }
+
+       // return the key names from newKeyMap that are different or missing 
from oldKeyMap
+       private Set<String> getKeyDifferences(final Map<String, 
List<DnsSecKeyPair>> newKeyMap, final Map<String, List<DnsSecKeyPair>> 
oldKeyMap) {
+               final Set<String> newKeyNames = new HashSet<>();
+               for (final String newName : newKeyMap.keySet()) {
+                       if (!oldKeyMap.containsKey(newName)) {
+                               newKeyNames.add(newName);
+                               continue;
                        }
 
-                       for (final DnsSecKeyPair newKeyPair : 
newKeyMap.get(key)) {
+                       for (final DnsSecKeyPair newKeyPair : 
newKeyMap.get(newName)) {
                                boolean matched = false;
 
-                               for (final DnsSecKeyPair keyPair : 
keyMap.get(key)) {
+                               for (final DnsSecKeyPair keyPair : 
oldKeyMap.get(newName)) {
                                        if (newKeyPair.equals(keyPair)) {
                                                matched = true;
                                                break;
@@ -194,12 +245,20 @@ public final class SignatureManager {
                                }
 
                                if (!matched) {
-                                       LOGGER.info("Found new or changed key 
for " + newKeyPair.getName());
-                                       return true; // has a new key because 
we didn't find a match
+                                       newKeyNames.add(newKeyPair.getName());
+                                       break;
                                }
                        }
                }
+               return newKeyNames;
+       }
 
+       private boolean hasNewKeys(final Map<String, List<DnsSecKeyPair>> 
oldKeyMap, final Map<String, List<DnsSecKeyPair>> newKeyMap) {
+               final Set<String> newOrChangedKeyNames = 
getKeyDifferences(newKeyMap, oldKeyMap);
+               if (!newOrChangedKeyNames.isEmpty()) {
+                       newOrChangedKeyNames.forEach(name -> LOGGER.info("Found 
new or changed key for " + name));
+                       return true;
+               }
                return false;
        }
 
@@ -407,7 +466,7 @@ public final class SignatureManager {
                sb.append(" is ");
                sb.append(zoneKey.getTimestampDate());
                sb.append("; expires ");
-               sb.append(zoneKey.getSignatureExpiration().getTime());
+               sb.append(zoneKey.getMinimumSignatureExpiration().getTime());
 
                if (needsRefresh) {
                        sb.append("; refresh needed");
@@ -447,9 +506,10 @@ public final class SignatureManager {
 
                                final ZoneSigner zoneSigner = new 
ZoneSignerImpl();
 
-                               signedRecords = zoneSigner.signZone(name, 
records, kskPairs, zskPairs, start.getTime(), signatureExpiration.getTime(), 
true, DSRecord.SHA256_DIGEST_ID);
+                               signedRecords = zoneSigner.signZone(records, 
kskPairs, zskPairs, start.getTime(),
+                                               signatureExpiration.getTime(), 
isRRSIGCacheEnabled() ? RRSIGCache : null);
 
-                               
zoneKey.setSignatureExpiration(signatureExpiration);
+                               
zoneKey.setMinimumSignatureExpiration(signedRecords, signatureExpiration);
                                zoneKey.setKSKExpiration(kskExpiration);
                                zoneKey.setZSKExpiration(zskExpiration);
 
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignedZoneKey.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignedZoneKey.java
index 61fd439..9918afd 100644
--- 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignedZoneKey.java
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/SignedZoneKey.java
@@ -17,12 +17,17 @@ package 
com.comcast.cdn.traffic_control.traffic_router.core.dns;
 
 import java.util.Calendar;
 import java.util.List;
+import java.util.OptionalLong;
 
+import org.apache.log4j.Logger;
 import org.xbill.DNS.Name;
+import org.xbill.DNS.RRSIGRecord;
 import org.xbill.DNS.Record;
 
 public class SignedZoneKey extends ZoneKey {
-       private Calendar signatureExpiration;
+       private static final Logger LOGGER = 
Logger.getLogger(SignedZoneKey.class);
+
+       private Calendar minimumSignatureExpiration;
        private Calendar kskExpiration;
        private Calendar zskExpiration;
 
@@ -31,16 +36,27 @@ public class SignedZoneKey extends ZoneKey {
                super(name, records);
        }
 
-       public Calendar getSignatureExpiration() {
-               return signatureExpiration;
+       public Calendar getMinimumSignatureExpiration() {
+               return minimumSignatureExpiration;
        }
 
-       public void setSignatureExpiration(final Calendar signatureExpiration) {
-               this.signatureExpiration = signatureExpiration;
+       public void setMinimumSignatureExpiration(final List<Record> 
signedRecords, final Calendar defaultExpiration) {
+               final OptionalLong minSignatureExpiration = 
signedRecords.stream()
+                               .filter(r -> r instanceof RRSIGRecord)
+                               .mapToLong(r -> ((RRSIGRecord) 
r).getExpire().getTime())
+                               .min();
+               if (!minSignatureExpiration.isPresent()) {
+                       LOGGER.error("unable to calculate minimum signature 
expiration: no RRSIG records given");
+                       this.minimumSignatureExpiration = defaultExpiration;
+                       return;
+               }
+               final Calendar tmp = Calendar.getInstance();
+               tmp.setTimeInMillis(minSignatureExpiration.getAsLong());
+               this.minimumSignatureExpiration = tmp;
        }
 
        public long getSignatureDuration() {
-               return this.signatureExpiration.getTimeInMillis() - 
getTimestamp();
+               return this.minimumSignatureExpiration.getTimeInMillis() - 
getTimestamp();
        }
 
        public long getRefreshHorizon() {
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneManager.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneManager.java
index e53845e..9b82493 100644
--- 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneManager.java
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneManager.java
@@ -212,6 +212,11 @@ public class ZoneManager extends Resolver {
                                ZoneManager.dynamicZoneCache = dzc;
                                ZoneManager.zoneCache = zc;
 
+                               final long oldZCSize = tzc == null ? 0 : 
tzc.size();
+                               final long oldDCZSize = tzc == null ? 0 : 
tdzc.size();
+                               LOGGER.info("old static zone cache size: " + 
oldZCSize + ", new static zone cache size: " + zc.size() +
+                                               ", old dynamic zone cache size: 
" + oldDCZSize + ", new dynamic zone cache size: " + dzc.size());
+
                                ZoneManager.domainsToZoneKeys = 
newDomainsToZoneKeys;
 
                                if (tze != null) {
@@ -278,6 +283,7 @@ public class ZoneManager extends Resolver {
        private static Runnable getMaintenanceRunnable(final 
LoadingCache<ZoneKey, Zone> cache, final ZoneCacheType type, final int 
refreshInterval) {
                return new Runnable() {
                        public void run() {
+                               LOGGER.info("starting maintenance on " + 
type.toString() + " zone cache: " + Integer.toHexString(cache.hashCode()) + ". 
Current size: " + cache.size());
                                cache.cleanUp();
 
                                for (final ZoneKey zoneKey : 
cache.asMap().keySet()) {
@@ -289,6 +295,7 @@ public class ZoneManager extends Resolver {
                                                LOGGER.fatal("RuntimeException 
caught on " + zoneKey.getClass().getSimpleName() + " for " + zoneKey.getName(), 
ex);
                                        }
                                }
+                               LOGGER.info("completed maintenance on " + 
type.toString() + " zone cache: " + Integer.toHexString(cache.hashCode()));
                        }
                };
        }
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSigner.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSigner.java
index c2fa4d9..9d17024 100644
--- 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSigner.java
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSigner.java
@@ -17,16 +17,18 @@ package 
com.comcast.cdn.traffic_control.traffic_router.core.dns;
 
 import org.xbill.DNS.DNSKEYRecord;
 import org.xbill.DNS.DSRecord;
-import org.xbill.DNS.Name;
 import org.xbill.DNS.Record;
+import org.xbill.DNS.RRSIGRecord;
 
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.ConcurrentMap;
 
 public interface ZoneSigner {
-       List<Record> signZone(Name name, List<Record> records, 
List<DnsSecKeyPair> kskPairs, List<DnsSecKeyPair> zskPairs,
-                             Date inception, Date expiration, boolean 
fullySignKeySet, int digestId) throws IOException, GeneralSecurityException;
+       List<Record> signZone(List<Record> records, List<DnsSecKeyPair> 
kskPairs,
+                                                 List<DnsSecKeyPair> zskPairs, 
Date inception, Date expiration,
+                                                 ConcurrentMap<RRSIGCacheKey, 
ConcurrentMap<RRsetKey, RRSIGRecord>> RRSIGCache) throws IOException, 
GeneralSecurityException;
        DSRecord calculateDSRecord(DNSKEYRecord dnskeyRecord, int digestId, 
long ttl);
 }
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSignerImpl.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSignerImpl.java
index b23e2a9..743c88b 100644
--- 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSignerImpl.java
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSignerImpl.java
@@ -37,6 +37,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
@@ -57,21 +59,53 @@ public class ZoneSignerImpl implements ZoneSigner {
                return StreamSupport.stream(iterable.spliterator(), false);
        }
 
-       private RRset signRRset(final RRset rrSet, final List<DnsSecKeyPair> 
kskPairs, final List<DnsSecKeyPair> zskPairs, final Date inception, final Date 
expiration) {
+       private RRSIGRecord sign(final RRset rrset, final DNSKEYRecord 
dnskeyRecord, final PrivateKey privateKey, final Date inception, final Date 
expiration) {
+               try {
+                       return DNSSEC.sign(rrset, dnskeyRecord, privateKey, 
inception, expiration);
+               } catch (DNSSEC.DNSSECException e) {
+                       final String message = String.format("Failed to sign 
Resource Record Set for %s %d %d %d : %s",
+                                       dnskeyRecord.getName(), 
dnskeyRecord.getDClass(), dnskeyRecord.getType(), dnskeyRecord.getTTL(), 
e.getMessage());
+                       LOGGER.error(message, e);
+                       return null;
+               }
+       }
+
+       private boolean isSignatureAlmostExpired(final Date inception, final 
Date expiration, final Date now) {
+               // now is over halfway through validity period
+               return now.getTime() > inception.getTime() + 
((expiration.getTime() - inception.getTime())/2);
+       }
+
+       private RRset signRRset(final RRset rrSet, final List<DnsSecKeyPair> 
kskPairs, final List<DnsSecKeyPair> zskPairs,
+                                                       final Date inception, 
final Date expiration,
+                                                       final 
ConcurrentMap<RRSIGCacheKey, ConcurrentMap<RRsetKey, RRSIGRecord>> RRSIGCache) {
                final List<RRSIGRecord> signatures = new ArrayList<>();
                final List<DnsSecKeyPair> pairs = rrSet.getType() == 
Type.DNSKEY ? kskPairs : zskPairs;
+               final Date now = new Date();
 
                pairs.forEach(pair -> {
                        final DNSKEYRecord dnskeyRecord = 
pair.getDNSKEYRecord();
                        final PrivateKey privateKey = pair.getPrivate();
+                       RRSIGRecord signature = null;
                        try {
-                               signatures.add(DNSSEC.sign(rrSet, dnskeyRecord, 
privateKey, inception, expiration));
+                               if (RRSIGCache == null) {
+                                       signature = sign(rrSet, dnskeyRecord, 
privateKey, inception, expiration);
+                               } else {
+                                       final ConcurrentMap<RRsetKey, 
RRSIGRecord> sigMap = RRSIGCache.computeIfAbsent(new 
RRSIGCacheKey(privateKey.getEncoded(), dnskeyRecord.getAlgorithm()), 
rrsigCacheKey -> new ConcurrentHashMap<>());
+                                       signature = sigMap.computeIfAbsent(new 
RRsetKey(rrSet), k -> sign(rrSet, dnskeyRecord, privateKey, inception, 
expiration));
+
+                                       if (signature != null && 
isSignatureAlmostExpired(signature.getTimeSigned(), signature.getExpire(), 
now)) {
+                                               signature = sigMap.compute(new 
RRsetKey(rrSet), (k, v) -> sign(rrSet, dnskeyRecord, privateKey, inception, 
expiration));
+                                       }
+                               }
                        } catch (Exception e) {
                                final String message = String.format("Failed to 
sign Resource Record Set for %s %d %d %d : %s",
                                        dnskeyRecord.getName(), 
dnskeyRecord.getDClass(), dnskeyRecord.getType(), dnskeyRecord.getTTL(), 
e.getMessage());
 
                                LOGGER.error(message, e);
                        }
+                       if (signature != null) {
+                               signatures.add(signature);
+                       }
                });
 
                final RRset signedRRset = new RRset();
@@ -124,9 +158,9 @@ public class ZoneSignerImpl implements ZoneSigner {
 
 
        @Override
-       public List<Record> signZone(final Name name, final List<Record> 
records, final List<DnsSecKeyPair> kskPairs, final List<DnsSecKeyPair> zskPairs,
-               final Date inception, final Date expiration, final boolean 
fullySignKeySet, final int digestId) throws IOException, 
GeneralSecurityException {
-               LOGGER.info("Signing records, name for first record is " + 
records.get(0).getName());
+       public List<Record> signZone(final List<Record> records, final 
List<DnsSecKeyPair> kskPairs,
+                                                                final 
List<DnsSecKeyPair> zskPairs, final Date inception, final Date expiration,
+                                                                final 
ConcurrentMap<RRSIGCacheKey, ConcurrentMap<RRsetKey, RRSIGRecord>> RRSIGCache) 
throws IOException, GeneralSecurityException {
 
                final List<NSECRecord> nsecRecords = createNsecRecords(records);
                records.addAll(nsecRecords);
@@ -158,7 +192,7 @@ public class ZoneSignerImpl implements ZoneSigner {
                final List<RRset> rrSets = new RRSetsBuilder().build(records);
 
                final List<RRset> signedRrSets = rrSets.stream()
-                       .map(rRset -> signRRset(rRset, kskPairs, zskPairs, 
inception, expiration))
+                       .map(rRset -> signRRset(rRset, kskPairs, zskPairs, 
inception, expiration, RRSIGCache))
                        .sorted((rRset1, rRset2) -> 
rRset1.getName().compareTo(rRset2.getName()))
                        .collect(toList());
 
diff --git 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/router/TrafficRouter.java
 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/router/TrafficRouter.java
index 7a45d1f..592272a 100644
--- 
a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/router/TrafficRouter.java
+++ 
b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/router/TrafficRouter.java
@@ -104,6 +104,7 @@ public class TrafficRouter {
        public static final String CLIENT_STEERING_DIVERSITY = 
"client.steering.forced.diversity";
        public static final String DNSSEC_ENABLED = "dnssec.enabled";
        public static final String DNSSEC_ZONE_DIFFING = 
"dnssec.zone.diffing.enabled";
+       public static final String DNSSEC_RRSIG_CACHE_ENABLED = 
"dnssec.rrsig.cache.enabled";
        private static final long DEFAULT_EDGE_NS_TTL = 3600;
        private static final int DEFAULT_EDGE_TR_LIMIT = 4;
 
diff --git 
a/traffic_router/core/src/test/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSignerImplTest.java
 
b/traffic_router/core/src/test/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSignerImplTest.java
new file mode 100644
index 0000000..f5ceb46
--- /dev/null
+++ 
b/traffic_router/core/src/test/java/com/comcast/cdn/traffic_control/traffic_router/core/dns/ZoneSignerImplTest.java
@@ -0,0 +1,160 @@
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.comcast.cdn.traffic_control.traffic_router.core.dns;
+
+import java.net.InetAddress;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.xbill.DNS.ARecord;
+import org.xbill.DNS.DClass;
+import org.xbill.DNS.DNSKEYRecord;
+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;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(ZoneSignerImpl.class)
+public class ZoneSignerImplTest {
+
+    static class IsRRsetTypeA extends ArgumentMatcher<RRset> {
+        public boolean matches(Object rrset) {
+            return ((RRset) rrset).getType() == Type.A;
+        }
+    }
+
+    static class IsRRsetTypeNSEC extends ArgumentMatcher<RRset> {
+        public boolean matches(Object rrset) {
+            return ((RRset) rrset).getType() == Type.NSEC;
+        }
+    }
+
+    @Test
+    public void signZoneWithRRSIGCacheTest() throws Exception {
+        ZoneSignerImpl zoneSigner = PowerMockito.spy(new ZoneSignerImpl());
+        List<Record> records = new ArrayList<>();
+        Record ARecord1 = new ARecord(new Name("foo.example.com."), DClass.IN, 
60, InetAddress.getByName("1.2.3.4"));
+        Record ARecord2 = new ARecord(new Name("foo.example.com."), DClass.IN, 
60, InetAddress.getByName("1.2.3.5"));
+        Record ARecord3 = new ARecord(new Name("foo.example.com."), DClass.IN, 
60, InetAddress.getByName("1.2.3.6"));
+        Record ARecord4 = new ARecord(new Name("foo.example.com."), DClass.IN, 
60, InetAddress.getByName("1.2.3.7"));
+        DnsSecKeyPair zskPair = mock(DnsSecKeyPairImpl.class);
+        DNSKEYRecord zskDnskey = mock(DNSKEYRecord.class);
+        when(zskPair.getDNSKEYRecord()).thenReturn(zskDnskey);
+        PrivateKey zskKey = mock(PrivateKey.class);
+        when(zskPair.getPrivate()).thenReturn(zskKey);
+        when(zskKey.getEncoded()).thenReturn(new byte[] {1});
+        when(zskDnskey.getAlgorithm()).thenReturn(1);
+        List<DnsSecKeyPair> kskPairs = new ArrayList<>();
+        List<DnsSecKeyPair> zskPairs = Collections.singletonList(zskPair);
+
+        Date inception = new Date();
+        Date expire = Date.from(inception.toInstant().plusSeconds(100000));
+        RRSIGRecord aRRSigRecord = new RRSIGRecord(new 
Name("foo.example.com."), DClass.IN, 60, Type.A, 1, 60, inception, expire, 1, 
new Name("example.com."), new byte[]{1});
+        RRSIGRecord nsecRRSigRecord = new RRSIGRecord(new 
Name("foo.example.com."), DClass.IN, 60, Type.NSEC, 1, 60, inception, expire, 
1, new Name("example.com."), new byte[]{2});
+        PowerMockito.doReturn(aRRSigRecord).when(zoneSigner, "sign", 
argThat(new IsRRsetTypeA()), any(DNSKEYRecord.class), any(PrivateKey.class), 
eq(inception), eq(expire));
+        PowerMockito.doReturn(nsecRRSigRecord).when(zoneSigner, "sign", 
argThat(new IsRRsetTypeNSEC()), any(DNSKEYRecord.class), any(PrivateKey.class), 
eq(inception), eq(expire));
+
+        Date newInception = Date.from(inception.toInstant().plusSeconds(100));
+        Date newExpire = 
Date.from(newInception.toInstant().plusSeconds(100000));
+        RRSIGRecord newARRSigRecord = new RRSIGRecord(new 
Name("foo.example.com."), DClass.IN, 60, Type.A, 1, 60, newInception, 
newExpire, 1, new Name("example.com."), new byte[]{3});
+        RRSIGRecord newNSECRRSigRecord = new RRSIGRecord(new 
Name("foo.example.com."), DClass.IN, 60, Type.NSEC, 1, 60, newInception, 
newExpire, 1, new Name("example.com."), new byte[]{4});
+        PowerMockito.doReturn(newARRSigRecord).when(zoneSigner, "sign", 
argThat(new IsRRsetTypeA()), any(DNSKEYRecord.class), any(PrivateKey.class), 
eq(newInception), eq(newExpire));
+        PowerMockito.doReturn(newNSECRRSigRecord).when(zoneSigner, "sign", 
argThat(new IsRRsetTypeNSEC()), any(DNSKEYRecord.class), any(PrivateKey.class), 
eq(newInception), eq(newExpire));
+
+        Date expiresSoonInception = 
Date.from(inception.toInstant().minusSeconds(100));
+        Date expiresSoonExpire = 
Date.from(inception.toInstant().plusSeconds(50));
+        RRSIGRecord expiresSoonARRSigRecord = new RRSIGRecord(new 
Name("foo.example.com."), DClass.IN, 60, Type.A, 1, 60, expiresSoonInception, 
expiresSoonExpire, 1, new Name("example.com."), new byte[]{5});
+        RRSIGRecord expiresSoonNSECRRSigRecord = new RRSIGRecord(new 
Name("foo.example.com."), DClass.IN, 60, Type.NSEC, 1, 60, 
expiresSoonInception, expiresSoonExpire, 1, new Name("example.com."), new 
byte[]{6});
+        PowerMockito.doReturn(expiresSoonARRSigRecord).when(zoneSigner, 
"sign", argThat(new IsRRsetTypeA()), any(DNSKEYRecord.class), 
any(PrivateKey.class), eq(expiresSoonInception), eq(expiresSoonExpire));
+        PowerMockito.doReturn(expiresSoonNSECRRSigRecord).when(zoneSigner, 
"sign", argThat(new IsRRsetTypeNSEC()), any(DNSKEYRecord.class), 
any(PrivateKey.class), eq(expiresSoonInception), eq(expiresSoonExpire));
+
+        ConcurrentMap<RRSIGCacheKey, ConcurrentMap<RRsetKey, RRSIGRecord>> 
RRSIGCache = new ConcurrentHashMap<>();
+
+        records.add(ARecord1);
+        records.add(ARecord2);
+        List<Record> signedRecords = zoneSigner.signZone(records, kskPairs, 
zskPairs, inception, expire, RRSIGCache);
+        RRSIGRecord ret = (RRSIGRecord) signedRecords.stream().filter(r -> r 
instanceof RRSIGRecord && ((RRSIGRecord) r).getTypeCovered() == 
Type.A).findFirst().orElse(null);
+        assertThat(ret, notNullValue());
+        assertThat(ret, equalTo(aRRSigRecord));
+
+        // re-signing the same RRset with new timestamps should reuse the 
cached RRSIG record
+        records.clear();
+        records.add(ARecord1);
+        records.add(ARecord2);
+        signedRecords = zoneSigner.signZone(records, kskPairs, zskPairs, 
newInception, newExpire, RRSIGCache);
+        ret = (RRSIGRecord) signedRecords.stream().filter(r -> r instanceof 
RRSIGRecord && ((RRSIGRecord) r).getTypeCovered() == 
Type.A).findFirst().orElse(null);
+        assertThat(ret, notNullValue());
+        assertThat(ret, equalTo(aRRSigRecord));
+
+        // changed RRset should be re-signed
+        records.clear();
+        records.add(ARecord1);
+        records.add(ARecord2);
+        records.add(ARecord3);
+        records.add(ARecord4);
+        signedRecords = zoneSigner.signZone(records, kskPairs, zskPairs, 
newInception, newExpire, RRSIGCache);
+        ret = (RRSIGRecord) signedRecords.stream().filter(r -> r instanceof 
RRSIGRecord && ((RRSIGRecord) r).getTypeCovered() == 
Type.A).findFirst().orElse(null);
+        assertThat(ret, notNullValue());
+        assertThat(ret, equalTo(newARRSigRecord));
+
+        // re-signing 1st RRset again should reuse the cached RRSIG record
+        records.clear();
+        records.add(ARecord1);
+        records.add(ARecord2);
+        signedRecords = zoneSigner.signZone(records, kskPairs, zskPairs, 
newInception, newExpire, RRSIGCache);
+        ret = (RRSIGRecord) signedRecords.stream().filter(r -> r instanceof 
RRSIGRecord && ((RRSIGRecord) r).getTypeCovered() == 
Type.A).findFirst().orElse(null);
+        assertThat(ret, notNullValue());
+        assertThat(ret, equalTo(aRRSigRecord));
+
+        // re-signing RRset that has a cached RRSIG record that is close to 
expiring should be re-signed
+        records.clear();
+        records.add(ARecord3);
+        records.add(ARecord4);
+        signedRecords = zoneSigner.signZone(records, kskPairs, zskPairs, 
expiresSoonInception, expiresSoonExpire, RRSIGCache);
+        ret = (RRSIGRecord) signedRecords.stream().filter(r -> r instanceof 
RRSIGRecord && ((RRSIGRecord) r).getTypeCovered() == 
Type.A).findFirst().orElse(null);
+        assertThat(ret, notNullValue());
+        assertThat(ret, equalTo(expiresSoonARRSigRecord));
+        records.clear();
+        records.add(ARecord3);
+        records.add(ARecord4);
+        signedRecords = zoneSigner.signZone(records, kskPairs, zskPairs, 
newInception, newExpire, RRSIGCache);
+        ret = (RRSIGRecord) signedRecords.stream().filter(r -> r instanceof 
RRSIGRecord && ((RRSIGRecord) r).getTypeCovered() == 
Type.A).findFirst().orElse(null);
+        assertThat(ret, notNullValue());
+        assertThat(ret, equalTo(newARRSigRecord));
+    }
+}

Reply via email to