Certificate Authority work

This commit is contained in:
Jamie Curnow
2021-07-29 17:45:14 +10:00
parent ae00ab09e4
commit 339ee13346
35 changed files with 737 additions and 136 deletions

View File

@@ -17,7 +17,7 @@ var acmeShFile string
// GetAcmeShVersion will return the acme.sh script version
func GetAcmeShVersion() string {
if r, err := acmeShExec("--version"); err == nil {
if r, err := shExec("--version"); err == nil {
// modify the output
r = strings.Trim(r, "\n")
v := strings.Split(r, "\n")
@@ -26,13 +26,15 @@ func GetAcmeShVersion() string {
return ""
}
func acmeShExec(args ...string) (string, error) {
// shExec executes the acme.sh with arguments
func shExec(args ...string) (string, error) {
if _, err := os.Stat(acmeShFile); os.IsNotExist(err) {
e := fmt.Errorf("%s does not exist", acmeShFile)
logger.Error("AcmeShError", e)
return "", e
}
logger.Debug("CMD: %s %v", acmeShFile, args)
// nolint: gosec
c := exec.Command(acmeShFile, args...)
b, e := c.Output()
@@ -61,3 +63,33 @@ func WriteAcmeSh() {
logger.Info("Wrote %s", acmeShFile)
}
}
// RequestCert does all the heavy lifting
func RequestCert(domains []string, method string) error {
args := []string{"--issue"}
webroot := "/home/wwwroot/example.com"
// Add domains to args
for _, domain := range domains {
args = append(args, "-d", domain)
}
switch method {
// case "dns":
case "http":
args = append(args, "-w", webroot)
default:
return fmt.Errorf("RequestCert method not supported: %s", method)
}
ret, err := shExec(args...)
if err != nil {
return err
}
logger.Debug("ret: %+v", ret)
return nil
}

View File

@@ -10,12 +10,14 @@ func CreateCertificateAuthority() string {
"additionalProperties": false,
"required": [
"name",
"acme2_url"
"acmesh_server",
"max_domains"
],
"properties": {
"name": %s,
"acme2_url": %s
"acmesh_server": %s,
"max_domains": %s
}
}
`, stringMinMax(1, 100), stringMinMax(8, 255))
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne)
}

View File

@@ -10,8 +10,9 @@ func UpdateCertificateAuthority() string {
"additionalProperties": false,
"properties": {
"name": %s,
"acme2_url": %s
"acmesh_server": %s,
"max_domains": %s
}
}
`, stringMinMax(1, 100), stringMinMax(8, 255))
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne)
}

View File

@@ -152,13 +152,11 @@ func GetByStatus(status string) ([]Model, error) {
SELECT
t.*
FROM "%s" t
INNER JOIN "dns_provider" d ON d."id" = t."dns_provider_id"
INNER JOIN "certificate_authority" c ON c."id" = t."certificate_authority_id"
WHERE
t."type" IN ("http", "dns") AND
t."status" = ? AND
t."certificate_authority_id" > 0 AND
t."dns_provider_id" > 0 AND
t."is_deleted" = 0
`, tableName)

View File

@@ -2,11 +2,14 @@ package certificate
import (
"fmt"
"strings"
"time"
"npm/internal/acme"
"npm/internal/database"
"npm/internal/entity/certificateauthority"
"npm/internal/entity/dnsprovider"
"npm/internal/logger"
"npm/internal/types"
)
@@ -86,6 +89,10 @@ func (m *Model) Save() error {
return fmt.Errorf("Certificate data is incorrect or incomplete for this type")
}
if !m.ValidateWildcardSupport() {
return fmt.Errorf("Cannot use Wildcard domains with this CA")
}
m.setDefaultStatus()
if m.ID == 0 {
@@ -129,6 +136,32 @@ func (m *Model) Validate() bool {
}
}
// ValidateWildcardSupport will ensure that the CA given supports wildcards,
// only if the domains on this object have at least 1 wildcard
func (m *Model) ValidateWildcardSupport() bool {
domains, err := m.DomainNames.AsStringArray()
if err != nil {
logger.Error("ValidateWildcardSupportError", err)
return false
}
hasWildcard := false
for _, domain := range domains {
if strings.Contains(domain, "*") {
hasWildcard = true
}
}
if hasWildcard {
m.Expand()
if !m.CertificateAuthority.IsWildcardSupported {
return false
}
}
return true
}
func (m *Model) setDefaultStatus() {
if m.ID == 0 {
// It's a new certificate
@@ -154,23 +187,33 @@ func (m *Model) Expand() {
// Request makes a certificate request
func (m *Model) Request() error {
logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name)
m.Expand()
m.Status = StatusRequesting
if err := m.Save(); err != nil {
return err
}
// If error
m.Status = StatusFailed
m.ErrorMessage = "something"
if err := m.Save(); err != nil {
// do request
domains, err := m.DomainNames.AsStringArray()
if err != nil {
return err
}
err = acme.RequestCert(domains, m.Type)
if err != nil {
m.Status = StatusFailed
m.ErrorMessage = err.Error()
if err := m.Save(); err != nil {
return err
}
}
// If done
m.Status = StatusProvided
t := time.Now()
m.ExpiresOn.Time = &t
m.ExpiresOn.Time = &t // todo
if err := m.Save(); err != nil {
return err
}

View File

@@ -33,13 +33,21 @@ func Create(ca *Model) (int, error) {
created_on,
modified_on,
name,
acme2_url,
acmesh_server,
ca_bundle,
max_domains,
is_wildcard_supported,
is_setup,
is_deleted
) VALUES (
:created_on,
:modified_on,
:name,
:acme2_url,
:acmesh_server,
:ca_bundle,
:max_domains,
:is_wildcard_supported,
:is_setup,
:is_deleted
)`, ca)
@@ -69,7 +77,11 @@ func Update(ca *Model) error {
created_on = :created_on,
modified_on = :modified_on,
name = :name,
acme2_url = :acme2_url,
acmesh_server = :acmesh_server,
ca_bundle = :ca_bundle,
max_domains = :max_domains,
is_wildcard_supported = :is_wildcard_supported,
is_setup = :is_setup,
is_deleted = :is_deleted
WHERE id = :id`, ca)

View File

@@ -14,12 +14,16 @@ const (
// Model is the user model
type Model struct {
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
Acme2URL string `json:"acme2_url" db:"acme2_url" filter:"acme2_url,string"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
ID int `json:"id" db:"id" filter:"id,integer"`
CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"`
Name string `json:"name" db:"name" filter:"name,string"`
AcmeshServer string `json:"acmesh_server" db:"acmesh_server" filter:"acmesh_server,string"`
CABundle string `json:"ca_bundle" db:"ca_bundle" filter:"ca_bundle,string"`
MaxDomains int `json:"max_domains" db:"max_domains" filter:"max_domains,integer"`
IsWildcardSupported bool `json:"is_wildcard_supported" db:"is_wildcard_supported" filter:"is_wildcard_supported,boolean"`
IsSetup bool `json:"is_setup" db:"is_setup" filter:"is_setup,boolean"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {

View File

@@ -56,3 +56,16 @@ func (j *JSONB) UnmarshalJSON(data []byte) error {
func (j JSONB) MarshalJSON() ([]byte, error) {
return json.Marshal(j.Decoded)
}
// AsStringArray will attempt to return as []string
func (j JSONB) AsStringArray() ([]string, error) {
var strs []string
// Encode then Decode onto this type
b, _ := j.MarshalJSON()
if err := json.Unmarshal(b, &strs); err != nil {
return strs, err
}
return strs, nil
}

View File

@@ -41,12 +41,14 @@ mainLoop:
break mainLoop
}
case <-ticker.C:
// Can confirm that this will wait for completion before the next loop
requestCertificates()
}
}
}
func requestCertificates() {
// logger.Debug("requestCertificates fired")
rows, err := certificate.GetByStatus(certificate.StatusReady)
if err != nil {
logger.Error("requestCertificatesError", err)