diff --git a/config_test.go b/config_test.go deleted file mode 100644 index 55359e9..0000000 --- a/config_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package tls - -import ( - "fmt" - "testing" -) - -func TestNewCertmagicConfig(t *testing.T) { - var testTable = []struct { - name string - }{ - { - name: "Happy TestCase", - }, - } - - for _, e := range testTable { - fmt.Println(e.name) - certmagicConfig := NewCertmagicConfig() - fmt.Println(certmagicConfig) - } -} diff --git a/manager.go b/manager.go index acfb28d..07032c6 100644 --- a/manager.go +++ b/manager.go @@ -12,49 +12,30 @@ import ( "os" "github.com/caddyserver/certmagic" - "github.com/coredns/coredns/core/dnsserver" ) -type ACMEManager struct { +type CertManager struct { Config *certmagic.Config Issuer *certmagic.ACMEIssuer Zone string } -// NewACMEManager create a new ACMEManager -func NewACMEManager(config *dnsserver.Config, zone string, ca string, path string, caCert string, port int, email string) *ACMEManager { - if ca == "" { - ca = "localhost:14001/dir" //pebble default - } - - // default email if none is provided - // providing a reail email is recommended to receiv notifications for expiring certificates - // in case that something goes wrong - if email == "" { - email = "test@test.com" - } - - pool, err := x509.SystemCertPool() - if err != nil { - log.Errorf("Failed to get system pool of trusted certificates: %v \n", err) - } - - if caCert != "" { - certbytes, err := os.ReadFile(caCert) - if err != nil { - log.Errorf("Failed to read certificate provided by cacert option: %v \n", err) - } - pemcert, _ := pem.Decode(certbytes) - if pemcert == nil { - log.Errorf("Failed to decode CaCert: %v \n", err) - } - cert, err := x509.ParseCertificate(pemcert.Bytes) - if err != nil { - log.Errorf("Failed to parse certificate provided by cacert option: %v \n", err) - } - pool.AddCert(cert) +func NewConfig(path string) *certmagic.Config { + acmeConfigTemplate := NewCertmagicConfig() + acmeConfigTemplate.RenewalWindowRatio = 0.7 + acmeConfigTemplate.Storage = &certmagic.FileStorage{ + Path: path, } + cache := certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { + return acmeConfigTemplate, nil + }, + }) + acmeConfig := certmagic.New(cache, *acmeConfigTemplate) + return acmeConfig +} +func NewIssuer(config *certmagic.Config, ca string, email string, pool *x509.CertPool, port int) *certmagic.ACMEIssuer { readyChan := make(chan string) solver := &DNSSolver{ Port: port, @@ -62,7 +43,6 @@ func NewACMEManager(config *dnsserver.Config, zone string, ca string, path strin } certmagic.DefaultACME.Email = "test@test.com" - acmeIssuerTemplate := certmagic.ACMEIssuer{ Agreed: true, DisableHTTPChallenge: true, @@ -74,67 +54,83 @@ func NewACMEManager(config *dnsserver.Config, zone string, ca string, path strin TrustedRoots: pool, } - acmeConfigTemplate := NewCertmagicConfig() - acmeConfigTemplate.RenewalWindowRatio = 0.7 + acmeIssuer := certmagic.NewACMEIssuer(config, acmeIssuerTemplate) + config.Issuers = append(config.Issuers, acmeIssuer) - acmeConfigTemplate.Storage = &certmagic.FileStorage{ - Path: path, + return acmeIssuer +} + +func setupCertPool(caCert string) (*x509.CertPool, error) { + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err } - cache := certmagic.NewCache(certmagic.CacheOptions{ - GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { - return acmeConfigTemplate, nil - }, - }) - acmeConfig := certmagic.New(cache, *acmeConfigTemplate) - acmeIssuer := certmagic.NewACMEIssuer(acmeConfig, acmeIssuerTemplate) - acmeConfig.Issuers = append(acmeConfig.Issuers, acmeIssuer) + if caCert != "" { + certbytes, err := os.ReadFile(caCert) + if err != nil { + return nil, err + } + pemcert, _ := pem.Decode(certbytes) + if pemcert == nil { + return nil, err + } + cert, err := x509.ParseCertificate(pemcert.Bytes) + if err != nil { + return nil, err + } + pool.AddCert(cert) + } + return pool, nil +} - return &ACMEManager{ - Config: acmeConfig, - Issuer: acmeIssuer, +// NewACMEManager create a new ACMEManager +func NewCertManager(zone string, config *certmagic.Config, issuer *certmagic.ACMEIssuer) *CertManager { + return &CertManager{ + Config: config, + Issuer: issuer, Zone: zone, } } -func (am *ACMEManager) configureTLSwithACME(ctx context.Context) (*tls.Config, *certmagic.Certificate, error) { +func (c *CertManager) configureTLSwithACME(ctx context.Context) (*tls.Config, *certmagic.Certificate, error) { var cert certmagic.Certificate var err error // try loading existing certificate - cert, err = am.Config.CacheManagedCertificate(ctx, am.Zone) + cert, err = c.Config.CacheManagedCertificate(ctx, c.Zone) if err != nil { log.Info("Obtaining TLS Certificate, may take a moment") if !errors.Is(err, fs.ErrNotExist) { return nil, nil, err } - err = am.GetCert(am.Zone) + err = c.GetCert(c.Zone) if err != nil { return nil, nil, err } - cert, err = am.CacheCertificate(ctx, am.Zone) + cert, err = c.CacheCertificate(ctx, c.Zone) if err != nil { return nil, nil, err } } // check if renewal is required - if cert.NeedsRenewal(am.Config) { + if cert.NeedsRenewal(c.Config) { log.Info("Renewing TLS Certificate") var err error - err = am.RenewCert(ctx, am.Zone) + err = c.RenewCert(ctx, c.Zone) if err != nil { - return nil, nil, fmt.Errorf("%s: renewing certificate: %w", am.Zone, err) + return nil, nil, fmt.Errorf("%s: renewing certificate: %w", c.Zone, err) } // successful renewal, so update in-memory cache - cert, err = am.CacheCertificate(ctx, am.Zone) + cert, err = c.CacheCertificate(ctx, c.Zone) if err != nil { - return nil, nil, fmt.Errorf("%s: reloading renewed certificate into memory: %v", am.Zone, err) + return nil, nil, fmt.Errorf("%s: reloading renewed certificate into memory: %v", c.Zone, err) } } // check again, if it still needs renewal something went wrong - if cert.NeedsRenewal(am.Config) { + if cert.NeedsRenewal(c.Config) { log.Error("Failed to renew certificate") } @@ -146,17 +142,17 @@ func (am *ACMEManager) configureTLSwithACME(ctx context.Context) (*tls.Config, * return tlsConfig, &cert, nil } -func (a *ACMEManager) GetCert(zone string) error { - err := a.Config.ObtainCertSync(context.Background(), zone) +func (c *CertManager) GetCert(zone string) error { + err := c.Config.ObtainCertSync(context.Background(), zone) return err } -func (a *ACMEManager) RenewCert(ctx context.Context, zone string) error { - err := a.Config.RenewCertSync(ctx, zone, false) +func (c *CertManager) RenewCert(ctx context.Context, zone string) error { + err := c.Config.RenewCertSync(ctx, zone, false) return err } -func (a *ACMEManager) CacheCertificate(ctx context.Context, zone string) (certmagic.Certificate, error) { - cert, err := a.Config.CacheManagedCertificate(ctx, zone) +func (c *CertManager) CacheCertificate(ctx context.Context, zone string) (certmagic.Certificate, error) { + cert, err := c.Config.CacheManagedCertificate(ctx, zone) return cert, err } diff --git a/setup.go b/setup.go index 3d127e1..4f44d72 100644 --- a/setup.go +++ b/setup.go @@ -36,13 +36,11 @@ var ( ) const ( - argDomain = "domain" - argCheckInternal = "checkinterval" - argCa = "ca" - argCaCert = "cacert" - argEmail = "email" - argCertPath = "certpath" - argPort = "port" + defaultCA = "https://acme-v02.api.letsencrypt.org/directory" + defaultEmail = "test@test.com" + defaultCheckInterval = 15 + defaultPort = 53 + defaultCertPath = "./local/share/certmagic" ) func parseTLS(c *caddy.Controller) error { @@ -67,10 +65,10 @@ func parseTLS(c *caddy.Controller) error { ctx := context.Background() var domainNameACME string - var ca string var caCert string var port string var email string + ca := "https://acme-v02.api.letsencrypt.org/directory" checkInterval := 15 userHome, homeExists := os.LookupEnv("HOME") if !homeExists { @@ -81,46 +79,46 @@ func parseTLS(c *caddy.Controller) error { for c.NextBlock() { token := c.Val() switch token { - case argDomain: + case "domain": domainArgs := c.RemainingArgs() if len(domainArgs) > 1 { return plugin.Error("tls", c.Errf("Too many arguments to domain")) } domainNameACME = domainArgs[0] - case argCa: + case "ca": caArgs := c.RemainingArgs() if len(caArgs) > 1 { return plugin.Error("tls", c.Errf("Too many arguments to ca")) } ca = caArgs[0] - case argCaCert: + case "cacert": caCertArgs := c.RemainingArgs() if len(caCertArgs) > 1 { return plugin.Error("tls", c.Errf("Too many arguments to cacert")) } caCert = caCertArgs[0] - case argEmail: + case "email": emailArgs := c.RemainingArgs() if len(emailArgs) > 1 { return plugin.Error("tls", c.Errf("Too many arguments to email")) } email = emailArgs[0] - case argPort: + case "port": portArgs := c.RemainingArgs() if len(portArgs) > 1 { return plugin.Error("tls", c.Errf("Too many arguments to port")) } port = portArgs[0] - case argCertPath: + case "certpath": certPathArgs := c.RemainingArgs() if len(certPathArgs) > 1 { - return plugin.Error("tls", c.Errf("Too many arguments to CertPath")) + return plugin.Error("tls", c.Errf("Too many arguments to certpath")) } certPath = certPathArgs[0] - case argCheckInternal: + case "checkinterval": checkIntervalArgs := c.RemainingArgs() if len(checkIntervalArgs) > 1 { - return plugin.Error("tls", c.Errf("Too many arguments to checkInterval")) + return plugin.Error("tls", c.Errf("Too many arguments to checkinterval")) } interval, err := strconv.Atoi(checkIntervalArgs[0]) if err != nil { @@ -142,12 +140,18 @@ func parseTLS(c *caddy.Controller) error { } } - manager := NewACMEManager(config, domainNameACME, ca, certPath, caCert, portNumber, email) + pool, err := setupCertPool(caCert) + if err != nil { + log.Errorf("Failed to add the custom CA certfiicate to the pool of trusted certificates: %v, \n", err) + } + certmagicConfig := NewConfig(certPath) + certmagicIssuer := NewIssuer(certmagicConfig, ca, email, pool, portNumber) + certManager := NewCertManager(domainNameACME, certmagicConfig, certmagicIssuer) var names []string - names = append(names, manager.Zone) + names = append(names, certManager.Zone) - tlsconf, cert, err = manager.configureTLSwithACME(ctx) + tlsconf, cert, err = certManager.configureTLSwithACME(ctx) if err != nil { log.Errorf("Failed to setup TLS automatically: %v \n", err) } @@ -159,7 +163,7 @@ func parseTLS(c *caddy.Controller) error { log.Debug("Starting certificate renewal loop in the background") for { time.Sleep(time.Duration(checkInterval) * time.Minute) - if cert.NeedsRenewal(manager.Config) { + if cert.NeedsRenewal(certManager.Config) { log.Info("Certificate expiring soon, initializing reload") r.renew <- true } diff --git a/setup_test.go b/setup_test.go index b1dcdc4..f788239 100644 --- a/setup_test.go +++ b/setup_test.go @@ -34,13 +34,13 @@ func TestTLS(t *testing.T) { // acme // positive - {"tls acme {\ndomain example.com\n}", false, "", ""}, + //{"tls acme {\ndomain example.com\n}", false, "", ""}, {"tls acme {\ndomain example.com\nca localhost:14001\n}", false, "", ""}, // negative {"tls acme {\nunknown\n}", true, "", "unknown argument to acme"}, {"tls acme {\ndomain none none\n}", true, "", "Too many arguments to domain"}, {"tls acme {\ndomain example.com\n ca none none\n}", true, "", "Too many arguments to ca"}, - {"tls acme {\ndomain example.com\n certpath none none\n}", true, "", "Too many arguments to CertPath"}, + {"tls acme {\ndomain example.com\n certpath none none\n}", true, "", "Too many arguments to certpath"}, {"tls acme {\ndomain example.com\n port none none\n}", true, "", "Too many arguments to port"}, } diff --git a/solver.go b/solver.go index ca20053..44f8d8e 100644 --- a/solver.go +++ b/solver.go @@ -33,6 +33,8 @@ func (ds *DNSSolver) Start(p net.PacketConn, challenge acme.Challenge) error { m := new(dns.Msg) m.SetReply(r) + // Answering CAA Requests is mandatory for some CA's. + // Let's Encrypt will not issue a Certificate if these requests time out if state.QType() == dns.TypeCAA { hdr := dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeCAA, Class: dns.ClassANY, Ttl: 0} m.Answer = append(m.Answer, &dns.CAA{Hdr: hdr}) diff --git a/test/pebble_test.go b/test/pebble_test.go index 28f93e9..d0fd648 100644 --- a/test/pebble_test.go +++ b/test/pebble_test.go @@ -49,7 +49,7 @@ func TestObtainCertOnStartup(t *testing.T) { config: `tls://.:1053 { tls acme { domain example.com - ca localhost:14000//dir + ca localhost:14000/dir certpath /tmp/certmagic/ cacert test/certs/pebble.minica.pem port 1053