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: ®istration.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: ®istration.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>