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/11 02:42:16 UTC

[trafficcontrol] branch master updated: Add TO Go deliveryservice/sslkeys/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 880ef0a  Add TO Go deliveryservice/sslkeys/generate
880ef0a is described below

commit 880ef0ae8f887281d1ecfb861c13b25dc5385f67
Author: Robert Butts <ro...@apache.org>
AuthorDate: Sun Jun 10 10:36:33 2018 -0600

    Add TO Go deliveryservice/sslkeys/generate
---
 lib/go-tc/deliveryservice_ssl_keys.go              | 110 ++++++++-----
 lib/go-util/num.go                                 |  19 +++
 lib/go-util/num_test.go                            | 180 +++++++++++++++++++++
 .../traffic_ops_golang/deliveryservice/gencert.go  | 121 ++++++++++++++
 .../traffic_ops_golang/deliveryservice/sslkeys.go  |  83 ++++++++++
 traffic_ops/traffic_ops_golang/riaksvc/dsutil.go   |  19 ++-
 traffic_ops/traffic_ops_golang/routes.go           |   1 +
 7 files changed, 486 insertions(+), 47 deletions(-)

diff --git a/lib/go-tc/deliveryservice_ssl_keys.go b/lib/go-tc/deliveryservice_ssl_keys.go
index b0258d1..0db1494 100644
--- a/lib/go-tc/deliveryservice_ssl_keys.go
+++ b/lib/go-tc/deliveryservice_ssl_keys.go
@@ -17,11 +17,10 @@ package tc
 
 import (
 	"database/sql"
-	"encoding/json"
 	"errors"
-	"fmt"
-	"strconv"
 	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 // DeliveryServiceSSLKeysResponse ...
@@ -47,45 +46,78 @@ type DeliveryServiceSSLKeys struct {
 	Country         string                            `json:"country,omitempty"`
 	State           string                            `json:"state,omitempty"`
 	Key             string                            `json:"key"`
-	Version         int                               `json:"version"`
+	Version         string                            `json:"version"`
 	Certificate     DeliveryServiceSSLKeysCertificate `json:"certificate,omitempty"`
 }
 
-/*
- * The DeliveryServicesSSLKeys are stored in RIAK as JSON.
- * It was found that the "Version" field has been written to
- * RIAK as both a string numeral enclosed in quotes ie,
- *	"version: "1"
- * and sometimes as an integer ie,
- *	"version: 1
- * In order to deal with this problem, a custom Unmarshal() workaround
- * is used, see below.
- *
- */
-func (v *DeliveryServiceSSLKeys) UnmarshalJSON(b []byte) (err error) {
-	type Alias DeliveryServiceSSLKeys
-	o := &struct {
-		Version interface{} `json:"version"`
-		*Alias
-	}{
-		Alias: (*Alias)(v),
-	}
-	if err = json.Unmarshal(b, &o); err == nil {
-		switch t := o.Version.(type) {
-		case float64:
-			v.Version = int(t)
-			break
-		case int:
-			v.Version = t
-			break
-		case string:
-			v.Version, err = strconv.Atoi(t)
-			break
-		default:
-			err = fmt.Errorf("Version field is an unandled type: %T", t)
-		}
-	}
-	return err
+type DeliveryServiceSSLKeysReq struct {
+	CDN             *string `json:"cdn,omitempty"`
+	DeliveryService *string `json:"deliveryservice,omitempty"`
+	BusinessUnit    *string `json:"businessUnit,omitempty"`
+	City            *string `json:"city,omitempty"`
+	Organization    *string `json:"organization,omitempty"`
+	HostName        *string `json:"hostname,omitempty"`
+	Country         *string `json:"country,omitempty"`
+	State           *string `json:"state,omitempty"`
+	// Key is the XMLID of the delivery service
+	Key         *string                            `json:"key"`
+	Version     *string                            `json:"version"`
+	Certificate *DeliveryServiceSSLKeysCertificate `json:"certificate,omitempty"`
+}
+
+func (r *DeliveryServiceSSLKeysReq) Sanitize() {
+	// DeliveryService and Key are the same value, so if the user sent one but not the other, set the missing one, in the principle of "be liberal in what you accept."
+	if r.DeliveryService == nil && r.Key != nil {
+		k := *r.Key // sqlx fails with aliased pointers, so make a new one
+		r.DeliveryService = &k
+	} else if r.Key == nil && r.DeliveryService != nil {
+		k := *r.DeliveryService // sqlx fails with aliased pointers, so make a new one
+		r.Key = &k
+	}
+	if r.Version == nil {
+		r.Version = util.StrPtr("")
+	}
+}
+
+func (r *DeliveryServiceSSLKeysReq) Validate(tx *sql.Tx) error {
+	r.Sanitize()
+	errs := []string{}
+	if r.CDN == nil {
+		errs = append(errs, "cdn required")
+	}
+	if r.Key == nil {
+		errs = append(errs, "key required")
+	}
+	if r.DeliveryService == nil {
+		errs = append(errs, "deliveryservice required")
+	}
+	if r.Key != nil && r.DeliveryService != nil && *r.Key != *r.DeliveryService {
+		errs = append(errs, "deliveryservice and key must match")
+	}
+	if r.BusinessUnit == nil {
+		errs = append(errs, "businessUnit required")
+	}
+	if r.City == nil {
+		errs = append(errs, "city required")
+	}
+	if r.Organization == nil {
+		errs = append(errs, "organization required")
+	}
+	if r.HostName == nil {
+		errs = append(errs, "hostname required")
+	}
+	if r.Country == nil {
+		errs = append(errs, "country required")
+	}
+	if r.State == nil {
+		errs = append(errs, "state required")
+	}
+	// version is optional
+	// certificate is optional
+	if len(errs) > 0 {
+		return errors.New("missing fields: " + strings.Join(errs, "; "))
+	}
+	return nil
 }
 
 type RiakPingResp struct {
diff --git a/lib/go-util/num.go b/lib/go-util/num.go
index 8f5532a..89d52af 100644
--- a/lib/go-util/num.go
+++ b/lib/go-util/num.go
@@ -79,3 +79,22 @@ func (i *JSONIntStr) UnmarshalJSON(d []byte) error {
 	*i = JSONIntStr(di)
 	return nil
 }
+
+// BytesLenSplit splits the given byte array into an n-length arrays. If n > len(s), returns a slice with a single []byte containing all of s. If n <= 0, returns an empty slice.
+func BytesLenSplit(s []byte, n int) [][]byte {
+	ss := [][]byte{}
+	if n <= 0 {
+		return ss
+	}
+	if n > len(s) {
+		n = len(s)
+	}
+	for i := 0; i+n <= len(s); i += n {
+		ss = append(ss, s[i:i+n])
+	}
+	rem := len(s) % n
+	if rem != 0 {
+		ss = append(ss, s[n*(len(s)/n):])
+	}
+	return ss
+}
diff --git a/lib/go-util/num_test.go b/lib/go-util/num_test.go
new file mode 100644
index 0000000..06e8aa2
--- /dev/null
+++ b/lib/go-util/num_test.go
@@ -0,0 +1,180 @@
+package util
+
+/*
+   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 rfc contains functions implementing RFC 7234, 2616, and other RFCs.
+// When changing functions, be sure they still conform to the corresponding RFC.
+// When adding symbols, document the RFC and section they correspond to.
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestBytesLenSplit(t *testing.T) {
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := 3
+		expected := [][]byte{
+			[]byte("abc"),
+			[]byte("def"),
+			[]byte("ghi"),
+			[]byte("jkl"),
+			[]byte("mno"),
+			[]byte("pqr"),
+			[]byte("stu"),
+			[]byte("vwx"),
+			[]byte("yz1"),
+			[]byte("234"),
+			[]byte("567"),
+			[]byte("890"),
+			[]byte("_-+"),
+			[]byte("="),
+		}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := 500
+		expected := [][]byte{[]byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := len(b) - 1
+		expected := [][]byte{
+			[]byte("abcdefghijklmnopqrstuvwxyz1234567890_-+"),
+			[]byte("="),
+		}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := len(b) - 2
+		expected := [][]byte{
+			[]byte("abcdefghijklmnopqrstuvwxyz1234567890_-"),
+			[]byte("+="),
+		}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := 20
+		expected := [][]byte{
+			[]byte("abcdefghijklmnopqrst"),
+			[]byte("uvwxyz1234567890_-+="),
+		}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := 0
+		expected := [][]byte{}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := -1
+		expected := [][]byte{}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := -30
+		expected := [][]byte{}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := 1000000000
+		expected := [][]byte{[]byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+	{
+		b := []byte("abcdefghijklmnopqrstuvwxyz1234567890_-+=")
+		n := 1
+		expected := [][]byte{
+			[]byte("a"),
+			[]byte("b"),
+			[]byte("c"),
+			[]byte("d"),
+			[]byte("e"),
+			[]byte("f"),
+			[]byte("g"),
+			[]byte("h"),
+			[]byte("i"),
+			[]byte("j"),
+			[]byte("k"),
+			[]byte("l"),
+			[]byte("m"),
+			[]byte("n"),
+			[]byte("o"),
+			[]byte("p"),
+			[]byte("q"),
+			[]byte("r"),
+			[]byte("s"),
+			[]byte("t"),
+			[]byte("u"),
+			[]byte("v"),
+			[]byte("w"),
+			[]byte("x"),
+			[]byte("y"),
+			[]byte("z"),
+			[]byte("1"),
+			[]byte("2"),
+			[]byte("3"),
+			[]byte("4"),
+			[]byte("5"),
+			[]byte("6"),
+			[]byte("7"),
+			[]byte("8"),
+			[]byte("9"),
+			[]byte("0"),
+			[]byte("_"),
+			[]byte("-"),
+			[]byte("+"),
+			[]byte("="),
+		}
+		actual := BytesLenSplit(b, n)
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("BytesLenSplit expected: %+v actual: %+v\n", expected, actual)
+		}
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/gencert.go b/traffic_ops/traffic_ops_golang/deliveryservice/gencert.go
new file mode 100644
index 0000000..21c3116
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/gencert.go
@@ -0,0 +1,121 @@
+package deliveryservice
+
+/*
+ * 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 (
+	"bytes"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/base64"
+	"encoding/pem"
+	"errors"
+	"math/big"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-util"
+)
+
+const NewCertValidDuration = time.Hour * 24 * 365
+
+// GenerateCert generates a key and certificate for serving HTTPS. The generated key is 2048-bit RSA, to match the old Perl code.
+// The certificate will be valid for NewCertValidDuration time after now.
+// Returns PEM-encoded certificate signing request (csr), certificate (crt), and key; or any error.
+func GenerateCert(host, country, city, state, org, unit string) ([]byte, []byte, []byte, error) {
+	priv, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		return nil, nil, nil, errors.New("generating key: " + err.Error())
+	}
+	now := time.Now()
+	expires := now.Add(NewCertValidDuration)
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		return nil, nil, nil, errors.New("getting random int for serial number: " + err.Error())
+	}
+
+	subj := pkix.Name{
+		CommonName:         host,
+		Country:            []string{country},
+		Province:           []string{state},
+		Locality:           []string{city},
+		Organization:       []string{org},
+		OrganizationalUnit: []string{unit},
+	}
+
+	crt := x509.Certificate{
+		SerialNumber:          serialNumber,
+		Subject:               subj,
+		NotBefore:             now,
+		NotAfter:              expires,
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+		BasicConstraintsValid: true,
+		DNSNames:              []string{host},
+		Version:               1,
+	}
+
+	crtReq := x509.CertificateRequest{
+		Subject:            subj,
+		SignatureAlgorithm: x509.SHA256WithRSA,
+		Version:            1,
+	}
+
+	crtDer, err := x509.CreateCertificate(rand.Reader, &crt, &crt, &priv.PublicKey, priv)
+	if err != nil {
+		return nil, nil, nil, errors.New("creating certificate: " + err.Error())
+	}
+	crtBuf := bytes.Buffer{}
+	if err := pem.Encode(&crtBuf, &pem.Block{Type: "CERTIFICATE", Bytes: crtDer}); err != nil {
+		return nil, nil, nil, errors.New("pem-encoding certificate: " + err.Error())
+	}
+	crtPem := crtBuf.Bytes()
+
+	csrDer, err := x509.CreateCertificateRequest(rand.Reader, &crtReq, priv)
+	if err != nil {
+		return nil, nil, nil, errors.New("creating certificate request: " + err.Error())
+	}
+	csrBuf := bytes.Buffer{}
+	if err := pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDer}); err != nil {
+		return nil, nil, nil, errors.New("pem-encoding certificate request: " + err.Error())
+	}
+	csrPem := csrBuf.Bytes()
+
+	keyDer := x509.MarshalPKCS1PrivateKey(priv)
+	if keyDer == nil {
+		return nil, nil, nil, errors.New("marshalling private key: nil der")
+	}
+	keyBuf := bytes.Buffer{}
+	if err := pem.Encode(&keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDer}); err != nil {
+		return nil, nil, nil, errors.New("pem-encoding private key: " + err.Error())
+	}
+	keyPem := keyBuf.Bytes()
+
+	return EncodePEMToLegacyPerlRiakFormat(csrPem), EncodePEMToLegacyPerlRiakFormat(crtPem), EncodePEMToLegacyPerlRiakFormat(keyPem), nil
+}
+
+// EncodePEMToLegacyPerlRiakFormat takes a PEM-encoded byte (typically a certificate, csr, or key) and returns the format Perl Traffic Ops used to send to Riak.
+func EncodePEMToLegacyPerlRiakFormat(pem []byte) []byte {
+	b64Pem := []byte(base64.StdEncoding.EncodeToString(pem)) // Why are we base64-encoding a base64-encoded format? Because Perl
+	b64Lines := util.BytesLenSplit(b64Pem, 76)               // Why 76? Because Perl
+	joined := bytes.Join(b64Lines, []byte{'\n'})             // Why are we joining arbitrary base64-encoded characters with newlines? Because Perl
+	return joined
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/sslkeys.go b/traffic_ops/traffic_ops_golang/deliveryservice/sslkeys.go
new file mode 100644
index 0000000..3337943
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/sslkeys.go
@@ -0,0 +1,83 @@
+package deliveryservice
+
+/*
+ * 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"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"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/riaksvc"
+)
+
+func GenerateSSLKeys(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.DeliveryServiceSSLKeysReq{}
+	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 err := generatePutRiakKeys(req, inf.Tx.Tx, inf.Config); err != nil {
+		api.HandleErr(w, r, http.StatusInternalServerError, nil, errors.New("generating and putting SSL keys: "+err.Error()))
+		return
+	}
+	*inf.CommitTx = true
+	api.WriteResp(w, r, "Successfully created ssl keys for "+*req.DeliveryService)
+}
+
+// generatePutRiakKeys generates a certificate, csr, and key from the given request, and insert it into the Riak key database.
+// The req MUST be validated, ensuring required fields exist.
+func generatePutRiakKeys(req tc.DeliveryServiceSSLKeysReq, tx *sql.Tx, cfg *config.Config) error {
+	dsSSLKeys := tc.DeliveryServiceSSLKeys{
+		CDN:             *req.CDN,
+		DeliveryService: *req.DeliveryService,
+		BusinessUnit:    *req.BusinessUnit,
+		City:            *req.City,
+		Organization:    *req.Organization,
+		Hostname:        *req.HostName,
+		Country:         *req.Country,
+		State:           *req.State,
+		Key:             *req.Key,
+		Version:         *req.Version,
+	}
+	if req.Certificate != nil {
+		dsSSLKeys.Certificate = *req.Certificate
+	} else {
+		csr, crt, key, err := GenerateCert(*req.HostName, *req.Country, *req.City, *req.State, *req.Organization, *req.BusinessUnit)
+		if err != nil {
+			return errors.New("generating certificate: " + err.Error())
+		}
+		dsSSLKeys.Certificate = tc.DeliveryServiceSSLKeysCertificate{Crt: string(crt), Key: string(key), CSR: string(csr)}
+	}
+	if err := riaksvc.PutDeliveryServiceSSLKeysObjTx(dsSSLKeys, tx, cfg.RiakAuthOptions); err != nil {
+		return errors.New("putting riak keys: " + err.Error())
+	}
+	return nil
+}
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
index 639f6f5..e6ff6ae 100644
--- a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
+++ b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
@@ -32,18 +32,21 @@ import (
 
 const DeliveryServiceSSLKeysBucket = "ssl"
 const DNSSECKeysBucket = "dnssec"
+const DefaultDSSSLKeyVersion = "latest"
 
-func GetDeliveryServiceSSLKeysObj(xmlID string, version string, tx *sql.Tx, authOpts *riak.AuthOptions) (tc.DeliveryServiceSSLKeys, bool, error) {
-	key := tc.DeliveryServiceSSLKeys{}
+func MakeDSSSLKeyKey(dsName, version string) string {
 	if version == "" {
-		xmlID += "-latest"
-	} else {
-		xmlID += "-" + version
+		version = DefaultDSSSLKeyVersion
 	}
+	return dsName + "-" + version
+}
+
+func GetDeliveryServiceSSLKeysObj(xmlID string, version string, tx *sql.Tx, authOpts *riak.AuthOptions) (tc.DeliveryServiceSSLKeys, bool, error) {
+	key := tc.DeliveryServiceSSLKeys{}
 	found := false
 	err := WithClusterTx(tx, authOpts, func(cluster StorageCluster) error {
 		// get the deliveryservice ssl keys by xmlID and version
-		ro, err := FetchObjectValues(xmlID, DeliveryServiceSSLKeysBucket, cluster)
+		ro, err := FetchObjectValues(MakeDSSSLKeyKey(xmlID, version), DeliveryServiceSSLKeysBucket, cluster)
 		if err != nil {
 			return err
 		}
@@ -103,7 +106,7 @@ func PutDeliveryServiceSSLKeysObj(key tc.DeliveryServiceSSLKeys, tx *sql.Tx, aut
 			ContentType:     "text/json",
 			Charset:         "utf-8",
 			ContentEncoding: "utf-8",
-			Key:             key.DeliveryService,
+			Key:             MakeDSSSLKeyKey(key.DeliveryService, key.Version),
 			Value:           []byte(keyJSON),
 		}
 		if err = SaveObject(obj, DeliveryServiceSSLKeysBucket, cluster); err != nil {
@@ -124,7 +127,7 @@ func PutDeliveryServiceSSLKeysObjTx(key tc.DeliveryServiceSSLKeys, tx *sql.Tx, a
 			ContentType:     "text/json",
 			Charset:         "utf-8",
 			ContentEncoding: "utf-8",
-			Key:             key.DeliveryService,
+			Key:             MakeDSSSLKeyKey(key.DeliveryService, key.Version),
 			Value:           []byte(keyJSON),
 		}
 		if err = SaveObject(obj, DeliveryServiceSSLKeysBucket, cluster); err != nil {
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index ed48e2a..94ec297 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -368,6 +368,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.1, http.MethodDelete, `deliveryservices/{id}/?(\.json)?$`, api.DeleteHandler(deliveryservice.GetTypeV12Factory()), auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodGet, `deliveryservices/{id}/servers/eligible/?(\.json)?$`, deliveryservice.GetServersEligible, auth.PrivLevelReadOnly, Authenticated, nil},
 
+		{1.1, http.MethodPost, `deliveryservices/sslkeys/generate/?(\.json)?$`, deliveryservice.GenerateSSLKeys, auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodGet, `riak/bucket/{bucket}/key/{key}/values/?(\.json)?$`, apiriak.GetBucketKey, auth.PrivLevelAdmin, Authenticated, nil},
 
 		//System