You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by mi...@apache.org on 2018/07/10 13:35:35 UTC

[trafficcontrol] branch master updated: Add TO Go cdns/dnsseckeys/generate

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

mitchell852 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 057fa9e  Add TO Go cdns/dnsseckeys/generate
057fa9e is described below

commit 057fa9ef93682f001edb1aba88ff8dc69623b41a
Author: Robert Butts <ro...@apache.org>
AuthorDate: Thu Jun 7 15:14:27 2018 -0600

    Add TO Go cdns/dnsseckeys/generate
---
 lib/go-tc/deliveryservice_ssl_keys.go              |  38 +++++
 lib/go-util/ptr.go                                 |   4 +
 traffic_ops/traffic_ops_golang/cdn/dnssec.go       | 179 +++++++++++++++++++++
 .../deliveryservice/deliveryservicesv13.go         |  28 ++--
 .../traffic_ops_golang/deliveryservice/dnssec.go   |  92 +++++------
 traffic_ops/traffic_ops_golang/routes.go           |   1 +
 6 files changed, 282 insertions(+), 60 deletions(-)

diff --git a/lib/go-tc/deliveryservice_ssl_keys.go b/lib/go-tc/deliveryservice_ssl_keys.go
index 1eddcca..b0258d1 100644
--- a/lib/go-tc/deliveryservice_ssl_keys.go
+++ b/lib/go-tc/deliveryservice_ssl_keys.go
@@ -16,9 +16,12 @@ package tc
 */
 
 import (
+	"database/sql"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"strconv"
+	"strings"
 )
 
 // DeliveryServiceSSLKeysResponse ...
@@ -115,3 +118,38 @@ type DNSSECKeyDSRecord struct {
 	DigestType int64  `json:"digestType,string"`
 	Digest     string `json:"digest"`
 }
+
+type CDNDNSSECGenerateReq struct {
+	// Key is the CDN name, as documented in the API documentation.
+	Key *string `json:"key"`
+	// Name is the CDN domain, as documented in the API documentation.
+	Name              *string `json:"name"`
+	TTL               *uint64 `json:"ttl,string"`
+	KSKExpirationDays *uint64 `json:"kskExpirationDays,string"`
+	ZSKExpirationDays *uint64 `json:"zskExpirationDays,string"`
+	EffectiveDateUnix *int64  `json:"effectiveDate"`
+}
+
+func (r CDNDNSSECGenerateReq) Validate(tx *sql.Tx) error {
+	errs := []string{}
+	if r.Key == nil {
+		errs = append(errs, "key (cdn name) must be set")
+	}
+	if r.Name == nil {
+		errs = append(errs, "name (cdn domain name) must be set")
+	}
+	if r.TTL == nil {
+		errs = append(errs, "ttl must be set")
+	}
+	if r.KSKExpirationDays == nil {
+		errs = append(errs, "kskExpirationDays must be set")
+	}
+	if r.ZSKExpirationDays == nil {
+		errs = append(errs, "zskExpirationDays must be set")
+	}
+	// effective date is optional
+	if len(errs) > 0 {
+		return errors.New("missing fields: " + strings.Join(errs, "; "))
+	}
+	return nil
+}
diff --git a/lib/go-util/ptr.go b/lib/go-util/ptr.go
index c672421..3bc227f 100644
--- a/lib/go-util/ptr.go
+++ b/lib/go-util/ptr.go
@@ -27,6 +27,10 @@ func IntPtr(i int) *int {
 	return &i
 }
 
+func Int64Ptr(i int64) *int64 {
+	return &i
+}
+
 func BoolPtr(b bool) *bool {
 	return &b
 }
diff --git a/traffic_ops/traffic_ops_golang/cdn/dnssec.go b/traffic_ops/traffic_ops_golang/cdn/dnssec.go
new file mode 100644
index 0000000..0f2b385
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cdn/dnssec.go
@@ -0,0 +1,179 @@
+package cdn
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"database/sql"
+	"errors"
+	"net/http"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
+)
+
+func CreateDNSSECKeys(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+	req := tc.CDNDNSSECGenerateReq{}
+	if err := api.Parse(r.Body, inf.Tx.Tx, &req); err != nil {
+		api.HandleErr(w, r, http.StatusBadRequest, errors.New("parsing request: "+err.Error()), nil)
+		return
+	}
+	if req.EffectiveDateUnix == nil {
+		req.EffectiveDateUnix = util.Int64Ptr(time.Now().Unix())
+	}
+	cdnName := *req.Key
+	if err := generateStoreDNSSECKeys(inf.Tx.Tx, inf.Config, cdnName, *req.TTL, *req.KSKExpirationDays, *req.ZSKExpirationDays, *req.EffectiveDateUnix); err != nil {
+		api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("generating and storing DNSSEC CDN keys: "+err.Error()))
+		return
+	}
+	*inf.CommitTx = true
+	api.WriteResp(w, r, "Successfully created dnssec keys for "+cdnName)
+}
+
+const DNSSECStatusExisting = "existing"
+
+func generateStoreDNSSECKeys(
+	tx *sql.Tx,
+	cfg *config.Config,
+	cdnName string,
+	ttlSeconds uint64,
+	kExpDays uint64,
+	zExpDays uint64,
+	effectiveDateUnix int64,
+) error {
+
+	zExp := time.Duration(zExpDays) * time.Hour * 24
+	kExp := time.Duration(kExpDays) * time.Hour * 24
+	ttl := time.Duration(ttlSeconds) * time.Second
+
+	newKeys := tc.DNSSECKeys{}
+	// (tc.DNSSECKeys, bool, error) {
+	oldKeys, oldKeysExist, err := riaksvc.GetDNSSECKeys(cdnName, tx, cfg.RiakAuthOptions)
+	if err != nil {
+		return errors.New("getting old dnssec keys: " + err.Error())
+	}
+	if oldKeysExist {
+		oldKeyCDN, oldKeyCDNExists := oldKeys[cdnName]
+		if oldKeyCDNExists && len(oldKeyCDN.KSK) > 0 {
+			ksk := oldKeyCDN.KSK[0]
+			ksk.Status = DNSSECStatusExisting
+			ksk.TTLSeconds = uint64(ttl / time.Second)
+			ksk.ExpirationDateUnix = effectiveDateUnix
+			oldKeyCDN.KSK = append(oldKeyCDN.KSK, ksk)
+		}
+		if oldKeyCDNExists && len(oldKeyCDN.ZSK) > 0 {
+			zsk := oldKeyCDN.ZSK[0]
+			zsk.Status = DNSSECStatusExisting
+			zsk.TTLSeconds = uint64(ttl / time.Second)
+			zsk.ExpirationDateUnix = effectiveDateUnix
+			oldKeyCDN.ZSK = append(oldKeyCDN.ZSK, zsk)
+		}
+		newKeys[cdnName] = tc.DNSSECKeySet{oldKeyCDN.ZSK, oldKeyCDN.KSK}
+	} else {
+		// TODO create CDN keys if they don't exist?
+		return errors.New("getting DNSSec keys from Riak: no DNSSec keys for CDN")
+	}
+	cdnKeys := newKeys[cdnName]
+	dses, cdnDomain, err := getCDNDeliveryServices(tx, cdnName)
+	if err != nil {
+		return errors.New("getting cdn delivery services: " + err.Error())
+	}
+	dsNames := []string{}
+	for _, ds := range dses {
+		dsNames = append(dsNames, ds.Name)
+	}
+	matchLists, err := deliveryservice.GetDeliveryServicesMatchLists(dsNames, tx)
+	if err != nil {
+		return errors.New("getting delivery service matchlists: " + err.Error())
+	}
+	for _, ds := range dses {
+		if !ds.Type.IsHTTP() && !ds.Type.IsDNS() {
+			continue // skip delivery services that aren't DNS or HTTP (e.g. ANY_MAP)
+		}
+
+		matchlist, ok := matchLists[ds.Name]
+		if !ok {
+			return errors.New("no regex match list found for delivery service '" + ds.Name)
+		}
+
+		exampleURLs := deliveryservice.MakeExampleURLs(ds.Protocol, ds.Type, ds.RoutingName, matchlist, cdnDomain)
+		log.Infoln("Creating keys for " + ds.Name)
+		overrideTTL := true
+		dsKeys, err := deliveryservice.CreateDNSSECKeys(tx, cfg, ds.Name, exampleURLs, cdnKeys, kExp, zExp, ttl, overrideTTL)
+		if err != nil {
+			return errors.New("creating delivery service DNSSEC keys: " + err.Error())
+		}
+		newKeys[ds.Name] = dsKeys
+	}
+	if err := riaksvc.PutDNSSECKeys(newKeys, cdnName, tx, cfg.RiakAuthOptions); err != nil {
+		return errors.New("putting Riak DNSSEC CDN keys: " + err.Error())
+	}
+	return nil
+}
+
+type CDNDS struct {
+	Name        string
+	Protocol    *int
+	Type        tc.DSType
+	RoutingName string
+}
+
+// getCDNDeliveryServices returns basic data for the delivery services on the given CDN, as well as the CDN name, or any error.
+func getCDNDeliveryServices(tx *sql.Tx, cdn string) ([]CDNDS, string, error) {
+	q := `
+SELECT ds.xml_id, ds.protocol, t.name as type, ds.routing_name, cdn.domain_name as cdn_domain
+FROM deliveryservice as ds
+JOIN cdn ON ds.cdn_id = cdn.id
+JOIN type as t ON ds.type = t.id
+WHERE cdn.name = $1
+`
+	rows, err := tx.Query(q, cdn)
+	if err != nil {
+		return nil, "", errors.New("getting cdn delivery services: " + err.Error())
+	}
+	defer rows.Close()
+	cdnDomain := ""
+	dses := []CDNDS{}
+	for rows.Next() {
+		ds := CDNDS{}
+		dsTypeStr := ""
+		if err := rows.Scan(&ds.Name, &ds.Protocol, dsTypeStr, &ds.RoutingName, &cdnDomain); err != nil {
+			return nil, "", errors.New("scanning cdn delivery services: " + err.Error())
+		}
+		dsType := tc.DSTypeFromString(dsTypeStr)
+		if dsType == tc.DSTypeInvalid {
+			return nil, "", errors.New("got invalid delivery service type '" + dsTypeStr + "'")
+		}
+		ds.Type = dsType
+		dses = append(dses, ds)
+	}
+	return dses, cdnDomain, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
index f48e948..c91b802 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
@@ -155,6 +155,7 @@ func create(tx *sql.Tx, cfg config.Config, user *auth.CurrentUser, ds tc.Deliver
 		}
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("inserting ds: " + err.Error())
 	}
+	defer resultRows.Close()
 
 	id := 0
 	lastUpdated := tc.TimeNoMod{}
@@ -188,7 +189,7 @@ func create(tx *sql.Tx, cfg config.Config, user *auth.CurrentUser, ds tc.Deliver
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating default regex: " + err.Error())
 	}
 
-	matchlists, err := readGetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
+	matchlists, err := GetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
 	if err != nil {
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DS: reading matchlists: " + err.Error())
 	}
@@ -203,7 +204,7 @@ func create(tx *sql.Tx, cfg config.Config, user *auth.CurrentUser, ds tc.Deliver
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DS: getting CDN info: " + err.Error())
 	}
 
-	ds.ExampleURLs = makeExampleURLs(ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, cdnDomain)
+	ds.ExampleURLs = MakeExampleURLs(ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, cdnDomain)
 
 	if err := ensureHeaderRewriteParams(tx, *ds.ID, *ds.XMLID, ds.EdgeHeaderRewrite, edgeTier, *ds.Type); err != nil {
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating edge header rewrite parameters: " + err.Error())
@@ -217,8 +218,10 @@ func create(tx *sql.Tx, cfg config.Config, user *auth.CurrentUser, ds tc.Deliver
 	if err := ensureCacheURLParams(tx, *ds.ID, *ds.XMLID, ds.CacheURL); err != nil {
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating cache url parameters: " + err.Error())
 	}
-	if err := createDNSSecKeys(tx, cfg, *ds.ID, *ds.XMLID, cdnName, cdnDomain, dnssecEnabled, ds.ExampleURLs); err != nil {
-		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys: " + err.Error())
+	if dnssecEnabled {
+		if err := PutDNSSecKeys(tx, &cfg, *ds.XMLID, cdnName, ds.ExampleURLs); err != nil {
+			return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys: " + err.Error())
+		}
 	}
 
 	if err := createPrimaryOrigin(tx, user, ds); err != nil {
@@ -343,7 +346,7 @@ WHERE ds.id=$1
 	if dsType == tc.DSTypeInvalid {
 		return "", errors.New("getting delivery services matchlist: got invalid delivery service type '" + dsTypeStr + "'")
 	}
-	matchLists, err := readGetDeliveryServicesMatchLists([]string{xmlID}, tx)
+	matchLists, err := GetDeliveryServicesMatchLists([]string{xmlID}, tx)
 	if err != nil {
 		return "", errors.New("getting delivery services matchlist: " + err.Error())
 	}
@@ -469,6 +472,7 @@ func update(tx *sql.Tx, cfg config.Config, user *auth.CurrentUser, ds *tc.Delive
 		}
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("query updating delivery service: " + err.Error())
 	}
+	resultRows.Close()
 	if !resultRows.Next() {
 		return tc.DeliveryServiceNullableV13{}, http.StatusNotFound, errors.New("no delivery service found with this id"), nil
 	}
@@ -516,7 +520,7 @@ func update(tx *sql.Tx, cfg config.Config, user *auth.CurrentUser, ds *tc.Delive
 		}
 	}
 
-	matchLists, err := readGetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
+	matchLists, err := GetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
 	if err != nil {
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("getting matchlists after update: " + err.Error())
 	}
@@ -663,7 +667,7 @@ func readGetDeliveryServices(params map[string]string, tx *sqlx.Tx, user *auth.C
 		dsNames[i] = *ds.XMLID
 	}
 
-	matchLists, err := readGetDeliveryServicesMatchLists(dsNames, tx.Tx)
+	matchLists, err := GetDeliveryServicesMatchLists(dsNames, tx.Tx)
 	if err != nil {
 		return nil, []error{errors.New("getting delivery service matchlists: " + err.Error())}, tc.SystemError
 	}
@@ -673,7 +677,7 @@ func readGetDeliveryServices(params map[string]string, tx *sqlx.Tx, user *auth.C
 			continue
 		}
 		ds.MatchList = &matchList
-		ds.ExampleURLs = makeExampleURLs(ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, dsCDNDomains[*ds.XMLID])
+		ds.ExampleURLs = MakeExampleURLs(ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, dsCDNDomains[*ds.XMLID])
 		dses[i] = ds
 	}
 
@@ -701,7 +705,7 @@ func updateSSLKeys(ds *tc.DeliveryServiceNullableV13, hostName string, tx *sql.T
 
 // getHostName gets the host name used for delivery service requests. The dsProtocol may be nil, if the delivery service type doesn't have a protocol (e.g. ANY_MAP).
 func getHostName(dsProtocol *int, dsType tc.DSType, dsRoutingName string, dsMatchList []tc.DeliveryServiceMatch, cdnDomain string) (string, error) {
-	exampleURLs := makeExampleURLs(dsProtocol, dsType, dsRoutingName, dsMatchList, cdnDomain)
+	exampleURLs := MakeExampleURLs(dsProtocol, dsType, dsRoutingName, dsMatchList, cdnDomain)
 
 	exampleURL := ""
 	if dsProtocol != nil && *dsProtocol == 2 {
@@ -748,7 +752,7 @@ func getCDNNameDomainDNSSecEnabled(dsID int, tx *sql.Tx) (string, string, bool,
 }
 
 // makeExampleURLs creates the example URLs for a delivery service. The dsProtocol may be nil, if the delivery service type doesn't have a protocol (e.g. ANY_MAP).
-func makeExampleURLs(protocol *int, dsType tc.DSType, routingName string, matchList []tc.DeliveryServiceMatch, cdnDomain string) []string {
+func MakeExampleURLs(protocol *int, dsType tc.DSType, routingName string, matchList []tc.DeliveryServiceMatch, cdnDomain string) []string {
 	examples := []string{}
 	scheme := ""
 	scheme2 := ""
@@ -792,7 +796,8 @@ func makeExampleURLs(protocol *int, dsType tc.DSType, routingName string, matchL
 	return examples
 }
 
-func readGetDeliveryServicesMatchLists(dses []string, tx *sql.Tx) (map[string][]tc.DeliveryServiceMatch, error) {
+func GetDeliveryServicesMatchLists(dses []string, tx *sql.Tx) (map[string][]tc.DeliveryServiceMatch, error) {
+	// TODO move somewhere generic
 	q := `
 SELECT ds.xml_id as ds_name, t.name as type, r.pattern, COALESCE(dsr.set_number, 0)
 FROM regex as r
@@ -805,6 +810,7 @@ WHERE ds.xml_id = ANY($1)
 	if err != nil {
 		return nil, errors.New("getting delivery service regexes: " + err.Error())
 	}
+	defer rows.Close()
 
 	matches := map[string][]tc.DeliveryServiceMatch{}
 	for rows.Next() {
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go b/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
index b2a9074..2c1360c 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
@@ -26,7 +26,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
@@ -34,75 +33,70 @@ import (
 	"github.com/miekg/dns"
 )
 
-func createDNSSecKeys(tx *sql.Tx, cfg config.Config, dsID int, xmlID string, cdnName string, cdnDomain string, dnssecEnabled bool, exampleURLs []string) error {
-	if !dnssecEnabled {
-		return nil
-	}
-
+func PutDNSSecKeys(tx *sql.Tx, cfg *config.Config, xmlID string, cdnName string, exampleURLs []string) error {
 	keys, ok, err := riaksvc.GetDNSSECKeys(cdnName, tx, cfg.RiakAuthOptions)
 	if err != nil {
-		log.Errorln("Getting DNSSec keys from Riak: " + err.Error())
 		return errors.New("getting DNSSec keys from Riak: " + err.Error())
-	}
-	if !ok {
-		log.Errorln("Getting DNSSec keys from Riak: no DNSSec keys found")
+	} else if !ok {
 		return errors.New("getting DNSSec keys from Riak: no DNSSec keys found")
 	}
-
 	cdnKeys, ok := keys[cdnName]
 	// TODO warn and continue?
 	if !ok {
-		log.Errorln("Getting DNSSec keys from Riak: no DNSSec keys for CDN '" + cdnName + "'")
 		return errors.New("getting DNSSec keys from Riak: no DNSSec keys for CDN")
 	}
+	kExp := getKeyExpiration(cdnKeys.KSK, dnssecDefaultKSKExpiration)
+	zExp := getKeyExpiration(cdnKeys.ZSK, dnssecDefaultZSKExpiration)
+	overrideTTL := false
+	dsKeys, err := CreateDNSSECKeys(tx, cfg, xmlID, exampleURLs, cdnKeys, kExp, zExp, dnssecDefaultTTL, overrideTTL)
+	if err != nil {
+		return errors.New("creating DNSSEC keys for delivery service '" + xmlID + "': " + err.Error())
+	}
+	keys[xmlID] = dsKeys
+	if err := riaksvc.PutDNSSECKeys(keys, cdnName, tx, cfg.RiakAuthOptions); err != nil {
+		return errors.New("putting Riak DNSSEC keys: " + err.Error())
+	}
+	return nil
+}
+
+// CreateDNSSECKeys creates DNSSEC keys for the given delivery service, updating existing keys if they exist. The overrideTTL parameter determines whether to reuse existing key TTLs if they exist, or to override existing TTLs with the ttl parameter's value.
+func CreateDNSSECKeys(tx *sql.Tx, cfg *config.Config, xmlID string, exampleURLs []string, cdnKeys tc.DNSSECKeySet, kskExpiration time.Duration, zskExpiration time.Duration, ttl time.Duration, overrideTTL bool) (tc.DNSSECKeySet, error) {
 	if len(cdnKeys.ZSK) == 0 {
-		log.Errorln("Getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN '" + cdnName + "'")
-		return errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
+		return tc.DNSSECKeySet{}, errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
 	}
 	if len(cdnKeys.KSK) == 0 {
-		log.Errorln("Getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN '" + cdnName + "'")
-		return errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
+		return tc.DNSSECKeySet{}, errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
 	}
-
-	kExpDays := getKeyExpirationDays(cdnKeys.KSK, dnssecDefaultKSKExpirationDays)
-	zExpDays := getKeyExpirationDays(cdnKeys.ZSK, dnssecDefaultZSKExpirationDays)
-	ttl := getKeyTTL(cdnKeys.KSK, dnssecDefaultTTL)
-	dsName, err := getDSDomainName(exampleURLs)
+	if !overrideTTL {
+		ttl = getKeyTTL(cdnKeys.KSK, ttl)
+	}
+	dsName, err := GetDSDomainName(exampleURLs)
 	if err != nil {
-		log.Errorln("creating DS domain name: " + err.Error())
-		return errors.New("creating DS domain name: " + err.Error())
+		return tc.DNSSECKeySet{}, errors.New("creating DS domain name: " + err.Error())
 	}
 	inception := time.Now()
-	zExpiration := inception.Add(time.Duration(zExpDays) * time.Hour * 24)
-	kExpiration := inception.Add(time.Duration(kExpDays) * time.Hour * 24)
+	zExpiration := inception.Add(zskExpiration)
+	kExpiration := inception.Add(kskExpiration)
 
 	tld := false
 	effectiveDate := inception
 	zsk, err := getDNSSECKeys(dnssecZSKType, dsName, ttl, inception, zExpiration, dnssecKeyStatusNew, effectiveDate, tld)
 	if err != nil {
-		log.Errorln("getting DNSSEC keys for ZSK: " + err.Error())
-		return errors.New("getting DNSSEC keys for ZSK: " + err.Error())
+		return tc.DNSSECKeySet{}, errors.New("getting DNSSEC keys for ZSK: " + err.Error())
 	}
 	ksk, err := getDNSSECKeys(dnssecKSKType, dsName, ttl, inception, kExpiration, dnssecKeyStatusNew, effectiveDate, tld)
 	if err != nil {
-		log.Errorln("getting DNSSEC keys for KSK: " + err.Error())
-		return errors.New("getting DNSSEC keys for KSK: " + err.Error())
+		return tc.DNSSECKeySet{}, errors.New("getting DNSSEC keys for KSK: " + err.Error())
 	}
-	keys[xmlID] = tc.DNSSECKeySet{ZSK: []tc.DNSSECKey{zsk}, KSK: []tc.DNSSECKey{ksk}}
-
-	if err := riaksvc.PutDNSSECKeys(keys, cdnName, tx, cfg.RiakAuthOptions); err != nil {
-		log.Errorln("putting Riak DNSSEC keys: " + err.Error())
-		return errors.New("putting Riak DNSSEC keys: " + err.Error())
-	}
-	return nil
+	return tc.DNSSECKeySet{ZSK: []tc.DNSSECKey{zsk}, KSK: []tc.DNSSECKey{ksk}}, nil
 }
 
-func getDNSSECKeys(keyType string, dsName string, ttl uint64, inception time.Time, expiration time.Time, status string, effectiveDate time.Time, tld bool) (tc.DNSSECKey, error) {
+func getDNSSECKeys(keyType string, dsName string, ttl time.Duration, inception time.Time, expiration time.Time, status string, effectiveDate time.Time, tld bool) (tc.DNSSECKey, error) {
 	key := tc.DNSSECKey{
 		InceptionDateUnix:  inception.Unix(),
 		ExpirationDateUnix: expiration.Unix(),
 		Name:               dsName + ".",
-		TTLSeconds:         ttl,
+		TTLSeconds:         uint64(ttl / time.Second),
 		Status:             status,
 		EffectiveDateUnix:  effectiveDate.Unix(),
 	}
@@ -114,7 +108,7 @@ func getDNSSECKeys(keyType string, dsName string, ttl uint64, inception time.Tim
 
 // genKeys generates keys for DNSSEC for a delivery service. Returns the public key, private key, and DS record (which will be nil if ksk or tld is false).
 // This emulates the old Perl Traffic Ops behavior: the public key is of the RFC1035 single-line zone file format, base64 encoded; the private key is of the BIND private-key-file format, base64 encoded; the DSRecord contains the algorithm, digest type, and digest.
-func genKeys(dsName string, ksk bool, ttl uint64, tld bool) (string, string, *tc.DNSSECKeyDSRecord, error) {
+func genKeys(dsName string, ksk bool, ttl time.Duration, tld bool) (string, string, *tc.DNSSECKeyDSRecord, error) {
 	bits := 1024
 	flags := 256
 	algorithm := dns.RSASHA1 // 5 - http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
@@ -130,7 +124,7 @@ func genKeys(dsName string, ksk bool, ttl uint64, tld bool) (string, string, *tc
 			Name:   dsName,
 			Rrtype: dns.TypeDNSKEY,
 			Class:  dns.ClassINET,
-			Ttl:    uint32(ttl),
+			Ttl:    uint32(ttl / time.Second),
 		},
 		Flags:     uint16(flags),
 		Protocol:  uint8(protocol),
@@ -162,7 +156,8 @@ func genKeys(dsName string, ksk bool, ttl uint64, tld bool) (string, string, *tc
 const dnssecKSKType = "ksk"
 const dnssecZSKType = "zsk"
 
-func getDSDomainName(dsExampleURLs []string) (string, error) {
+func GetDSDomainName(dsExampleURLs []string) (string, error) {
+	// TODO move somewhere generic
 	if len(dsExampleURLs) == 0 {
 		return "", errors.New("no example URLs")
 	}
@@ -180,27 +175,26 @@ func getDSDomainName(dsExampleURLs []string) (string, error) {
 }
 
 const dnssecKeyStatusNew = "new"
-const secondsPerDay = 86400
-const dnssecDefaultKSKExpirationDays = 365
-const dnssecDefaultZSKExpirationDays = 30
+const dnssecDefaultKSKExpiration = time.Duration(365) * time.Hour * 24
+const dnssecDefaultZSKExpiration = time.Duration(30) * time.Hour * 24
 const dnssecDefaultTTL = 60
 
-func getKeyExpirationDays(keys []tc.DNSSECKey, defaultExpirationDays uint64) uint64 {
+func getKeyExpiration(keys []tc.DNSSECKey, defaultExpiration time.Duration) time.Duration {
 	for _, key := range keys {
 		if key.Status != dnssecKeyStatusNew {
 			continue
 		}
-		return uint64((key.ExpirationDateUnix - key.InceptionDateUnix) / secondsPerDay)
+		return time.Duration(key.ExpirationDateUnix-key.InceptionDateUnix) * time.Second
 	}
-	return defaultExpirationDays
+	return defaultExpiration
 }
 
-func getKeyTTL(keys []tc.DNSSECKey, defaultTTL uint64) uint64 {
+func getKeyTTL(keys []tc.DNSSECKey, defaultTTL time.Duration) time.Duration {
 	for _, key := range keys {
 		if key.Status != dnssecKeyStatusNew {
 			continue
 		}
-		return key.TTLSeconds
+		return time.Duration(key.TTLSeconds) * time.Second
 	}
 	return defaultTTL
 }
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 447245c..bcf0777 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -126,6 +126,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 
 		//CDN: queue updates
 		{1.1, http.MethodPost, `cdns/{id}/queue_update$`, cdn.Queue(d.DB.DB), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodPost, `cdns/dnsseckeys/generate(\.json)?$`, cdn.CreateDNSSECKeys, auth.PrivLevelAdmin, Authenticated, nil},
 
 		//CDN: Monitoring: Traffic Monitor
 		{1.1, http.MethodGet, `cdns/{name}/configs/monitoring(\.json)?$`, monitoringHandler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil},