You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2021/03/26 17:53:23 UTC

[GitHub] [trafficcontrol] rawlinp commented on a change in pull request #5682: Added ACME integration to create new certificates

rawlinp commented on a change in pull request #5682:
URL: https://github.com/apache/trafficcontrol/pull/5682#discussion_r602484445



##########
File path: traffic_ops/traffic_ops_golang/deliveryservice/acme.go
##########
@@ -22,178 +22,408 @@ package deliveryservice
 import (
 	"bytes"
 	"context"
+	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/x509"
 	"database/sql"
 	"encoding/pem"
 	"errors"
+	"github.com/apache/trafficcontrol/lib/go-util"
 	"net/http"
 	"strconv"
+	"strings"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/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/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"
 	"github.com/jmoiron/sqlx"
 )
 
 const validAccountStatus = "valid"
+const AcmeTimeout = time.Minute * 20
+const API_ACME_GENERATE_LE = "/deliveryservices/sslkeys/generate/acme"
+
+// MyUser stores the user's information for use in ACME protocol.
+type MyUser struct {
+	Email        string
+	Registration *registration.Resource
+	key          crypto.PrivateKey
+}
+
+// GetEmail returns a user's email for use in ACME protocol.
+func (u *MyUser) GetEmail() string {
+	return u.Email
+}
+
+// GetRegistration returns a user's registration for use in ACME protocol.
+func (u MyUser) GetRegistration() *registration.Resource {
+	return u.Registration
+}
+
+// GetPrivateKey returns a user's private key for use in ACME protocol.
+func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
+	return u.key
+}
+
+// DNSProviderTrafficRouter is used in the lego library and contains a database in order to store the DNS challenges for ACME protocol.
+type DNSProviderTrafficRouter struct {
+	db *sqlx.DB
+}
+
+// NewDNSProviderTrafficRouter returns a new DNSProviderTrafficRouter object.
+func NewDNSProviderTrafficRouter() *DNSProviderTrafficRouter {
+	return &DNSProviderTrafficRouter{}
+}
+
+// Timeout returns timeout information for the lego library including the timeout duration and the interval between checks.
+func (d *DNSProviderTrafficRouter) Timeout() (timeout, interval time.Duration) {
+	return AcmeTimeout, time.Second * 30
+}
+
+// Present inserts the DNS challenge record into the database to be used by Traffic Router. This is used in the lego library.
+func (d *DNSProviderTrafficRouter) Present(domain, token, keyAuth string) error {
+	tx, err := d.db.Begin()
+	fqdn, value := dns01.GetRecord(domain, keyAuth)
+
+	q := `INSERT INTO dnschallenges (fqdn, record) VALUES ($1, $2)`
+	response, err := tx.Exec(q, fqdn, value)
+	tx.Commit()
+	if err != nil {
+		log.Errorf("Inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		return errors.New("Inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+	} else {
+		rows, err := response.RowsAffected()
+		if err != nil {
+			log.Errorf("Determining rows affected dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+			return errors.New("Determining rows affected dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		}
+		if rows == 0 {
+			log.Errorf("Zero rows affected when inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+			return errors.New("Zero rows affected when inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		}
+	}
 
-// RenewAcmeCertificate renews the SSL certificate for a delivery service if possible through ACME protocol.
-func RenewAcmeCertificate(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"xmlid"}, nil)
+	return nil
+}
+
+// CleanUp removes the DNS challenge record from the database after the challenge has completed. This is used in the lego library.
+func (d *DNSProviderTrafficRouter) CleanUp(domain, token, keyAuth string) error {
+	fqdn, value := dns01.GetRecord(domain, keyAuth)
+	tx, err := d.db.Begin()
+
+	q := `DELETE FROM dnschallenges WHERE fqdn = $1 and record = $2`
+	response, err := tx.Exec(q, fqdn, value)
+	tx.Commit()
+	if err != nil {
+		log.Errorf("Deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		return errors.New("Deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+	} else {
+		rows, err := response.RowsAffected()
+		if err != nil {
+			log.Errorf("Determining rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+			return errors.New("Determining rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		}
+		if rows == 0 {
+			log.Errorf("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value)
+			return errors.New("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value)
+		}
+	}
+
+	return nil
+}
+
+// GenerateAcmeCertificates gets and saves certificates using ACME protocol from a give ACME provider.
+func GenerateAcmeCertificates(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)

Review comment:
       This handler should return a 500 if Traffic Vault is not configured

##########
File path: traffic_ops/traffic_ops_golang/deliveryservice/acme.go
##########
@@ -22,178 +22,408 @@ package deliveryservice
 import (
 	"bytes"
 	"context"
+	"crypto"
 	"crypto/rand"
 	"crypto/rsa"
 	"crypto/x509"
 	"database/sql"
 	"encoding/pem"
 	"errors"
+	"github.com/apache/trafficcontrol/lib/go-util"
 	"net/http"
 	"strconv"
+	"strings"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/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/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"
 	"github.com/jmoiron/sqlx"
 )
 
 const validAccountStatus = "valid"
+const AcmeTimeout = time.Minute * 20
+const API_ACME_GENERATE_LE = "/deliveryservices/sslkeys/generate/acme"
+
+// MyUser stores the user's information for use in ACME protocol.
+type MyUser struct {
+	Email        string
+	Registration *registration.Resource
+	key          crypto.PrivateKey
+}
+
+// GetEmail returns a user's email for use in ACME protocol.
+func (u *MyUser) GetEmail() string {
+	return u.Email
+}
+
+// GetRegistration returns a user's registration for use in ACME protocol.
+func (u MyUser) GetRegistration() *registration.Resource {
+	return u.Registration
+}
+
+// GetPrivateKey returns a user's private key for use in ACME protocol.
+func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
+	return u.key
+}
+
+// DNSProviderTrafficRouter is used in the lego library and contains a database in order to store the DNS challenges for ACME protocol.
+type DNSProviderTrafficRouter struct {
+	db *sqlx.DB
+}
+
+// NewDNSProviderTrafficRouter returns a new DNSProviderTrafficRouter object.
+func NewDNSProviderTrafficRouter() *DNSProviderTrafficRouter {
+	return &DNSProviderTrafficRouter{}
+}
+
+// Timeout returns timeout information for the lego library including the timeout duration and the interval between checks.
+func (d *DNSProviderTrafficRouter) Timeout() (timeout, interval time.Duration) {
+	return AcmeTimeout, time.Second * 30
+}
+
+// Present inserts the DNS challenge record into the database to be used by Traffic Router. This is used in the lego library.
+func (d *DNSProviderTrafficRouter) Present(domain, token, keyAuth string) error {
+	tx, err := d.db.Begin()
+	fqdn, value := dns01.GetRecord(domain, keyAuth)
+
+	q := `INSERT INTO dnschallenges (fqdn, record) VALUES ($1, $2)`
+	response, err := tx.Exec(q, fqdn, value)
+	tx.Commit()
+	if err != nil {
+		log.Errorf("Inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		return errors.New("Inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+	} else {
+		rows, err := response.RowsAffected()
+		if err != nil {
+			log.Errorf("Determining rows affected dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+			return errors.New("Determining rows affected dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		}
+		if rows == 0 {
+			log.Errorf("Zero rows affected when inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+			return errors.New("Zero rows affected when inserting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		}
+	}
 
-// RenewAcmeCertificate renews the SSL certificate for a delivery service if possible through ACME protocol.
-func RenewAcmeCertificate(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"xmlid"}, nil)
+	return nil
+}
+
+// CleanUp removes the DNS challenge record from the database after the challenge has completed. This is used in the lego library.
+func (d *DNSProviderTrafficRouter) CleanUp(domain, token, keyAuth string) error {
+	fqdn, value := dns01.GetRecord(domain, keyAuth)
+	tx, err := d.db.Begin()
+
+	q := `DELETE FROM dnschallenges WHERE fqdn = $1 and record = $2`
+	response, err := tx.Exec(q, fqdn, value)
+	tx.Commit()
+	if err != nil {
+		log.Errorf("Deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		return errors.New("Deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+	} else {
+		rows, err := response.RowsAffected()
+		if err != nil {
+			log.Errorf("Determining rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+			return errors.New("Determining rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value + "': " + err.Error())
+		}
+		if rows == 0 {
+			log.Errorf("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value)
+			return errors.New("Zero rows affected when deleting dns txt record for fqdn '" + fqdn + "' record '" + value)
+		}
+	}
+
+	return nil
+}
+
+// GenerateAcmeCertificates gets and saves certificates using ACME protocol from a give ACME provider.
+func GenerateAcmeCertificates(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, 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"))
+
+	ctx, _ := context.WithTimeout(r.Context(), AcmeTimeout)
+
+	req := tc.DeliveryServiceLetsEncryptSSLKeysReq{}
+	if err := api.Parse(r.Body, nil, &req); err != nil {
+		api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("parsing request: "+err.Error()), nil)
+		return
+	}
+	if *req.DeliveryService == "" {
+		req.DeliveryService = req.Key
+	}
+
+	dsID, cdnName, ok, err := dbhelpers.GetDSIDAndCDNFromName(inf.Tx.Tx, *req.DeliveryService)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deliveryservice.GenerateLetsEncryptCertificates: getting DS ID from name "+err.Error()))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("no DS with name "+*req.DeliveryService), nil)
 		return
 	}
-	xmlID := inf.Params["xmlid"]
 
-	if userErr, sysErr, errCode := tenant.Check(inf.User, xmlID, inf.Tx.Tx); userErr != nil || sysErr != nil {
+	userErr, sysErr, errCode = tenant.CheckID(inf.Tx.Tx, inf.User, dsID)
+	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
 
-	ctx, _ := context.WithTimeout(r.Context(), LetsEncryptTimeout)
+	_, ok, err = dbhelpers.GetCDNIDFromName(inf.Tx.Tx, tc.CDNName(*req.CDN))
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking CDN existence: "+err.Error()))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("cdn not found with name "+*req.CDN), nil)
+		return
+	}
+
+	if cdnName != tc.CDNName(*req.CDN) {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("delivery service not in cdn"), nil)
+		return
+	}
 
-	userErr, sysErr, statusCode := renewAcmeCerts(inf.Config, xmlID, ctx, inf.User)
+	go GetAcmeCertificates(inf.Config, req, ctx, inf.User)
+
+	api.WriteRespAlert(w, r, tc.SuccessLevel, "Beginning async ACME call for "+*req.DeliveryService+" using "+*req.AuthType+". This may take a few minutes.")
+}
+
+// GenerateLetsEncryptCertificates gets and saves new certificates from Let's Encrypt.
+func GenerateLetsEncryptCertificates(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()

Review comment:
       This handler should return a 500 if Traffic Vault is not configured




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org