feat: add powerdns config, domain check, api changes
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Paul 2024-04-19 16:50:57 +02:00
parent dadb907740
commit 90bfc25975
8 changed files with 136 additions and 62 deletions

View File

@ -2,10 +2,13 @@ package cert
import "time"
func (e *Entry) Z() {
}
// Entry is the main struct for stored certificates
type Entry struct {
ID int `xorm:"pk autoincr"`
Domains string `xorm:"notnull"`
Domain string `xorm:"notnull"`
Certificate string `xorm:"text notnull"`
PrivateKey string `xorm:"text notnull"`
AuthURL string `xorm:"notnull"`

View File

@ -60,10 +60,13 @@ func (cfg *Config) GetConfig() error {
options["ovhas"] = pkisection.Key("ovhas").MustString("")
options["ovhck"] = pkisection.Key("ovhck").MustString("")
options["pdnsapiurl"] = pkisection.Key("pdnsapiurl").MustString("")
options["pdnsapikey"] = pkisection.Key("pdnsapikey").MustString("")
cfg.ACME.ProviderOptions = options
for k, v := range options {
if v == "" {
utils.Advice(fmt.Sprintf("OVH provider parameter %s not set", k))
for key, value := range options {
if value == "" {
utils.Advice(fmt.Sprintf("Provider parameter %s not set", key))
}
}
@ -72,6 +75,8 @@ func (cfg *Config) GetConfig() error {
cfg.ACME.AuthURL = lego.LEDirectoryProduction
case "staging":
cfg.ACME.AuthURL = lego.LEDirectoryStaging
default:
cfg.ACME.AuthURL = lego.LEDirectoryStaging
}
return nil

View File

@ -7,6 +7,7 @@ import (
"git.paulbsd.com/paulbsd/pki/src/cert"
"git.paulbsd.com/paulbsd/pki/src/config"
"git.paulbsd.com/paulbsd/pki/src/domain"
"git.paulbsd.com/paulbsd/pki/src/pki"
_ "github.com/lib/pq"
"xorm.io/xorm"
@ -17,7 +18,7 @@ import (
func Init(cfg *config.Config) (err error) {
var databaseEngine = "postgres"
tables := []interface{}{cert.Entry{},
pki.User{}}
pki.User{}, domain.Domain{}}
cfg.Db, err = xorm.NewEngine(databaseEngine,
fmt.Sprintf("%s://%s:%s@%s/%s",

12
src/domain/main.go Normal file
View File

@ -0,0 +1,12 @@
package domain
import "time"
// Domain describes a domain
type Domain struct {
ID int `xorm:"pk autoincr"`
Domain string `xorm:"text notnull unique(domain_provider)"`
Provider string `xorm:"text notnull unique(domain_provider)"`
Created time.Time `xorm:"created notnull"`
Updated time.Time `xorm:"updated notnull"`
}

View File

@ -9,12 +9,13 @@ import (
"encoding/pem"
"fmt"
"log"
"strings"
"git.paulbsd.com/paulbsd/pki/src/cert"
"git.paulbsd.com/paulbsd/pki/src/config"
"git.paulbsd.com/paulbsd/pki/src/domain"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
)
@ -29,9 +30,8 @@ func (u *User) Init(cfg *config.Config) (err error) {
}
// GetEntry returns requested acme ressource in database relative to domain
func (u *User) GetEntry(cfg *config.Config, domains []string) (Entry cert.Entry, err error) {
has, err := cfg.Db.Where("domains = ?", strings.Join(domains, ",")).And(
func (u *User) GetEntry(cfg *config.Config, domain *string) (Entry cert.Entry, err error) {
has, err := cfg.Db.Where("domain = ?", domain).And(
"auth_url = ?", cfg.ACME.AuthURL).And(
fmt.Sprintf("validity_end::timestamp-'%d DAY'::INTERVAL >= now()", cfg.ACME.MaxDaysBefore)).Desc(
"id").Get(&Entry)
@ -66,12 +66,27 @@ func (u *User) HandleRegistration(cfg *config.Config, client *lego.Client) (err
}
// RequestNewCert returns a newly requested certificate to letsencrypt
func (u *User) RequestNewCert(cfg *config.Config, domains []string) (certificates *certificate.Resource, err error) {
func (u *User) RequestNewCert(cfg *config.Config, domainname *string) (certs *certificate.Resource, err error) {
legoconfig := lego.NewConfig(u)
legoconfig.CADirURL = cfg.ACME.AuthURL
legoconfig.Certificate.KeyType = certcrypto.RSA2048
ovhprovider, err := initProvider(cfg)
dom := domain.Domain{Domain: *domainname}
_, err = cfg.Db.Get(&dom)
if err != nil {
log.Println(err)
}
var provider challenge.Provider
switch dom.Provider {
case "ovh":
provider, err = initOVHProvider(cfg)
case "pdns":
provider, err = initPowerDNSProvider(cfg)
default:
return
}
if err != nil {
log.Println(err)
}
@ -81,7 +96,7 @@ func (u *User) RequestNewCert(cfg *config.Config, domains []string) (certificate
log.Println(err)
}
err = client.Challenge.SetDNS01Provider(ovhprovider)
err = client.Challenge.SetDNS01Provider(provider)
if err != nil {
log.Println(err)
}
@ -95,14 +110,15 @@ func (u *User) RequestNewCert(cfg *config.Config, domains []string) (certificate
}
request := certificate.ObtainRequest{
Domains: domains,
Domains: []string{*domainname, fmt.Sprintf(`*.%s`, *domainname)},
Bundle: true,
}
certificates, err = client.Certificate.Obtain(request)
certs, err = client.Certificate.Obtain(request)
if err != nil {
log.Println(err)
}
return
}

View File

@ -1,12 +1,16 @@
package pki
import (
"log"
"net/url"
"git.paulbsd.com/paulbsd/pki/src/config"
"github.com/go-acme/lego/v4/providers/dns/ovh"
"github.com/go-acme/lego/v4/providers/dns/pdns"
)
// initProvider initialize DNS provider configuration
func initProvider(cfg *config.Config) (ovhprovider *ovh.DNSProvider, err error) {
// initOVHProvider initialize DNS provider configuration
func initOVHProvider(cfg *config.Config) (ovhprovider *ovh.DNSProvider, err error) {
ovhconfig := ovh.NewDefaultConfig()
ovhconfig.APIEndpoint = cfg.ACME.ProviderOptions["ovhendpoint"]
@ -15,6 +19,24 @@ func initProvider(cfg *config.Config) (ovhprovider *ovh.DNSProvider, err error)
ovhconfig.ConsumerKey = cfg.ACME.ProviderOptions["ovhck"]
ovhprovider, err = ovh.NewDNSProviderConfig(ovhconfig)
if err != nil {
log.Println(err)
}
return
}
// initPowerDNSProvider initialize DNS provider configuration
func initPowerDNSProvider(cfg *config.Config) (pdnsprovider *pdns.DNSProvider, err error) {
pdnsconfig := pdns.NewDefaultConfig()
pdnsconfig.Host, err = url.Parse(cfg.ACME.ProviderOptions["pdnsapiurl"])
pdnsconfig.APIKey = cfg.ACME.ProviderOptions["pdnsapikey"]
pdnsprovider, err = pdns.NewDNSProviderConfig(pdnsconfig)
if err != nil {
log.Println(err)
}
return
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"log"
"net/http"
"strings"
"git.paulbsd.com/paulbsd/pki/src/config"
"git.paulbsd.com/paulbsd/pki/src/pki"
@ -30,13 +29,17 @@ func RunServer(cfg *config.Config) (err error) {
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to PKI software (https://git.paulbsd.com/paulbsd/pki)")
})
e.GET("/domain/:domains", func(c echo.Context) (err error) {
var result EntryResponse
var domains = strings.Split(c.Param("domains"), ",")
e.POST("/cert", func(c echo.Context) (err error) {
var request EntryRequest
var result = make(map[string]EntryResponse)
err = c.Bind(&request)
if err != nil {
return c.JSON(http.StatusInternalServerError, "error parsing request")
}
log.Println(fmt.Sprintf("Providing %s to user %s at %s", domains, c.Get("username"), c.RealIP()))
log.Printf("Providing %s to user %s at %s\n", request.Domains, c.Get("username"), c.RealIP())
result, err = GetCertificate(cfg, c.Get("user").(*pki.User), domains)
result, err = GetCertificate(cfg, c.Get("user").(*pki.User), &request.Domains)
if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("%s", err))
}

View File

@ -6,7 +6,6 @@ import (
"fmt"
"log"
"regexp"
"strings"
"time"
"git.paulbsd.com/paulbsd/pki/src/cert"
@ -14,56 +13,66 @@ import (
"git.paulbsd.com/paulbsd/pki/src/pki"
)
const timeformatstring string = "2006-01-02 15:04:05"
var domainRegex, err = regexp.Compile(`^[a-z0-9\*]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}$`)
// GetCertificate get certificate from database if exists, of request it from ACME
func GetCertificate(cfg *config.Config, user *pki.User, domains []string) (result EntryResponse, err error) {
func GetCertificate(cfg *config.Config, user *pki.User, domains *[]string) (result map[string]EntryResponse, err error) {
err = CheckDomains(domains)
if err != nil {
return result, err
}
result = make(map[string]EntryResponse)
entry, err := user.GetEntry(cfg, domains)
if err != nil {
certs, err := user.RequestNewCert(cfg, domains)
for _, domain := range *domains {
entry, err := user.GetEntry(cfg, &domain)
if err != nil {
log.Println(fmt.Sprintf("Error fetching new certificate %s", err))
certs, err := user.RequestNewCert(cfg, &domain)
if err != nil {
log.Printf("Error fetching new certificate %s\n", err)
return result, err
}
NotBefore, NotAfter, err := GetDates(certs.Certificate)
if err != nil {
log.Println("Error where parsing dates")
return result, err
}
entry := cert.Entry{Domain: certs.Domain,
Certificate: string(certs.Certificate),
PrivateKey: string(certs.PrivateKey),
ValidityBegin: NotBefore,
ValidityEnd: NotAfter,
AuthURL: cfg.ACME.AuthURL}
cfg.Db.Insert(&entry)
result[domain] = convertEntryToResponse(entry)
return result, err
}
NotBefore, NotAfter, err := GetDates(certs.Certificate)
if err != nil {
log.Println("Error where parsing dates")
return result, err
}
entry := cert.Entry{Domains: strings.Join(domains, ","),
Certificate: string(certs.Certificate),
PrivateKey: string(certs.PrivateKey),
ValidityBegin: NotBefore,
ValidityEnd: NotAfter,
AuthURL: cfg.ACME.AuthURL}
cfg.Db.Insert(&entry)
result = convertEntryToResponse(entry)
return result, err
result[domain] = convertEntryToResponse(entry)
}
result = convertEntryToResponse(entry)
return
}
// CheckDomains check if requested domains are valid
func CheckDomains(domains []string) (err error) {
domainRegex, err := regexp.Compile(`^[a-z0-9\*]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}$`)
if err != nil {
return
}
for _, d := range domains {
res := domainRegex.Match([]byte(d))
if !res {
return fmt.Errorf(fmt.Sprintf("Domain %s has not a valid syntax %s, please verify", d, err))
func CheckDomains(domains *[]string) (err error) {
for _, domain := range *domains {
err = CheckDomain(&domain)
if err != nil {
return
}
}
return
}
// CheckDomain check if requested domain are valid
func CheckDomain(domain *string) (err error) {
res := domainRegex.Match([]byte(*domain))
if !res {
return fmt.Errorf("Domain %s has not a valid syntax %s, please verify", *domain, err)
}
return
}
// GetDates decodes NotBefore and NotAfter date of cert
func GetDates(cert []byte) (NotBefore time.Time, NotAfter time.Time, err error) {
block, _ := pem.Decode(cert)
@ -80,9 +89,7 @@ func GetDates(cert []byte) (NotBefore time.Time, NotAfter time.Time, err error)
// convertEntryToResponse converts database ACME entry to JSON ACME entry
func convertEntryToResponse(in cert.Entry) (out EntryResponse) {
timeformatstring := "2006-01-02 15:04:05"
out.Domains = in.Domains
out.Domains = append(out.Domains, in.Domain)
out.Certificate = in.Certificate
out.PrivateKey = in.PrivateKey
out.ValidityBegin = in.ValidityBegin.Format(timeformatstring)
@ -91,11 +98,16 @@ func convertEntryToResponse(in cert.Entry) (out EntryResponse) {
return
}
// EntryRequest
type EntryRequest struct {
Domains []string `json:"domains"`
}
// EntryResponse is the struct defining JSON response from webservice
type EntryResponse struct {
Domains string `json:"domains"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privatekey"`
ValidityBegin string `json:"validitybegin"`
ValidityEnd string `json:"validityend"`
Domains []string `json:"domains"`
Certificate string `json:"certificate"`
PrivateKey string `json:"privatekey"`
ValidityBegin string `json:"validitybegin"`
ValidityEnd string `json:"validityend"`
}