You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2018/11/26 22:23:53 UTC

[trafficcontrol] branch master updated: Add TO Go GET cdn/name/dnsseckeys, DS record txt

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

rawlin 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 22d56e1  Add TO Go GET cdn/name/dnsseckeys, DS record txt
22d56e1 is described below

commit 22d56e1b1713574883a1358b9d720b594a87cf25
Author: Robert Butts <ro...@apache.org>
AuthorDate: Tue Nov 6 13:41:33 2018 -0700

    Add TO Go GET cdn/name/dnsseckeys, DS record txt
---
 CHANGELOG.md                                       |   3 +
 docs/source/api/v14/cdn.rst                        | 104 ++++++++++++++++++
 lib/go-tc/deliveryservice_ssl_keys.go              |  47 ++++++--
 traffic_ops/traffic_ops_golang/cdn/dnssec.go       |  79 +++++++++++--
 traffic_ops/traffic_ops_golang/cdn/genksk.go       |  16 +--
 .../traffic_ops_golang/deliveryservice/dnssec.go   | 122 ++++++++++++++++++---
 traffic_ops/traffic_ops_golang/riaksvc/dsutil.go   |   6 +-
 traffic_ops/traffic_ops_golang/routes.go           |   2 +
 8 files changed, 333 insertions(+), 46 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 70c9348..78eed22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
   - /api/1.1/deliveryservices/sslkeys/add `POST`
   - /api/1.1/deliveryservices/xmlId/:xmlid/sslkeys/delete `GET`
   - /api/1.4/cdns/dnsseckeys/refresh `GET`
+  - /api/1.1/cdns/name/:name/dnsseckeys `GET`
+  - /api/1.4/cdns/name/:name/dnsseckeys `GET`
 - To support reusing a single riak cluster connection, an optional parameter is added to riak.conf: "HealthCheckInterval". This options takes a 'Duration' value (ie: 10s, 5m) which affects how often the riak cluster is health checked.  Default is currently set to: "HealthCheckInterval": "5s".
 - Added an API 1.4 endpoint, /api/1.4/cdns/dnsseckeys/refresh, to perform necessary behavior previously served outside the API under `/internal`.
+- Adds the DS Record text to the cdn dnsseckeys endpoint in 1.4.
 
 ### Changed
 - Issue 2821: Fixed "Traffic Router may choose wrong certificate when SNI names overlap"
diff --git a/docs/source/api/v14/cdn.rst b/docs/source/api/v14/cdn.rst
index 7884420..5b9b3ca 100644
--- a/docs/source/api/v14/cdn.rst
+++ b/docs/source/api/v14/cdn.rst
@@ -92,3 +92,107 @@ CDN
     }
 
 |
+
+.. _to-api-v12-cdn-dnsseckeys:
+
+DNSSEC Keys
++++++++++++
+
+**GET /api/1.2/cdns/name/:name/dnsseckeys**
+
+  Gets a list of dnsseckeys for a CDN and all associated Delivery Services.
+
+  Authentication Required: Yes
+
+  Role(s) Required: Admin
+
+  **Request Route Parameters**
+
+	  +----------+----------+-------------+
+  |   Name   | Required | Description |
+  +==========+==========+=============+
+  | ``name`` | yes      |             |
+  +----------+----------+-------------+
+
+  **Response Properties**
+
+  +-------------------------------+--------+---------------------------------------------------------------+
+  |           Parameter           |  Type  |                          Description                          |
+  +===============================+========+===============================================================+
+  | ``cdn name/ds xml_id``        | string | identifier for ds or cdn                                      |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``>zsk/ksk``                  | array  | collection of zsk/ksk data                                    |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``>>ttl``                     | string | time-to-live for dnssec requests                              |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``>>inceptionDate``           | string | epoch timestamp for when the keys were created                |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``>>expirationDate``          | string | epoch timestamp representing the expiration of the keys       |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``>>private``                 | string | encoded private key                                           |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``>>public``                  | string | encoded public key                                            |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``>>name``                    | string | domain name                                                   |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``version``                   | string | API version                                                   |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``ksk>>dsRecord>>algorithm``  | string | The algorithm of the referenced DNSKEY-recor.                 |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``ksk>>dsRecord>>digestType`` | string | Cryptographic hash algorithm used to create the Digest value. |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``ksk>>dsRecord>>digest``     | string | A cryptographic hash value of the referenced DNSKEY-record.   |
+  +-------------------------------+--------+---------------------------------------------------------------+
+  | ``ksk>>dsRecord>>text``       | string | The DS Record text, to be inserted in the parent resolver.    |
+  +-------------------------------+--------+---------------------------------------------------------------+
+
+  **Response Example** ::
+
+    {
+      "response": {
+        "cdn1": {
+          "zsk": {
+            "ttl": "60",
+            "inceptionDate": "1426196750",
+            "private": "zsk private key",
+            "public": "zsk public key",
+            "expirationDate": "1428788750",
+            "name": "foo.kabletown.com."
+          },
+          "ksk": {
+            "name": "foo.kabletown.com.",
+            "expirationDate": "1457732750",
+            "public": "ksk public key",
+            "private": "ksk private key",
+            "inceptionDate": "1426196750",
+            "ttl": "60",
+            "dsRecord": {
+              "algorithm": "5",
+              "digestType": "2",
+              "digest": "abc123def456",
+              "text": "foo.kabletown.com.\t30\tIN\tDS\t12345 8 2 DEADBEEF123456789"
+            }
+          }
+        },
+        "ds-01": {
+          "zsk": {
+            "ttl": "60",
+            "inceptionDate": "1426196750",
+            "private": "zsk private key",
+            "public": "zsk public key",
+            "expirationDate": "1428788750",
+            "name": "ds-01.foo.kabletown.com."
+          },
+          "ksk": {
+            "name": "ds-01.foo.kabletown.com.",
+            "expirationDate": "1457732750",
+            "public": "ksk public key",
+            "private": "ksk private key",
+            "inceptionDate": "1426196750"
+          }
+        },
+        ... repeated for each ds in the cdn
+      }
+    }
+
+|
diff --git a/lib/go-tc/deliveryservice_ssl_keys.go b/lib/go-tc/deliveryservice_ssl_keys.go
index 3c7dc06..5f50001 100644
--- a/lib/go-tc/deliveryservice_ssl_keys.go
+++ b/lib/go-tc/deliveryservice_ssl_keys.go
@@ -171,27 +171,54 @@ type RiakPingResp struct {
 	Server string `json:"server"`
 }
 
-// DNSSECKeys is the DNSSEC keys object stored in Riak. The map key strings are both DeliveryServiceNames and CDNNames.
+// DNSSECKeys is the DNSSEC keys as stored in Riak, plus the DS record text.
 type DNSSECKeys map[string]DNSSECKeySet
 
+// DNSSECKeysV11 is the DNSSEC keys object stored in Riak. The map key strings are both DeliveryServiceNames and CDNNames.
+
+type DNSSECKeysRiak DNSSECKeysV11
+
+type DNSSECKeysV11 map[string]DNSSECKeySetV11
+
 type DNSSECKeySet struct {
 	ZSK []DNSSECKey `json:"zsk"`
 	KSK []DNSSECKey `json:"ksk"`
 }
 
+// DNSSECKeyDSRecordRiak is a DNSSEC key set (ZSK and KSK), as stored in Riak.
+// This is specifically the key data, without the DS record text (which can be computed), and is also the format used in API 1.1 through 1.3.
+type DNSSECKeySetV11 struct {
+	ZSK []DNSSECKeyV11 `json:"zsk"`
+	KSK []DNSSECKeyV11 `json:"ksk"`
+}
+
 type DNSSECKey struct {
-	InceptionDateUnix  int64              `json:"inceptionDate"`
-	ExpirationDateUnix int64              `json:"expirationDate"`
-	Name               string             `json:"name"`
-	TTLSeconds         uint64             `json:"ttl,string"`
-	Status             string             `json:"status"`
-	EffectiveDateUnix  int64              `json:"effectiveDate"`
-	Public             string             `json:"public"`
-	Private            string             `json:"private"`
-	DSRecord           *DNSSECKeyDSRecord `json:"dsRecord,omitempty"`
+	DNSSECKeyV11
+	DSRecord *DNSSECKeyDSRecord `json:"dsRecord,omitempty"`
 }
 
+type DNSSECKeyV11 struct {
+	InceptionDateUnix  int64                 `json:"inceptionDate"`
+	ExpirationDateUnix int64                 `json:"expirationDate"`
+	Name               string                `json:"name"`
+	TTLSeconds         uint64                `json:"ttl,string"`
+	Status             string                `json:"status"`
+	EffectiveDateUnix  int64                 `json:"effectiveDate"`
+	Public             string                `json:"public"`
+	Private            string                `json:"private"`
+	DSRecord           *DNSSECKeyDSRecordV11 `json:"dsRecord,omitempty"`
+}
+
+// DNSSECKeyDSRecordRiak is a DNSSEC key DS record, as stored in Riak.
+// This is specifically the key data, without the DS record text (which can be computed), and is also the format used in API 1.1 through 1.3.
+type DNSSECKeyDSRecordRiak DNSSECKeyDSRecordV11
+
 type DNSSECKeyDSRecord struct {
+	DNSSECKeyDSRecordV11
+	Text string `json:"text"`
+}
+
+type DNSSECKeyDSRecordV11 struct {
 	Algorithm  int64  `json:"algorithm,string"`
 	DigestType int64  `json:"digestType,string"`
 	Digest     string `json:"digest"`
diff --git a/traffic_ops/traffic_ops_golang/cdn/dnssec.go b/traffic_ops/traffic_ops_golang/cdn/dnssec.go
index a9f8998..bfd0195 100644
--- a/traffic_ops/traffic_ops_golang/cdn/dnssec.go
+++ b/traffic_ops/traffic_ops_golang/cdn/dnssec.go
@@ -62,6 +62,71 @@ func CreateDNSSECKeys(w http.ResponseWriter, r *http.Request) {
 	api.WriteResp(w, r, "Successfully created dnssec keys for "+cdnName)
 }
 
+func GetDNSSECKeys(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	cdnName := inf.Params["name"]
+
+	riakKeys, keysExist, err := riaksvc.GetDNSSECKeys(cdnName, inf.Tx.Tx, inf.Config.RiakAuthOptions)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting DNSSEC CDN keys: "+err.Error()))
+		return
+	}
+	if !keysExist {
+		// TODO emulates Perl; change to error, 404?
+		api.WriteRespAlert(w, r, tc.SuccessLevel, " - Dnssec keys for "+cdnName+" could not be found. ") // emulates Perl
+		return
+	}
+
+	dsTTL, err := GetDSRecordTTL(inf.Tx.Tx, cdnName)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting DS Record TTL: "+err.Error()))
+		return
+	}
+
+	keys, err := deliveryservice.MakeDNSSECKeysFromRiakKeys(riakKeys, dsTTL)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys object from Riak keys: "+err.Error()))
+		return
+	}
+	api.WriteResp(w, r, keys)
+}
+
+func GetDNSSECKeysV11(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	cdnName := inf.Params["name"]
+	riakKeys, keysExist, err := riaksvc.GetDNSSECKeys(cdnName, inf.Tx.Tx, inf.Config.RiakAuthOptions)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting DNSSEC CDN keys: "+err.Error()))
+		return
+	}
+	if !keysExist {
+		// TODO emulates Perl; change to error, 404?
+		api.WriteRespAlert(w, r, tc.SuccessLevel, " - Dnssec keys for "+cdnName+" could not be found. ") // emulates Perl
+		return
+	}
+	api.WriteResp(w, r, riakKeys)
+}
+
+func GetDSRecordTTL(tx *sql.Tx, cdn string) (time.Duration, error) {
+	ttlSeconds := 0
+	if err := tx.QueryRow(`SELECT JSON_EXTRACT_PATH_TEXT(content, 'config', 'ttls', 'DS') FROM snapshot WHERE cdn = $1`, cdn).Scan(&ttlSeconds); err != nil {
+		return 0, errors.New("getting cdn '" + cdn + "' DS Record TTL from CRConfig: " + err.Error())
+	}
+	return time.Duration(ttlSeconds) * time.Second, nil
+}
+
 func generateStoreDNSSECKeys(
 	tx *sql.Tx,
 	cfg *config.Config,
@@ -92,18 +157,18 @@ func generateStoreDNSSECKeys(
 	}
 
 	inception := time.Now()
-	newCDNZSK, err := deliveryservice.GetDNSSECKeys(tc.DNSSECZSKType, cdnDNSDomain, ttl, inception, inception.Add(zExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), false)
+	newCDNZSK, err := deliveryservice.GetDNSSECKeysV11(tc.DNSSECZSKType, cdnDNSDomain, ttl, inception, inception.Add(zExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), false)
 	if err != nil {
 		return errors.New("creating zsk for cdn: " + err.Error())
 	}
 
-	newCDNKSK, err := deliveryservice.GetDNSSECKeys(tc.DNSSECKSKType, cdnDNSDomain, ttl, inception, inception.Add(kExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), true)
+	newCDNKSK, err := deliveryservice.GetDNSSECKeysV11(tc.DNSSECKSKType, cdnDNSDomain, ttl, inception, inception.Add(kExp), tc.DNSSECKeyStatusNew, time.Unix(effectiveDateUnix, 0), true)
 	if err != nil {
 		return errors.New("creating ksk for cdn: " + err.Error())
 	}
 
-	newCDNZSKs := []tc.DNSSECKey{newCDNZSK}
-	newCDNKSKs := []tc.DNSSECKey{newCDNKSK}
+	newCDNZSKs := []tc.DNSSECKeyV11{newCDNZSK}
+	newCDNKSKs := []tc.DNSSECKeyV11{newCDNKSK}
 
 	if oldKeysExist {
 		oldKeyCDN, oldKeyCDNExists := oldKeys[cdnName]
@@ -123,8 +188,8 @@ func generateStoreDNSSECKeys(
 		}
 	}
 
-	newKeys := tc.DNSSECKeys{}
-	newKeys[cdnName] = tc.DNSSECKeySet{ZSK: newCDNZSKs, KSK: newCDNKSKs}
+	newKeys := tc.DNSSECKeysV11{}
+	newKeys[cdnName] = tc.DNSSECKeySetV11{ZSK: newCDNZSKs, KSK: newCDNKSKs}
 
 	cdnKeys := newKeys[cdnName]
 
@@ -155,7 +220,7 @@ func generateStoreDNSSECKeys(
 		}
 		newKeys[ds.Name] = dsKeys
 	}
-	if err := riaksvc.PutDNSSECKeys(newKeys, cdnName, tx, cfg.RiakAuthOptions); err != nil {
+	if err := riaksvc.PutDNSSECKeys(tc.DNSSECKeysRiak(newKeys), cdnName, tx, cfg.RiakAuthOptions); err != nil {
 		return errors.New("putting Riak DNSSEC CDN keys: " + err.Error())
 	}
 	return nil
diff --git a/traffic_ops/traffic_ops_golang/cdn/genksk.go b/traffic_ops/traffic_ops_golang/cdn/genksk.go
index 6957d33..360e6a1 100644
--- a/traffic_ops/traffic_ops_golang/cdn/genksk.go
+++ b/traffic_ops/traffic_ops_golang/cdn/genksk.go
@@ -143,16 +143,16 @@ WHERE
 }
 
 // regenExpiredKeys regenerates expired keys. The key is the map key into the keys object, which may be a CDN name or a delivery service name.
-func regenExpiredKeys(typeKSK bool, existingKeys tc.DNSSECKeySet, effectiveDate time.Time, tld bool, resetExp bool) (tc.DNSSECKeySet, error) {
+func regenExpiredKeys(typeKSK bool, existingKeys tc.DNSSECKeySetV11, effectiveDate time.Time, tld bool, resetExp bool) (tc.DNSSECKeySetV11, error) {
 
-	existingKey := ([]tc.DNSSECKey)(nil)
+	existingKey := ([]tc.DNSSECKeyV11)(nil)
 	if typeKSK {
 		existingKey = existingKeys.KSK
 	} else {
 		existingKey = existingKeys.ZSK
 	}
 
-	oldKey := tc.DNSSECKey{}
+	oldKey := tc.DNSSECKeyV11{}
 	oldKeyFound := false
 	for _, key := range existingKey {
 		if key.Status == tc.DNSSECKeyStatusNew {
@@ -180,9 +180,9 @@ func regenExpiredKeys(typeKSK bool, existingKeys tc.DNSSECKeySet, effectiveDate
 	if !typeKSK {
 		keyType = tc.DNSSECZSKType
 	}
-	newKey, err := deliveryservice.GetDNSSECKeys(keyType, name, ttl, newInception, newExpiration, tc.DNSSECKeyStatusNew, effectiveDate, tld)
+	newKey, err := deliveryservice.GetDNSSECKeysV11(keyType, name, ttl, newInception, newExpiration, tc.DNSSECKeyStatusNew, effectiveDate, tld)
 	if err != nil {
-		return tc.DNSSECKeySet{}, errors.New("getting and generating DNSSEC keys: " + err.Error())
+		return tc.DNSSECKeySetV11{}, errors.New("getting and generating DNSSEC keys: " + err.Error())
 	}
 
 	oldKey.Status = tc.DNSSECKeyStatusExpired
@@ -190,11 +190,11 @@ func regenExpiredKeys(typeKSK bool, existingKeys tc.DNSSECKeySet, effectiveDate
 		oldKey.ExpirationDateUnix = effectiveDate.Unix()
 	}
 
-	regenKeys := tc.DNSSECKeySet{}
+	regenKeys := tc.DNSSECKeySetV11{}
 	if typeKSK {
-		regenKeys = tc.DNSSECKeySet{ZSK: existingKeys.ZSK, KSK: []tc.DNSSECKey{newKey, oldKey}}
+		regenKeys = tc.DNSSECKeySetV11{ZSK: existingKeys.ZSK, KSK: []tc.DNSSECKeyV11{newKey, oldKey}}
 	} else {
-		regenKeys = tc.DNSSECKeySet{ZSK: []tc.DNSSECKey{newKey, oldKey}, KSK: existingKeys.KSK}
+		regenKeys = tc.DNSSECKeySetV11{ZSK: []tc.DNSSECKeyV11{newKey, oldKey}, KSK: existingKeys.KSK}
 	}
 	return regenKeys, nil
 }
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go b/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
index 269c8af..20642e3 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/dnssec.go
@@ -23,6 +23,7 @@ import (
 	"database/sql"
 	"encoding/base64"
 	"errors"
+	"strconv"
 	"strings"
 	"time"
 
@@ -60,19 +61,19 @@ func PutDNSSecKeys(tx *sql.Tx, cfg *config.Config, xmlID string, cdnName string,
 }
 
 // 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) {
+func CreateDNSSECKeys(tx *sql.Tx, cfg *config.Config, xmlID string, exampleURLs []string, cdnKeys tc.DNSSECKeySetV11, kskExpiration time.Duration, zskExpiration time.Duration, ttl time.Duration, overrideTTL bool) (tc.DNSSECKeySetV11, error) {
 	if len(cdnKeys.ZSK) == 0 {
-		return tc.DNSSECKeySet{}, errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
+		return tc.DNSSECKeySetV11{}, errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
 	}
 	if len(cdnKeys.KSK) == 0 {
-		return tc.DNSSECKeySet{}, errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
+		return tc.DNSSECKeySetV11{}, errors.New("getting DNSSec keys from Riak: no DNSSec ZSK keys for CDN")
 	}
 	if !overrideTTL {
 		ttl = getKeyTTL(cdnKeys.KSK, ttl)
 	}
 	dsName, err := GetDSDomainName(exampleURLs)
 	if err != nil {
-		return tc.DNSSECKeySet{}, errors.New("creating DS domain name: " + err.Error())
+		return tc.DNSSECKeySetV11{}, errors.New("creating DS domain name: " + err.Error())
 	}
 	inception := time.Now()
 	zExpiration := inception.Add(zskExpiration)
@@ -80,19 +81,19 @@ func CreateDNSSECKeys(tx *sql.Tx, cfg *config.Config, xmlID string, exampleURLs
 
 	tld := false
 	effectiveDate := inception
-	zsk, err := GetDNSSECKeys(tc.DNSSECZSKType, dsName, ttl, inception, zExpiration, tc.DNSSECKeyStatusNew, effectiveDate, tld)
+	zsk, err := GetDNSSECKeysV11(tc.DNSSECZSKType, dsName, ttl, inception, zExpiration, tc.DNSSECKeyStatusNew, effectiveDate, tld)
 	if err != nil {
-		return tc.DNSSECKeySet{}, errors.New("getting DNSSEC keys for ZSK: " + err.Error())
+		return tc.DNSSECKeySetV11{}, errors.New("getting DNSSEC keys for ZSK: " + err.Error())
 	}
-	ksk, err := GetDNSSECKeys(tc.DNSSECKSKType, dsName, ttl, inception, kExpiration, tc.DNSSECKeyStatusNew, effectiveDate, tld)
+	ksk, err := GetDNSSECKeysV11(tc.DNSSECKSKType, dsName, ttl, inception, kExpiration, tc.DNSSECKeyStatusNew, effectiveDate, tld)
 	if err != nil {
-		return tc.DNSSECKeySet{}, errors.New("getting DNSSEC keys for KSK: " + err.Error())
+		return tc.DNSSECKeySetV11{}, errors.New("getting DNSSEC keys for KSK: " + err.Error())
 	}
-	return tc.DNSSECKeySet{ZSK: []tc.DNSSECKey{zsk}, KSK: []tc.DNSSECKey{ksk}}, nil
+	return tc.DNSSECKeySetV11{ZSK: []tc.DNSSECKeyV11{zsk}, KSK: []tc.DNSSECKeyV11{ksk}}, nil
 }
 
-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{
+func GetDNSSECKeysV11(keyType string, dsName string, ttl time.Duration, inception time.Time, expiration time.Time, status string, effectiveDate time.Time, tld bool) (tc.DNSSECKeyV11, error) {
+	key := tc.DNSSECKeyV11{
 		InceptionDateUnix:  inception.Unix(),
 		ExpirationDateUnix: expiration.Unix(),
 		Name:               dsName,
@@ -108,7 +109,7 @@ func GetDNSSECKeys(keyType string, dsName string, ttl time.Duration, inception t
 
 // 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 time.Duration, tld bool) (string, string, *tc.DNSSECKeyDSRecord, error) {
+func genKeys(dsName string, ksk bool, ttl time.Duration, tld bool) (string, string, *tc.DNSSECKeyDSRecordV11, error) {
 	bits := 1024
 	flags := 256
 	algorithm := dns.RSASHA256 // 8 - http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
@@ -119,6 +120,10 @@ func genKeys(dsName string, ksk bool, ttl time.Duration, tld bool) (string, stri
 		bits *= 2
 	}
 
+	// Note: currently, the Router appears to hard-code this in what it generates for the DS record (or at least the "Publish this" log message).
+	// DO NOT change this, without verifying the Router works correctly with this digest/type, and specifically with the text generated by MakeDSRecordText inserted in the parent resolver.
+	digestType := dns.SHA256
+
 	dnskey := dns.DNSKEY{
 		Hdr: dns.RR_Header{
 			Name:   dsName,
@@ -142,10 +147,13 @@ func genKeys(dsName string, ksk bool, ttl time.Duration, tld bool) (string, stri
 	pubKeyStr := dnskey.String() // RFC1035 single-line zone file format; cooresponds to Perl Net::DNS::RR.plain
 	pubKeyStrBase64 := base64.StdEncoding.EncodeToString([]byte(pubKeyStr))
 
-	keyDS := (*tc.DNSSECKeyDSRecord)(nil)
+	keyDS := (*tc.DNSSECKeyDSRecordV11)(nil)
 	if ksk && tld {
-		dsRecord := dnskey.ToDS(dns.SHA1) // TODO update to SHA512
-		keyDS = &tc.DNSSECKeyDSRecord{Algorithm: int64(dsRecord.Algorithm), DigestType: int64(dsRecord.DigestType), Digest: dsRecord.Digest}
+		dsRecord := dnskey.ToDS(digestType)
+		if dsRecord == nil {
+			return "", "", nil, errors.New("creating DS record from DNSKEY record: " + err.Error())
+		}
+		keyDS = &tc.DNSSECKeyDSRecordV11{Algorithm: int64(dsRecord.Algorithm), DigestType: int64(dsRecord.DigestType), Digest: dsRecord.Digest}
 	}
 
 	return pubKeyStrBase64, priKeyStrBase64, keyDS, nil
@@ -173,9 +181,9 @@ func GetDSDomainName(dsExampleURLs []string) (string, error) {
 
 const dnssecDefaultKSKExpiration = time.Duration(365) * time.Hour * 24
 const dnssecDefaultZSKExpiration = time.Duration(30) * time.Hour * 24
-const dnssecDefaultTTL = 60
+const dnssecDefaultTTL = 30
 
-func getKeyExpiration(keys []tc.DNSSECKey, defaultExpiration time.Duration) time.Duration {
+func getKeyExpiration(keys []tc.DNSSECKeyV11, defaultExpiration time.Duration) time.Duration {
 	for _, key := range keys {
 		if key.Status != tc.DNSSECKeyStatusNew {
 			continue
@@ -185,7 +193,7 @@ func getKeyExpiration(keys []tc.DNSSECKey, defaultExpiration time.Duration) time
 	return defaultExpiration
 }
 
-func getKeyTTL(keys []tc.DNSSECKey, defaultTTL time.Duration) time.Duration {
+func getKeyTTL(keys []tc.DNSSECKeyV11, defaultTTL time.Duration) time.Duration {
 	for _, key := range keys {
 		if key.Status != tc.DNSSECKeyStatusNew {
 			continue
@@ -194,3 +202,81 @@ func getKeyTTL(keys []tc.DNSSECKey, defaultTTL time.Duration) time.Duration {
 	}
 	return defaultTTL
 }
+
+// MakeDNSSECKeySetFromRiakKeySet creates a DNSSECKeySet (as served by Traffic Ops) from a DNSSECKeysRiak (as stored in Riak), adding any computed data.
+// Notably, this adds the full DS Record text to CDN KSKs
+func MakeDNSSECKeysFromRiakKeys(riakKeys tc.DNSSECKeysRiak, dsTTL time.Duration) (tc.DNSSECKeys, error) {
+	keys := map[string]tc.DNSSECKeySet{}
+	for name, riakKeySet := range riakKeys {
+		newKeySet := tc.DNSSECKeySet{}
+		for _, zsk := range riakKeySet.ZSK {
+			newZSK := tc.DNSSECKey{DNSSECKeyV11: zsk}
+			// ZSKs don't have DSRecords, so we don't need to check here
+			newKeySet.ZSK = append(newKeySet.ZSK, newZSK)
+		}
+		for _, ksk := range riakKeySet.KSK {
+			newKSK := tc.DNSSECKey{DNSSECKeyV11: ksk}
+			if ksk.DSRecord != nil {
+				newKSK.DSRecord = &tc.DNSSECKeyDSRecord{DNSSECKeyDSRecordV11: *ksk.DSRecord}
+				err := error(nil)
+				newKSK.DSRecord.Text, err = MakeDSRecordText(ksk, dsTTL)
+				if err != nil {
+					return tc.DNSSECKeys{}, errors.New("making DS record text: " + err.Error())
+				}
+			}
+			newKeySet.KSK = append(newKeySet.KSK, newKSK)
+		}
+		keys[name] = newKeySet
+	}
+	return tc.DNSSECKeys(keys), nil
+}
+
+func MakeDSRecordText(ksk tc.DNSSECKeyV11, ttl time.Duration) (string, error) {
+	kskPublic := strings.Replace(ksk.Public, `\n`, "", -1) // note this is replacing the actual string slash-n not a newline. Because Perl.
+	kskPublic = strings.Replace(kskPublic, "\n", "", -1)
+	kskPublicBts := []byte(kskPublic)
+	publicKeyBtsLen := base64.StdEncoding.DecodedLen(len(kskPublicBts))
+	publicKeyBts := make([]byte, publicKeyBtsLen)
+	publicKeyBtsLen, err := base64.StdEncoding.Decode(publicKeyBts, kskPublicBts)
+	if err != nil {
+		return "", errors.New("decoding ksk public key base64: " + err.Error())
+	}
+	publicKeyBts = publicKeyBts[:publicKeyBtsLen]
+
+	// ksk.Public isn't just the public key, it's the RFC 1035 single-line zone file format: "name ttl IN DNSKEY flags protocol algorithm keyBytes".
+	fields := strings.Fields(string(publicKeyBts))
+	if len(fields) < 8 {
+		return "", errors.New("malformed ksk public key: not enough fields")
+	}
+	flagsStr := fields[4]
+	protocolStr := fields[5]
+	flags, err := strconv.Atoi(flagsStr)
+	if err != nil {
+		return "", errors.New("malformed ksk public key: flags '" + flagsStr + "' not a number")
+	}
+	protocol, err := strconv.Atoi(protocolStr)
+	if err != nil {
+		return "", errors.New("malformed ksk public key: protocol '" + protocolStr + "' not a number")
+	}
+
+	realPublicKey := fields[7] // the Riak ksk.Public key is actually the RFC1035 single-line zone file format. For which the 7th field from 0 is the actual public key.
+
+	dnsKey := dns.DNSKEY{
+		Hdr: dns.RR_Header{
+			Name:   ksk.Name,
+			Rrtype: dns.TypeDNSKEY,
+			Class:  dns.ClassINET,
+			Ttl:    uint32(ttl / time.Second), // NOT ksk.TTLSeconds, which is the DNSKEY TTL. The DS has its own TTL, which in Traffic Ops (as of this writing) comes from the Parameter name 'tld.ttls.DS' config 'CRConfig.json' assigned to the profile of a Router on this CDN.
+		},
+		Flags:     uint16(flags),
+		Protocol:  uint8(protocol),
+		Algorithm: uint8(ksk.DSRecord.Algorithm),
+		PublicKey: realPublicKey,
+	}
+
+	ds := dnsKey.ToDS(uint8(ksk.DSRecord.DigestType))
+	if ds == nil {
+		return "", errors.New("failed to convert DNSKEY to DS record (is some field in the KSK invalid?)")
+	}
+	return ds.String(), nil
+}
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
index 2427331..d6a2f20 100644
--- a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
+++ b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
@@ -123,8 +123,8 @@ func Ping(tx *sql.Tx, authOpts *riak.AuthOptions) (tc.RiakPingResp, error) {
 	return tc.RiakPingResp{}, errors.New("failed to ping any Riak server")
 }
 
-func GetDNSSECKeys(cdnName string, tx *sql.Tx, authOpts *riak.AuthOptions) (tc.DNSSECKeys, bool, error) {
-	key := tc.DNSSECKeys{}
+func GetDNSSECKeys(cdnName string, tx *sql.Tx, authOpts *riak.AuthOptions) (tc.DNSSECKeysRiak, bool, error) {
+	key := tc.DNSSECKeysRiak{}
 	found := false
 	err := WithCluster(tx, authOpts, func(cluster StorageCluster) error {
 		ro, err := FetchObjectValues(cdnName, DNSSECKeysBucket, cluster)
@@ -146,7 +146,7 @@ func GetDNSSECKeys(cdnName string, tx *sql.Tx, authOpts *riak.AuthOptions) (tc.D
 	return key, found, nil
 }
 
-func PutDNSSECKeys(keys tc.DNSSECKeys, cdnName string, tx *sql.Tx, authOpts *riak.AuthOptions) error {
+func PutDNSSECKeys(keys tc.DNSSECKeysRiak, cdnName string, tx *sql.Tx, authOpts *riak.AuthOptions) error {
 	keyJSON, err := json.Marshal(&keys)
 	if err != nil {
 		return errors.New("marshalling keys: " + err.Error())
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 9b41047..982722b 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -136,6 +136,8 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.1, http.MethodPost, `cdns/{id}/queue_update$`, cdn.Queue, auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodPost, `cdns/dnsseckeys/generate(\.json)?$`, cdn.CreateDNSSECKeys, auth.PrivLevelAdmin, Authenticated, nil},
 		{1.1, http.MethodGet, `cdns/name/{name}/dnsseckeys/delete/?(\.json)?$`, cdn.DeleteDNSSECKeys, auth.PrivLevelAdmin, Authenticated, nil},
+		{1.4, http.MethodGet, `cdns/name/{name}/dnsseckeys/?(\.json)?$`, cdn.GetDNSSECKeys, auth.PrivLevelAdmin, Authenticated, nil},
+		{1.1, http.MethodGet, `cdns/name/{name}/dnsseckeys/?(\.json)?$`, cdn.GetDNSSECKeysV11, auth.PrivLevelAdmin, Authenticated, nil},
 
 		{1.4, http.MethodGet, `cdns/dnsseckeys/refresh/?(\.json)?$`, cdn.RefreshDNSSECKeys, auth.PrivLevelOperations, Authenticated, nil},