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 5bcdc89  ACME integration and external account binding (#5466)
5bcdc89 is described below

commit 5bcdc896fea59e25ccf5edfed61ce8e15c00ee15
Author: mattjackson220 <[email protected]>
AuthorDate: Tue Feb 2 17:23:56 2021 -0700

    ACME integration and external account binding (#5466)
    
    * ACME integration and external account binding
    
    * updates per comments
    
    * moved route to api 4.0
    
    * fixed indentation
---
 CHANGELOG.md                                       |   1 +
 docs/source/admin/traffic_ops.rst                  |  10 +
 docs/source/admin/traffic_router.rst               |  39 +++
 .../deliveryservices_xmlid_xmlid_sslkeys_renew.rst |  54 +++
 infrastructure/cdn-in-a-box/traffic_ops/config.sh  |  11 +-
 traffic_ops/app/conf/cdn.conf                      |  11 +-
 traffic_ops/traffic_ops_golang/config/config.go    |  14 +-
 .../traffic_ops_golang/deliveryservice/acme.go     | 381 +++++++++++++++++++++
 .../deliveryservice/autorenewcerts.go              |   2 +-
 .../deliveryservice/letsencryptcert.go             | 184 ++--------
 traffic_ops/traffic_ops_golang/routing/routes.go   |   5 +-
 .../common/api/DeliveryServiceSslKeysService.js    |  15 +
 .../FormDeliveryServiceSslKeysController.js        |  23 ++
 .../form.deliveryServiceSslKeys.tpl.html           |   1 +
 14 files changed, 585 insertions(+), 166 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d681be0..19b088b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Ops: Added validation to ensure that the cachegroups of a delivery 
services' assigned ORG servers are present in the topology
 - Traffic Ops: Added validation to ensure that the `weight` parameter of 
`parent.config` is a float
 - Added license files to the RPMs
+- Added ACME certificate renewals and ACME account registration using external 
account binding
 
 ### Fixed
 - [#5445](https://github.com/apache/trafficcontrol/issues/5445) - When 
updating a registered user, ignore updates on registration_sent field.
diff --git a/docs/source/admin/traffic_ops.rst 
b/docs/source/admin/traffic_ops.rst
index 9f2ff3b..4b620eb 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -306,6 +306,16 @@ cdn.conf
 """"""""
 This file deals with the configuration parameters of running Traffic Ops 
itself. It is a JSON-format set of options and their respective values. For the 
`Legacy Perl Script`_ to work with this file, it must be in its default 
location at :file:`/opt/traffic_ops/app/conf/cdn.conf`, but 
`traffic_ops_golang`_ will use whatever file is specified by its 
:option:`--cfg` option. The keys of the file are described below.
 
+:acme_accounts: This is an optional array of objects to define 
ref:`external_account_binding` information to an existing :abbr:`ACME 
(Automatic Certificate Management Environment)` account.  The `acme_provider` 
and `user_email` combination must be unique.
+
+       .. versionadded:: 5.1
+
+       :acme_provider: The certificate provider. This field needs to correlate 
to the AuthType field for each certificate so the renewal functionality knows 
which provider to use.
+       :user_email:    The email used to set up the account with the provider.
+       :acme_url:      The URL for the :abbr:`ACME (Automatic Certificate 
Management Environment)`.
+       :kid:           The key ID provided by the :abbr:`ACME (Automatic 
Certificate Management Environment)` provider for 
ref:`external_account_binding`.
+       :hmac_encoded:  The :abbr:`HMAC (Hashed Message Authentication Code)` 
key provided by the :abbr:`ACME (Automatic Certificate Management Environment)` 
provider for ref:`external_account_binding`. This should be in Base64 URL 
encoded.
+
 :geniso: This object contains configuration options for system ISO generation.
 
        :iso_root_path: Sets the filesystem path to the root of the ISO 
generation directory. For default installations, this should usually be set to 
:file:`/opt/traffic_ops/app/public`.
diff --git a/docs/source/admin/traffic_router.rst 
b/docs/source/admin/traffic_router.rst
index 95f081c..6ccce57 100644
--- a/docs/source/admin/traffic_router.rst
+++ b/docs/source/admin/traffic_router.rst
@@ -762,6 +762,45 @@ The ordering of certificates within the certificate bundle 
matters. It must be:
 
 To see the ordering of certificates you may have to manually split up your 
certificate chain and use :manpage:`openssl(1ssl)` on each individual 
certificate
 
+Automatic Certificate Management Environment
+--------------------------------------------
+Automatic Certificate Management Environment (ACME) is a protocol for 
automatically generating, renewing, and revoking SSL certificates.  Currently, 
:abbr:`ACME (Automatic Certificate Management Environment)` can be used through 
:ref:`lets_encrypt` or through :ref:`external_account_binding`.
+
+.. _external_account_binding:
+
+External Account Binding
+------------------------
+External account binding allows the user to use an existing account with an 
:abbr:`ACME (Automatic Certificate Management Environment)` provider to obtain, 
renew, and revoke SSL certificates.
+To use this functionality, fill in the fields in :ref:`cdn.conf` for the 
:abbr:`ACME (Automatic Certificate Management Environment)` provider with which 
the account is set up.
+The first time this is used for a specific :abbr:`ACME (Automatic Certificate 
Management Environment)` provider (defined by the `acme_provider` and 
`user_email` fields) the information will be used to get a private key and 
account URL from the :abbr:`ACME (Automatic Certificate Management 
Environment)` provider and register the account. These will be stored for later 
use.
+External account binding information can only be used once, so after the first 
time, the private key and URL will be used.
+
+.. Important:: The `acme_provider` and `user_email` combination must be 
unique.  The `acme_provider` field must correlate to the `AuthType` field for 
each certificate to be renewed using that provider.
+
+.. Note:: As of writing, external account binding can only be used for 
certificate renewals.
+
+External account binding can be set up through :ref:`cdn.conf` by updating the 
following fields:
+
+.. table:: Fields to update for external account binding using :abbr:`ACME 
(Automatic Certificate Management Environment)` protocol under `acme_accounts`
+
+       
+------------------------------+---------+----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | Name                         | Type    | Required | Description       
                                                                                
                                                                                
                                 |
+       
+==============================+=========+==========+====================================================================================================================================================================================================================+
+       | acme_provider                | string  | Yes      | The certificate 
provider. This field needs to correlate to the AuthType field for each 
certificate so the renewal functionality knows which provider to use.           
                                            |
+       
+------------------------------+---------+----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | user_email                   | string  | Yes      | The email used to 
set up the account with the provider.                                           
                                                                                
                                 |
+       
+------------------------------+---------+----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | acme_url                     | string  | Yes      | The URL for the 
:abbr:`ACME (Automatic Certificate Management Environment)`.                    
                                                                                
                                   |
+       
+------------------------------+---------+----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | kid                          | string  | No       | The key ID 
provided by the :abbr:`ACME (Automatic Certificate Management Environment)` 
provider for external account binding.                                          
                                            |
+       
+------------------------------+---------+----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+       | hmac_encoded                 | string  | No       | The :abbr:`HMAC 
(Hashed Message Authentication Code)` key provided by the :abbr:`ACME 
(Automatic Certificate Management Environment)` provider for external account 
binding. This should be in Base64 URL encoded. |
+       
+------------------------------+---------+----------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+
+.. Note:: The `kid` and `hmac_encoded` fields are required unless the account 
has already been registered and the information has been stored in the Traffic 
Ops Database.
+
+.. _lets_encrypt:
+
 Let's Encrypt
 -------------
 Let’s Encrypt is a free, automated :abbr:`CA (Certificate Authority)` using 
:abbr:`ACME (Automated Certificate Management Environment)` protocol. Let's 
Encrypt performs a domain validation before issuing or renewing a certificate. 
There are several options for domain validation but for this application the 
DNS challenge is used in order to receive wildcard certificates. Let's Encrypt 
sends a token to be used as a TXT record at 
``_acme-challenge.domain.example.com`` and after verifying th [...]
diff --git a/docs/source/api/v3/deliveryservices_xmlid_xmlid_sslkeys_renew.rst 
b/docs/source/api/v3/deliveryservices_xmlid_xmlid_sslkeys_renew.rst
new file mode 100644
index 0000000..f90d58f
--- /dev/null
+++ b/docs/source/api/v3/deliveryservices_xmlid_xmlid_sslkeys_renew.rst
@@ -0,0 +1,54 @@
+..
+..
+.. 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.
+..
+
+.. _to-api-deliveryservices-xmlid-xmlid-sslkeys-renew:
+
+**************************************************
+``deliveryservices/xmlId/{{XMLID}}/sslkeys/renew``
+**************************************************
+
+``POST``
+========
+Uses :abbr:`ACME (Automatic Certificate Management Environment)` protocol to 
renew SSL keys for a :term:`Delivery Service`.
+
+:Auth. Required: Yes
+:Roles Required: "admin"
+:Response Type:  Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+       +-------+------------------------------------------------------+
+       |  Name |              Description                             |
+       +=======+======================================================+
+       | XMLID | The 'xml_id' of the desired :term:`Delivery Service` |
+       +-------+------------------------------------------------------+
+
+
+Request Structure
+-----------------
+No parameters available
+
+
+Response Structure
+------------------
+.. code-block:: json
+       :caption: Response Example
+
+       { "alerts": [{
+               "level": "success",
+               "text": "Certificate for test-xml-id successfully renewed."
+       }]}
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/config.sh 
b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
index f98e7a3..de6830e 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/config.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
@@ -151,7 +151,16 @@ cat <<-EOF >/opt/traffic_ops/app/conf/cdn.conf
         "convert_self_signed": false,
         "renew_days_before_expiration": 30,
         "environment": "staging"
-    }
+    },
+    "acme_accounts": [
+        {
+            "acme_provider" : "",
+            "user_email" : "",
+            "acme_url" : "",
+            "kid" : "",
+            "hmac_encoded" : ""
+        }
+    ]
 }
 EOF
 
diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf
index 531e5d4..b6905ae 100644
--- a/traffic_ops/app/conf/cdn.conf
+++ b/traffic_ops/app/conf/cdn.conf
@@ -81,5 +81,14 @@
         "convert_self_signed": false,
         "renew_days_before_expiration": 30,
         "environment": "production"
-    }
+    },
+    "acme_accounts": [
+        {
+            "acme_provider" : "",
+            "user_email" : "",
+            "acme_url" : "",
+            "kid" : "",
+            "hmac_encoded" : ""
+        }
+    ]
 }
diff --git a/traffic_ops/traffic_ops_golang/config/config.go 
b/traffic_ops/traffic_ops_golang/config/config.go
index dba528b..7f95a33 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -48,8 +48,9 @@ type Config struct {
        SMTP                   *ConfigSMTP `json:"smtp"`
        ConfigPortal           `json:"portal"`
        ConfigLetsEncrypt      `json:"lets_encrypt"`
-       DB                     ConfigDatabase `json:"db"`
-       Secrets                []string       `json:"secrets"`
+       AcmeAccounts           []ConfigAcmeAccount `json:"acme_accounts"`
+       DB                     ConfigDatabase      `json:"db"`
+       Secrets                []string            `json:"secrets"`
        // NOTE: don't care about any other fields for now..
        RiakAuthOptions  *riak.AuthOptions
        RiakEnabled      bool
@@ -158,6 +159,15 @@ type ConfigLetsEncrypt struct {
        Environment               string `json:"environment"`
 }
 
+// ConfigAcmeAccount contains all account information for a single ACME 
provider to be registered with External Account Binding
+type ConfigAcmeAccount struct {
+       AcmeProvider string `json:"acme_provider"`
+       UserEmail    string `json:"user_email"`
+       AcmeUrl      string `json:"acme_url"`
+       Kid          string `json:"kid"`
+       HmacEncoded  string `json:"hmac_encoded"`
+}
+
 // ConfigDatabase reflects the structure of the database.conf file
 type ConfigDatabase struct {
        Description string `json:"description"`
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/acme.go 
b/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
new file mode 100644
index 0000000..35476b4
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
@@ -0,0 +1,381 @@
+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"
+       "context"
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/x509"
+       "database/sql"
+       "encoding/pem"
+       "errors"
+       "net/http"
+       "strconv"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+       "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/auth"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+
+       "github.com/go-acme/lego/certcrypto"
+       "github.com/go-acme/lego/certificate"
+       "github.com/go-acme/lego/challenge"
+       "github.com/go-acme/lego/lego"
+       "github.com/go-acme/lego/registration"
+       "github.com/jmoiron/sqlx"
+)
+
+const validAccountStatus = "valid"
+
+func RenewAcmeCertificate(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"xmlid"}, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+       if inf.Config.RiakEnabled == false {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, 
userErr, errors.New("deliveryservice.DeleteSSLKeys: Riak is not configured"))
+               return
+       }
+       xmlID := inf.Params["xmlid"]
+
+       if userErr, sysErr, errCode := tenant.Check(inf.User, xmlID, 
inf.Tx.Tx); userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+
+       ctx, _ := context.WithTimeout(r.Context(), LetsEncryptTimeout)
+
+       userErr, sysErr, statusCode := renewAcmeCerts(inf.Config, xmlID, ctx, 
inf.User)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
+       }
+
+       api.WriteRespAlert(w, r, tc.SuccessLevel, "Certificate for "+xmlID+" 
successfully renewed.")
+
+}
+
+func renewAcmeCerts(cfg *config.Config, dsName string, ctx context.Context, 
currentUser *auth.CurrentUser) (error, error, int) {
+       db, err := api.GetDB(ctx)
+       if err != nil {
+               log.Errorf(dsName+": Error getting db: %s", err.Error())
+               return nil, err, http.StatusInternalServerError
+       }
+
+       tx, err := db.Begin()
+       if err != nil {
+               log.Errorf(dsName+": Error getting tx: %s", err.Error())
+               return nil, err, http.StatusInternalServerError
+       }
+
+       userTx, err := db.Begin()
+       if err != nil {
+               log.Errorf(dsName+": Error getting userTx: %s", err.Error())
+               return nil, err, http.StatusInternalServerError
+       }
+       defer userTx.Commit()
+
+       logTx, err := db.Begin()
+       if err != nil {
+               log.Errorf(dsName+": Error getting logTx: %s", err.Error())
+               return nil, err, http.StatusInternalServerError
+       }
+       defer logTx.Commit()
+
+       dsID, certVersion, err := getDSIdAndVersionFromName(db, dsName)
+       if err != nil {
+               return nil, errors.New("querying DS info: " + err.Error()), 
http.StatusInternalServerError
+       }
+       if dsID == nil || *dsID == 0 {
+               return errors.New("DS id for " + dsName + " was nil or 0"), 
nil, http.StatusBadRequest
+       }
+       if certVersion == nil || *certVersion == 0 {
+               return errors.New("certificate for " + dsName + " could not be 
renewed because version was nil or 0"), nil, http.StatusBadRequest
+       }
+
+       if cfg == nil {
+               return nil, errors.New("acme: config was nil"), 
http.StatusInternalServerError
+       }
+       keyObj, ok, err := riaksvc.GetDeliveryServiceSSLKeysObjV15(dsName, 
strconv.Itoa(int(*certVersion)), tx, cfg.RiakAuthOptions, cfg.RiakPort)
+       if err != nil {
+               return nil, errors.New("getting ssl keys for xmlId: " + dsName 
+ " and version: " + strconv.Itoa(int(*certVersion)) + " : " + err.Error()), 
http.StatusInternalServerError
+       }
+       if !ok {
+               return nil, errors.New("no object found for the specified key 
with xmlId: " + dsName + " and version: " + strconv.Itoa(int(*certVersion))), 
http.StatusInternalServerError
+       }
+
+       err = base64DecodeCertificate(&keyObj.Certificate)
+       if err != nil {
+               return nil, errors.New("decoding cert for XMLID " + dsName + " 
: " + err.Error()), http.StatusInternalServerError
+       }
+
+       acmeAccount := getAcmeAccountConfig(cfg, keyObj.AuthType)
+       if acmeAccount == nil {
+               return nil, errors.New("No acme account information in cdn.conf 
for " + keyObj.AuthType), http.StatusInternalServerError
+       }
+
+       client, err := GetAcmeClient(acmeAccount, userTx, db)
+       if err != nil {
+               log.Errorf(dsName+": Error getting acme client: %s", 
err.Error())
+               api.CreateChangeLogRawTx(api.ApiChange, "DS: "+dsName+", ID: 
"+strconv.Itoa(*dsID)+", ACTION: FAILED to add SSL keys with 
"+acmeAccount.AcmeProvider, currentUser, logTx)
+               return nil, errors.New("getting acme client: " + err.Error()), 
http.StatusInternalServerError
+       }
+
+       renewRequest := certificate.Resource{
+               Certificate: []byte(keyObj.Certificate.Crt),
+       }
+
+       cert, err := client.Certificate.Renew(renewRequest, true, false)
+       if err != nil || cert == nil {
+               log.Errorf("Error obtaining acme certificate: %s", err.Error())
+               return nil, err, http.StatusInternalServerError
+       }
+
+       newCertObj := tc.DeliveryServiceSSLKeys{
+               AuthType:        keyObj.AuthType,
+               CDN:             keyObj.CDN,
+               DeliveryService: keyObj.DeliveryService,
+               Key:             keyObj.DeliveryService,
+               Hostname:        keyObj.Hostname,
+               Version:         keyObj.Version + 1,
+       }
+
+       newCertObj.Certificate = tc.DeliveryServiceSSLKeysCertificate{
+               Crt: string(EncodePEMToLegacyPerlRiakFormat(cert.Certificate)),
+               Key: string(EncodePEMToLegacyPerlRiakFormat(cert.PrivateKey)),
+               CSR: string(EncodePEMToLegacyPerlRiakFormat([]byte("ACME 
Generated"))),
+       }
+
+       if err := riaksvc.PutDeliveryServiceSSLKeysObj(newCertObj, tx, 
cfg.RiakAuthOptions, cfg.RiakPort); err != nil {
+               log.Errorf("Error posting acme certificate to riak: %s", 
err.Error())
+               api.CreateChangeLogRawTx(api.ApiChange, "DS: "+dsName+", ID: 
"+strconv.Itoa(*dsID)+", ACTION: FAILED to add SSL keys with 
"+acmeAccount.AcmeProvider, currentUser, logTx)
+               return nil, errors.New(dsName + ": putting riak keys: " + 
err.Error()), http.StatusInternalServerError
+       }
+
+       tx2, err := db.Begin()
+       if err != nil {
+               log.Errorf("starting sql transaction for delivery service " + 
dsName + ": " + err.Error())
+               return nil, errors.New("starting sql transaction for delivery 
service " + dsName + ": " + err.Error()), http.StatusInternalServerError
+       }
+
+       if err := updateSSLKeyVersion(dsName, *certVersion+1, tx2); err != nil {
+               log.Errorf("updating SSL key version for delivery service '" + 
dsName + "': " + err.Error())
+               return nil, errors.New("updating SSL key version for delivery 
service '" + dsName + "': " + err.Error()), http.StatusInternalServerError
+       }
+       tx2.Commit()
+
+       api.CreateChangeLogRawTx(api.ApiChange, "DS: "+dsName+", ID: 
"+strconv.Itoa(*dsID)+", ACTION: Added SSL keys with 
"+acmeAccount.AcmeProvider, currentUser, logTx)
+
+       return nil, nil, http.StatusOK
+}
+
+func getAcmeAccountConfig(cfg *config.Config, acmeProvider string) 
*config.ConfigAcmeAccount {
+       for _, acmeCfg := range cfg.AcmeAccounts {
+               if acmeCfg.AcmeProvider == acmeProvider {
+                       return &acmeCfg
+               }
+       }
+       return nil
+}
+
+func getDSIdAndVersionFromName(db *sqlx.DB, xmlId string) (*int, *int64, 
error) {
+       var dsID int
+       var certVersion int64
+
+       if err := db.QueryRow(`SELECT id, ssl_key_version FROM deliveryservice 
WHERE xml_id = $1`, xmlId).Scan(&dsID, &certVersion); err != nil {
+               return nil, nil, err
+       }
+
+       return &dsID, &certVersion, nil
+}
+
+// GetAcmeClient uses the ACME account information in either cdn.conf or the 
database to create and register an ACME client
+func GetAcmeClient(acmeAccount *config.ConfigAcmeAccount, userTx *sql.Tx, db 
*sqlx.DB) (*lego.Client, error) {
+       if acmeAccount.UserEmail == "" {
+               log.Errorf("An email address must be provided to use ACME with 
%v", acmeAccount.AcmeProvider)
+               return nil, errors.New("An email address must be provided to 
use ACME with " + acmeAccount.AcmeProvider)
+       }
+       storedAcmeInfo, err := getStoredAcmeAccountInfo(userTx, 
acmeAccount.UserEmail, acmeAccount.AcmeProvider)
+       if err != nil {
+               log.Errorf("Error finding stored ACME information: %s", 
err.Error())
+               return nil, err
+       }
+
+       myUser := MyUser{}
+       foundPreviousAccount := false
+       userPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+       if err != nil {
+               log.Errorf("Error generating private key: %s", err.Error())
+               return nil, err
+       }
+
+       if storedAcmeInfo == nil || acmeAccount.UserEmail == "" {
+               myUser = MyUser{
+                       key:   userPrivateKey,
+                       Email: acmeAccount.UserEmail,
+               }
+       } else {
+               foundPreviousAccount = true
+               myUser = MyUser{
+                       key:   &storedAcmeInfo.PrivateKey,
+                       Email: storedAcmeInfo.Email,
+                       Registration: &registration.Resource{
+                               URI: storedAcmeInfo.URI,
+                       },
+               }
+       }
+
+       config := lego.NewConfig(&myUser)
+       config.CADirURL = acmeAccount.AcmeUrl
+       config.Certificate.KeyType = certcrypto.RSA2048
+
+       client, err := lego.NewClient(config)
+       if err != nil {
+               log.Errorf("Error creating acme client: %s", err.Error())
+               return nil, err
+       }
+
+       if acmeAccount.AcmeProvider == tc.LetsEncryptAuthType {
+               client.Challenge.Remove(challenge.HTTP01)
+               client.Challenge.Remove(challenge.TLSALPN01)
+               trafficRouterDns := NewDNSProviderTrafficRouter()
+               trafficRouterDns.db = db
+               if err != nil {
+                       log.Errorf("Error creating Traffic Router DNS provider: 
%s", err.Error())
+                       return nil, err
+               }
+               client.Challenge.SetDNS01Provider(trafficRouterDns)
+       }
+
+       if foundPreviousAccount {
+               log.Debugf("Found existing account with %s", 
acmeAccount.AcmeProvider)
+               reg, err := client.Registration.QueryRegistration()
+               if err != nil {
+                       log.Errorf("Error querying %s for existing account: 
%s", acmeAccount.AcmeProvider, err.Error())
+                       return nil, err
+               }
+               myUser.Registration = reg
+               if reg.Body.Status != validAccountStatus {
+                       log.Debugf("Account found with %s is not valid.", 
acmeAccount.AcmeProvider)
+                       foundPreviousAccount = false
+               }
+       }
+       if !foundPreviousAccount {
+               if acmeAccount.Kid != "" && acmeAccount.HmacEncoded != "" {
+                       reg, err := 
client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
+                               TermsOfServiceAgreed: true,
+                               Kid:                  acmeAccount.Kid,
+                               HmacEncoded:          acmeAccount.HmacEncoded,
+                       })
+                       if err != nil {
+                               log.Errorf("Error registering acme client with 
external account binding: %s", err.Error())
+                               return nil, err
+                       }
+                       myUser.Registration = reg
+                       log.Debugf("Creating a new account with %s", 
acmeAccount.AcmeProvider)
+               } else {
+                       reg, err := 
client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: 
true})
+                       if err != nil {
+                               log.Errorf("Error registering acme client: %s", 
err.Error())
+                               return nil, err
+                       }
+                       myUser.Registration = reg
+                       log.Debugf("Creating a new account with %s", 
acmeAccount.AcmeProvider)
+               }
+
+               // save account info
+               userKeyPem, err := ConvertPrivateKeyToKeyPem(userPrivateKey)
+               if err != nil {
+                       return nil, err
+               }
+               err = storeAcmeAccountInfo(userTx, myUser.Email, 
string(userKeyPem), myUser.Registration.URI, acmeAccount.AcmeProvider)
+               if err != nil {
+                       log.Errorf("storing user account info: " + err.Error())
+                       return nil, errors.New("storing user account info: " + 
err.Error())
+               }
+       }
+
+       return client, nil
+}
+
+func ConvertPrivateKeyToKeyPem(userPrivateKey *rsa.PrivateKey) ([]byte, error) 
{
+       userKeyDer := x509.MarshalPKCS1PrivateKey(userPrivateKey)
+       if userKeyDer == nil {
+               log.Errorf("marshalling private key: nil der")
+               return nil, errors.New("marshalling private key: nil der")
+       }
+       userKeyBuf := bytes.Buffer{}
+       if err := pem.Encode(&userKeyBuf, &pem.Block{Type: "RSA PRIVATE KEY", 
Bytes: userKeyDer}); err != nil {
+               log.Errorf("pem-encoding private key: " + err.Error())
+               return nil, errors.New("pem-encoding private key: " + 
err.Error())
+       }
+       return userKeyBuf.Bytes(), nil
+}
+
+func getStoredAcmeAccountInfo(tx *sql.Tx, email string, provider string) 
(*AcmeInfo, error) {
+       acmeInfo := AcmeInfo{}
+       selectQuery := `SELECT email, private_key, uri FROM acme_account WHERE 
email = $1 AND provider = $2 LIMIT 1`
+       if err := tx.QueryRow(selectQuery, email, 
provider).Scan(&acmeInfo.Email, &acmeInfo.Key, &acmeInfo.URI); err != nil {
+               if err == sql.ErrNoRows {
+                       return nil, nil
+               }
+               return nil, errors.New("getting lets encrypt account record: " 
+ err.Error())
+       }
+
+       decodedKeyBlock, _ := pem.Decode([]byte(acmeInfo.Key))
+       decodedKey, err := x509.ParsePKCS1PrivateKey(decodedKeyBlock.Bytes)
+       if err != nil {
+               return nil, errors.New("decoding private key for user account")
+       }
+       acmeInfo.PrivateKey = *decodedKey
+
+       return &acmeInfo, nil
+}
+
+func storeAcmeAccountInfo(tx *sql.Tx, email string, privateKey string, uri 
string, provider string) error {
+       q := `INSERT INTO acme_account (email, private_key, uri, provider) 
VALUES ($1, $2, $3, $4)`
+       response, err := tx.Exec(q, email, privateKey, uri, provider)
+       if err != nil {
+               return err
+       }
+
+       rows, err := response.RowsAffected()
+       if err != nil {
+               return err
+       }
+       if rows == 0 {
+               return errors.New("zero rows affected when inserting Let's 
Encrypt account information")
+       }
+
+       return nil
+}
+
+type AcmeInfo struct {
+       Email      string `db:"email"`
+       Key        string `db:"private_key"`
+       URI        string `db:"uri"`
+       PrivateKey rsa.PrivateKey
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go 
b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
index 8a3825d..287409d 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
@@ -91,7 +91,7 @@ func RenewCertificates(w http.ResponseWriter, r 
*http.Request) {
 
        go RunAutorenewal(existingCerts, inf.Config, ctx, inf.User)
 
-       api.WriteRespAlert(w, r, tc.InfoLevel, "Beginning async call to renew 
Let's Encrypt certificates.  This may take a few minutes.")
+       api.WriteRespAlert(w, r, tc.SuccessLevel, "Beginning async call to 
renew Let's Encrypt certificates.  This may take a few minutes.")
 
 }
 func RunAutorenewal(existingCerts []ExistingCerts, cfg *config.Config, ctx 
context.Context, currentUser *auth.CurrentUser) {
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go 
b/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
index 9f34f37..431f9ec 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/letsencryptcert.go
@@ -25,9 +25,6 @@ import (
        "crypto"
        "crypto/rand"
        "crypto/rsa"
-       "crypto/x509"
-       "database/sql"
-       "encoding/pem"
        "errors"
        "net/http"
        "strconv"
@@ -42,9 +39,8 @@ import (
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
-       "github.com/go-acme/lego/certcrypto"
+
        "github.com/go-acme/lego/certificate"
-       "github.com/go-acme/lego/challenge"
        "github.com/go-acme/lego/challenge/dns01"
        "github.com/go-acme/lego/lego"
        "github.com/go-acme/lego/registration"
@@ -183,7 +179,7 @@ func GenerateLetsEncryptCertificates(w http.ResponseWriter, 
r *http.Request) {
 
        go GetLetsEncryptCertificates(inf.Config, req, ctx, inf.User)
 
-       api.WriteRespAlert(w, r, tc.InfoLevel, "Beginning async call to Let's 
Encrypt for "+*req.DeliveryService+".  This may take a few minutes.")
+       api.WriteRespAlert(w, r, tc.SuccessLevel, "Beginning async call to 
Let's Encrypt for "+*req.DeliveryService+".  This may take a few minutes.")
 
 }
 
@@ -228,88 +224,28 @@ func GetLetsEncryptCertificates(cfg *config.Config, req 
tc.DeliveryServiceLetsEn
        }
        tx.Commit()
 
-       storedLEInfo, err := getStoredAcmeAccountInfo(userTx, 
cfg.ConfigLetsEncrypt.Email, tc.LetsEncryptAuthType)
-       if err != nil {
-               log.Errorf(deliveryService+": Error finding stored LE 
information: %s", err.Error())
+       if cfg == nil {
+               log.Errorf("lets encrypt: config was nil")
                api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return err
+               return errors.New("lets encrypt: config was nil")
        }
 
-       myUser := MyUser{}
-       foundPreviousAccount := false
-       userPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
-       if err != nil {
-               log.Errorf(deliveryService+": Error generating private key: 
%s", err.Error())
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return err
+       letsEncryptAccount := config.ConfigAcmeAccount{
+               UserEmail:    cfg.ConfigLetsEncrypt.Email,
+               AcmeProvider: tc.LetsEncryptAuthType,
        }
-       if storedLEInfo == nil || cfg.ConfigLetsEncrypt.Email == "" {
 
-               myUser = MyUser{
-                       key:   userPrivateKey,
-                       Email: cfg.ConfigLetsEncrypt.Email,
-               }
-       } else {
-               foundPreviousAccount = true
-               myUser = MyUser{
-                       key:   &storedLEInfo.PrivateKey,
-                       Email: cfg.ConfigLetsEncrypt.Email,
-                       Registration: &registration.Resource{
-                               URI: storedLEInfo.URI,
-                       },
-               }
-       }
-
-       config := lego.NewConfig(&myUser)
        if strings.EqualFold(cfg.ConfigLetsEncrypt.Environment, "staging") {
-               config.CADirURL = lego.LEDirectoryStaging // provides 
certificate signed by invalid authority for testing purposes
+               letsEncryptAccount.AcmeUrl = lego.LEDirectoryStaging // 
provides certificate signed by invalid authority for testing purposes
        } else {
-               config.CADirURL = lego.LEDirectoryProduction // provides 
certificate signed by valid LE authority
+               letsEncryptAccount.AcmeUrl = lego.LEDirectoryProduction // 
provides certificate signed by valid LE authority
        }
 
-       config.Certificate.KeyType = certcrypto.RSA2048
-
-       client, err := lego.NewClient(config)
+       client, err := GetAcmeClient(&letsEncryptAccount, userTx, db)
        if err != nil {
-               log.Errorf(deliveryService+": Error creating lets encrypt 
client: %s", err.Error())
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return err
-       }
-
-       client.Challenge.Remove(challenge.HTTP01)
-       client.Challenge.Remove(challenge.TLSALPN01)
-       trafficRouterDns := NewDNSProviderTrafficRouter()
-       trafficRouterDns.db = db
-       if err != nil {
-               log.Errorf(deliveryService+": Error creating Traffic Router DNS 
provider: %s", err.Error())
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return err
-       }
-       client.Challenge.SetDNS01Provider(trafficRouterDns)
-
-       if foundPreviousAccount {
-               log.Debugf("Found existing account with Let's Encrypt")
-               reg, err := client.Registration.QueryRegistration()
-               if err != nil {
-                       log.Errorf(deliveryService+": Error querying Lets 
Encrypt for existing account: %s", err.Error())
-                       api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-                       return err
-               }
-               myUser.Registration = reg
-               if reg.Body.Status != "valid" {
-                       log.Debugf("Account found with Let's Encrypt is not 
valid.")
-                       foundPreviousAccount = false
-               }
-       }
-       if !foundPreviousAccount {
-               reg, err := 
client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: 
true})
-               if err != nil {
-                       log.Errorf(deliveryService+": Error registering lets 
encrypt client: %s", err.Error())
-                       api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-                       return err
-               }
-               myUser.Registration = reg
-               log.Debugf("Creating a new account with Let's Encrypt")
+               log.Errorf(deliveryService+": Error getting acme client: %s", 
err.Error())
+               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+deliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL keys 
with "+letsEncryptAccount.AcmeProvider, currentUser, logTx)
+               return errors.New("getting acme client: " + err.Error())
        }
 
        priv, err := rsa.GenerateKey(rand.Reader, 2048)
@@ -341,24 +277,22 @@ func GetLetsEncryptCertificates(cfg *config.Config, req 
tc.DeliveryServiceLetsEn
                Version:         *req.Version,
        }
 
-       keyDer := x509.MarshalPKCS1PrivateKey(priv)
-       if keyDer == nil {
-               log.Errorf("marshalling private key: nil der")
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return 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 {
-               log.Errorf("pem-encoding private key: " + err.Error())
+       keyPem, err := ConvertPrivateKeyToKeyPem(priv)
+       if err != nil {
+               log.Errorf(err.Error())
                api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return errors.New("pem-encoding private key: " + err.Error())
+               return err
        }
-       keyPem := keyBuf.Bytes()
 
        // remove extra line if LE returns it
        trimmedCert := bytes.ReplaceAll(certificates.Certificate, 
[]byte("\n\n"), []byte("\n"))
 
-       dsSSLKeys.Certificate = tc.DeliveryServiceSSLKeysCertificate{Crt: 
string(EncodePEMToLegacyPerlRiakFormat(trimmedCert)), Key: 
string(EncodePEMToLegacyPerlRiakFormat(keyPem)), CSR: ""}
+       dsSSLKeys.Certificate = tc.DeliveryServiceSSLKeysCertificate{
+               Crt: string(EncodePEMToLegacyPerlRiakFormat(trimmedCert)),
+               Key: string(EncodePEMToLegacyPerlRiakFormat(keyPem)),
+               CSR: string(EncodePEMToLegacyPerlRiakFormat([]byte("Lets 
Encrypt Generated"))),
+       }
+
        if err := riaksvc.PutDeliveryServiceSSLKeysObj(dsSSLKeys, tx, 
cfg.RiakAuthOptions, cfg.RiakPort); err != nil {
                log.Errorf("Error posting lets encrypt certificate to riak: 
%s", err.Error())
                api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
@@ -377,77 +311,7 @@ func GetLetsEncryptCertificates(cfg *config.Config, req 
tc.DeliveryServiceLetsEn
        }
        tx2.Commit()
 
-       if foundPreviousAccount {
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: Added SSL keys 
with Lets Encrypt", currentUser, logTx)
-               return nil
-       }
-
-       userKeyDer := x509.MarshalPKCS1PrivateKey(userPrivateKey)
-       if userKeyDer == nil {
-               log.Errorf("marshalling private key: nil der")
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return errors.New("marshalling private key: nil der")
-       }
-       userKeyBuf := bytes.Buffer{}
-       if err := pem.Encode(&userKeyBuf, &pem.Block{Type: "RSA PRIVATE KEY", 
Bytes: userKeyDer}); err != nil {
-               log.Errorf("pem-encoding private key: " + err.Error())
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return errors.New("pem-encoding private key: " + err.Error())
-       }
-       userKeyPem := userKeyBuf.Bytes()
-       err = storeAcmeAccountInfo(userTx, myUser.Email, string(userKeyPem), 
myUser.Registration.URI, tc.LetsEncryptAuthType)
-       if err != nil {
-               log.Errorf("storing user account info: " + err.Error())
-               api.CreateChangeLogRawTx(api.ApiChange, "DS: 
"+*req.DeliveryService+", ID: "+strconv.Itoa(dsID)+", ACTION: FAILED to add SSL 
keys with Lets Encrypt", currentUser, logTx)
-               return errors.New("storing user account info: " + err.Error())
-       }
-
        api.CreateChangeLogRawTx(api.ApiChange, "DS: "+*req.DeliveryService+", 
ID: "+strconv.Itoa(dsID)+", ACTION: Added SSL keys with Lets Encrypt", 
currentUser, logTx)
 
        return nil
 }
-
-func getStoredAcmeAccountInfo(tx *sql.Tx, email string, provider string) 
(*LEInfo, error) {
-       leInfo := LEInfo{}
-       selectQuery := `SELECT email, private_key, uri FROM acme_account WHERE 
email = $1 AND provider = $2 LIMIT 1`
-       if err := tx.QueryRow(selectQuery, email, provider).Scan(&leInfo.Email, 
&leInfo.Key, &leInfo.URI); err != nil {
-               if err == sql.ErrNoRows {
-                       return nil, nil
-               }
-               return nil, errors.New("getting lets encrypt account record: " 
+ err.Error())
-       }
-
-       decodedKeyBlock, _ := pem.Decode([]byte(leInfo.Key))
-       decodedKey, err := x509.ParsePKCS1PrivateKey(decodedKeyBlock.Bytes)
-       if err != nil {
-               return nil, errors.New("decoding private key for user account")
-       }
-       leInfo.PrivateKey = *decodedKey
-
-       return &leInfo, nil
-}
-
-func storeAcmeAccountInfo(tx *sql.Tx, email string, privateKey string, uri 
string, provider string) error {
-       q := `INSERT INTO acme_account (email, private_key, uri, provider) 
VALUES ($1, $2, $3, $4)`
-       response, err := tx.Exec(q, email, privateKey, uri, provider)
-       if err != nil {
-               return err
-       }
-
-       rows, err := response.RowsAffected()
-       if err != nil {
-               return err
-       }
-       if rows == 0 {
-               return errors.New("zero rows affected when inserting Let's 
Encrypt account information")
-       }
-
-       return nil
-}
-
-type LEInfo struct {
-       Email      string `db:"email"`
-       Key        string `db:"private_key"`
-       URI        string `db:"uri"`
-       PrivateKey rsa.PrivateKey
-}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index e97dfa3..ef76227 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -145,12 +145,15 @@ func Routes(d ServerData) ([]Route, []RawRoute, 
http.Handler, error) {
                /**
                 * 4.x API
                 */
-               // Acme account information
+               // ACME account information
                {api.Version{4, 0}, http.MethodGet, `acme_accounts/?$`, 
acme.Read, auth.PrivLevelAdmin, Authenticated, nil, 4034390561, noPerlBypass},
                {api.Version{4, 0}, http.MethodPost, `acme_accounts/?$`, 
acme.Create, auth.PrivLevelAdmin, Authenticated, nil, 4034390562, noPerlBypass},
                {api.Version{4, 0}, http.MethodPut, `acme_accounts/?$`, 
acme.Update, auth.PrivLevelAdmin, Authenticated, nil, 4034390563, noPerlBypass},
                {api.Version{4, 0}, http.MethodDelete, 
`acme_accounts/{provider}/{email}?$`, acme.Delete, auth.PrivLevelAdmin, 
Authenticated, nil, 4034390564, noPerlBypass},
 
+               //Delivery service ACME
+               {api.Version{4, 0}, http.MethodPost, 
`deliveryservices/xmlId/{xmlid}/sslkeys/renew$`, 
deliveryservice.RenewAcmeCertificate, auth.PrivLevelOperations, Authenticated, 
nil, 2534390573, noPerlBypass},
+
                // API Capability
                {api.Version{4, 0}, http.MethodGet, `api_capabilities/?$`, 
apicapability.GetAPICapabilitiesHandler, auth.PrivLevelReadOnly, Authenticated, 
nil, 48132065893, noPerlBypass},
 
diff --git a/traffic_portal/app/src/common/api/DeliveryServiceSslKeysService.js 
b/traffic_portal/app/src/common/api/DeliveryServiceSslKeysService.js
index 56ee125..b157593 100644
--- a/traffic_portal/app/src/common/api/DeliveryServiceSslKeysService.js
+++ b/traffic_portal/app/src/common/api/DeliveryServiceSslKeysService.js
@@ -54,6 +54,21 @@ var DeliveryServiceSslKeysService = function($http, 
locationUtils, messageModel,
         );
        };
 
+       this.renewCert = function(deliveryService) {
+               return $http.post(ENV.api['root'] + "deliveryservices/xmlId/" + 
deliveryService.xmlId + "/sslkeys/renew").then(
+                       function(result) {
+                               messageModel.setMessages(result.data.alerts, 
false);
+                               return result.data.response;
+                       },
+                       function(err) {
+                               if (err.data && err.data.alerts) {
+                                       
messageModel.setMessages(err.data.alerts, false);
+                               }
+                               throw err;
+                       }
+               );
+       };
+
        this.addSslKeys = function(sslKeys, deliveryService) {
 
         sslKeys.key = deliveryService.xmlId;
diff --git 
a/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/FormDeliveryServiceSslKeysController.js
 
b/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/FormDeliveryServiceSslKeysController.js
index c00aa5c..ed8d68e 100644
--- 
a/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/FormDeliveryServiceSslKeysController.js
+++ 
b/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/FormDeliveryServiceSslKeysController.js
@@ -49,6 +49,29 @@ var FormDeliveryServiceSslKeysController = 
function(deliveryService, sslKeys, $s
                locationUtils.navigateToPath('/delivery-services/' + 
deliveryService.id + '/ssl-keys/generate');
        };
 
+       $scope.renewCert = function() {
+               var params = {
+                       title: 'Renew SSL Keys for Delivery Service: ' + 
deliveryService.xmlId
+               };
+               var modalInstance = $uibModal.open({
+                       templateUrl: 
'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+                       controller: 'DialogConfirmController',
+                       size: 'md',
+                       resolve: {
+                               params: function () {
+                                       return params;
+                               }
+                       }
+               });
+               modalInstance.result.then(function() {
+                       
deliveryServiceSslKeysService.renewCert(deliveryService).then(
+                               function() {
+                                       $anchorScroll();
+                                       if ($scope.dsSslKeyForm) 
$scope.dsSslKeyForm.$setPristine();
+                               });
+               });
+       };
+
        $scope.save = function() {
                var params = {
                        title: 'Add New SSL Keys for Delivery Service: ' + 
deliveryService.xmlId
diff --git 
a/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/form.deliveryServiceSslKeys.tpl.html
 
b/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/form.deliveryServiceSslKeys.tpl.html
index c629b4f..3c53e83 100644
--- 
a/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/form.deliveryServiceSslKeys.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/form/deliveryServiceSslKeys/form.deliveryServiceSslKeys.tpl.html
@@ -32,6 +32,7 @@ under the License.
                 </button>
                 <ul class="dropdown-menu-right dropdown-menu" 
uib-dropdown-menu>
                     <li><a ng-click="generateKeys()">Generate SSL Keys</a></li>
+                    <li><a ng-click="renewCert()">Renew Certificate</a></li>
                 </ul>
             </div>
         </div>

Reply via email to