From 90bfc259750163064d9a61b4350ad9cdc626cda6 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 19 Apr 2024 16:50:57 +0200 Subject: [PATCH] feat: add powerdns config, domain check, api changes --- src/cert/main.go | 5 ++- src/config/main.go | 11 +++-- src/database/main.go | 3 +- src/domain/main.go | 12 +++++ src/pki/acme.go | 34 +++++++++++---- src/pki/provider.go | 26 ++++++++++- src/pkiws/server.go | 15 ++++--- src/pkiws/serverhandle.go | 92 ++++++++++++++++++++++----------------- 8 files changed, 136 insertions(+), 62 deletions(-) create mode 100644 src/domain/main.go diff --git a/src/cert/main.go b/src/cert/main.go index fc68d34..2d7e85b 100644 --- a/src/cert/main.go +++ b/src/cert/main.go @@ -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"` diff --git a/src/config/main.go b/src/config/main.go index 5a3f31e..09fc5e4 100644 --- a/src/config/main.go +++ b/src/config/main.go @@ -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 diff --git a/src/database/main.go b/src/database/main.go index 641bb62..76545b3 100644 --- a/src/database/main.go +++ b/src/database/main.go @@ -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", diff --git a/src/domain/main.go b/src/domain/main.go new file mode 100644 index 0000000..3d13b65 --- /dev/null +++ b/src/domain/main.go @@ -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"` +} diff --git a/src/pki/acme.go b/src/pki/acme.go index 1ab84c6..7d32fe8 100644 --- a/src/pki/acme.go +++ b/src/pki/acme.go @@ -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 } diff --git a/src/pki/provider.go b/src/pki/provider.go index 691c3a9..00d44e2 100644 --- a/src/pki/provider.go +++ b/src/pki/provider.go @@ -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 } diff --git a/src/pkiws/server.go b/src/pkiws/server.go index 87fb940..87e5e45 100644 --- a/src/pkiws/server.go +++ b/src/pkiws/server.go @@ -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)) } diff --git a/src/pkiws/serverhandle.go b/src/pkiws/serverhandle.go index 73a2336..07962ff 100644 --- a/src/pkiws/serverhandle.go +++ b/src/pkiws/serverhandle.go @@ -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"` }