github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/pkg/acme/acme.go (about) 1 // Package acme provides a means of performing Let's Encrypt DNS challenges via a DNSConfig 2 package acme 3 4 import ( 5 "crypto/x509" 6 "encoding/pem" 7 "fmt" 8 "io/ioutil" 9 "log" 10 "net/url" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/StackExchange/dnscontrol/v2/models" 16 "github.com/StackExchange/dnscontrol/v2/pkg/nameservers" 17 "github.com/StackExchange/dnscontrol/v2/pkg/notifications" 18 "github.com/go-acme/lego/certcrypto" 19 "github.com/go-acme/lego/certificate" 20 "github.com/go-acme/lego/challenge" 21 "github.com/go-acme/lego/challenge/dns01" 22 "github.com/go-acme/lego/lego" 23 acmelog "github.com/go-acme/lego/log" 24 ) 25 26 type CertConfig struct { 27 CertName string `json:"cert_name"` 28 Names []string `json:"names"` 29 UseECC bool `json:"use_ecc"` 30 MustStaple bool `json:"must_staple"` 31 } 32 33 type Client interface { 34 IssueOrRenewCert(config *CertConfig, renewUnder int, verbose bool) (bool, error) 35 } 36 37 type certManager struct { 38 email string 39 acmeDirectory string 40 acmeHost string 41 42 storage Storage 43 cfg *models.DNSConfig 44 domains map[string]*models.DomainConfig 45 originalDomains []*models.DomainConfig 46 47 notifier notifications.Notifier 48 49 account *Account 50 waitedOnce bool 51 } 52 53 const ( 54 LetsEncryptLive = "https://acme-v02.api.letsencrypt.org/directory" 55 LetsEncryptStage = "https://acme-staging-v02.api.letsencrypt.org/directory" 56 ) 57 58 func New(cfg *models.DNSConfig, directory string, email string, server string, notify notifications.Notifier) (Client, error) { 59 return commonNew(cfg, directoryStorage(directory), email, server, notify) 60 } 61 62 func commonNew(cfg *models.DNSConfig, storage Storage, email string, server string, notify notifications.Notifier) (Client, error) { 63 u, err := url.Parse(server) 64 if err != nil || u.Host == "" { 65 return nil, fmt.Errorf("ACME directory '%s' is not a valid URL", server) 66 } 67 c := &certManager{ 68 storage: storage, 69 email: email, 70 acmeDirectory: server, 71 acmeHost: u.Host, 72 cfg: cfg, 73 domains: map[string]*models.DomainConfig{}, 74 notifier: notify, 75 } 76 77 acct, err := c.getOrCreateAccount() 78 if err != nil { 79 return nil, err 80 } 81 c.account = acct 82 return c, nil 83 } 84 85 func NewVault(cfg *models.DNSConfig, vaultPath string, email string, server string, notify notifications.Notifier) (Client, error) { 86 storage, err := makeVaultStorage(vaultPath) 87 if err != nil { 88 return nil, err 89 } 90 return commonNew(cfg, storage, email, server, notify) 91 } 92 93 // IssueOrRenewCert will obtain a certificate with the given name if it does not exist, 94 // or renew it if it is close enough to the expiration date. 95 // It will return true if it issued or updated the certificate. 96 func (c *certManager) IssueOrRenewCert(cfg *CertConfig, renewUnder int, verbose bool) (bool, error) { 97 if !verbose { 98 acmelog.Logger = log.New(ioutil.Discard, "", 0) 99 } 100 defer c.finalCleanUp() 101 102 log.Printf("Checking certificate [%s]", cfg.CertName) 103 existing, err := c.storage.GetCertificate(cfg.CertName) 104 if err != nil { 105 return false, err 106 } 107 108 var client *lego.Client 109 110 var action = func() (*certificate.Resource, error) { 111 return client.Certificate.Obtain(certificate.ObtainRequest{ 112 Bundle: true, 113 Domains: cfg.Names, 114 MustStaple: cfg.MustStaple, 115 }) 116 } 117 118 if existing == nil { 119 log.Println("No existing cert found. Issuing new...") 120 } else { 121 names, daysLeft, err := getCertInfo(existing.Certificate) 122 if err != nil { 123 return false, err 124 } 125 log.Printf("Found existing cert. %0.2f days remaining.", daysLeft) 126 namesOK := dnsNamesEqual(cfg.Names, names) 127 if daysLeft >= float64(renewUnder) && namesOK { 128 log.Println("Nothing to do") 129 //nothing to do 130 return false, nil 131 } 132 if !namesOK { 133 log.Println("DNS Names don't match expected set. Reissuing.") 134 } else { 135 log.Println("Renewing cert") 136 action = func() (*certificate.Resource, error) { 137 return client.Certificate.Renew(*existing, true, cfg.MustStaple) 138 } 139 } 140 } 141 142 kt := certcrypto.RSA2048 143 if cfg.UseECC { 144 kt = certcrypto.EC256 145 } 146 config := lego.NewConfig(c.account) 147 config.CADirURL = c.acmeDirectory 148 config.Certificate.KeyType = kt 149 client, err = lego.NewClient(config) 150 if err != nil { 151 return false, err 152 } 153 client.Challenge.Remove(challenge.HTTP01) 154 client.Challenge.Remove(challenge.TLSALPN01) 155 client.Challenge.SetDNS01Provider(c, dns01.WrapPreCheck(c.preCheckDNS)) 156 157 certResource, err := action() 158 if err != nil { 159 return false, err 160 } 161 fmt.Printf("Obtained certificate for %s\n", cfg.CertName) 162 if err = c.storage.StoreCertificate(cfg.CertName, certResource); err != nil { 163 return true, err 164 } 165 166 return true, nil 167 } 168 169 func getCertInfo(pemBytes []byte) (names []string, remaining float64, err error) { 170 block, _ := pem.Decode(pemBytes) 171 if block == nil { 172 return nil, 0, fmt.Errorf("Invalid certificate pem data") 173 } 174 cert, err := x509.ParseCertificate(block.Bytes) 175 if err != nil { 176 return nil, 0, err 177 } 178 var daysLeft = float64(cert.NotAfter.Sub(time.Now())) / float64(time.Hour*24) 179 return cert.DNSNames, daysLeft, nil 180 } 181 182 // checks two lists of sans to make sure they have all the same names in them. 183 func dnsNamesEqual(a []string, b []string) bool { 184 if len(a) != len(b) { 185 return false 186 } 187 sort.Strings(a) 188 sort.Strings(b) 189 for i, s := range a { 190 if b[i] != s { 191 return false 192 } 193 } 194 return true 195 } 196 197 func (c *certManager) Present(domain, token, keyAuth string) (e error) { 198 d := c.cfg.DomainContainingFQDN(domain) 199 name := d.Name 200 if seen := c.domains[name]; seen != nil { 201 // we've already pre-processed this domain, just need to add to it. 202 d = seen 203 } else { 204 // one-time tasks to get this domain ready. 205 // if multiple validations on a single domain, we don't need to rebuild all this. 206 207 // fix NS records for this domain's DNS providers 208 nsList, err := nameservers.DetermineNameservers(d) 209 if err != nil { 210 return err 211 } 212 d.Nameservers = nsList 213 nameservers.AddNSRecords(d) 214 215 // make sure we have the latest config before we change anything. 216 // alternately, we could avoid a lot of this trouble if we really really trusted no-purge in all cases 217 if err := c.ensureNoPendingCorrections(d); err != nil { 218 return err 219 } 220 221 // copy domain and work from copy from now on. That way original config can be used to "restore" when we are all done. 222 copy, err := d.Copy() 223 if err != nil { 224 return err 225 } 226 c.originalDomains = append(c.originalDomains, d) 227 c.domains[name] = copy 228 d = copy 229 } 230 231 fqdn, val := dns01.GetRecord(domain, keyAuth) 232 txt := &models.RecordConfig{Type: "TXT"} 233 txt.SetTargetTXT(val) 234 txt.SetLabelFromFQDN(fqdn, d.Name) 235 d.Records = append(d.Records, txt) 236 return c.getAndRunCorrections(d) 237 } 238 239 func (c *certManager) ensureNoPendingCorrections(d *models.DomainConfig) error { 240 corrections, err := c.getCorrections(d) 241 if err != nil { 242 return err 243 } 244 if len(corrections) != 0 { 245 // TODO: maybe allow forcing through this check. 246 for _, c := range corrections { 247 fmt.Println(c.Msg) 248 } 249 return fmt.Errorf("Found %d pending corrections for %s. Not going to proceed issuing certificates", len(corrections), d.Name) 250 } 251 return nil 252 } 253 254 // IgnoredProviders is a lit of provider names that should not be used to fill challenges. 255 var IgnoredProviders = map[string]bool{} 256 257 func (c *certManager) getCorrections(d *models.DomainConfig) ([]*models.Correction, error) { 258 cs := []*models.Correction{} 259 for _, p := range d.DNSProviderInstances { 260 if IgnoredProviders[p.Name] { 261 continue 262 } 263 dc, err := d.Copy() 264 if err != nil { 265 return nil, err 266 } 267 corrections, err := p.Driver.GetDomainCorrections(dc) 268 if err != nil { 269 return nil, err 270 } 271 for _, c := range corrections { 272 c.Msg = fmt.Sprintf("[%s] %s", p.Name, strings.TrimSpace(c.Msg)) 273 } 274 cs = append(cs, corrections...) 275 } 276 return cs, nil 277 } 278 279 func (c *certManager) getAndRunCorrections(d *models.DomainConfig) error { 280 cs, err := c.getCorrections(d) 281 if err != nil { 282 return err 283 } 284 fmt.Printf("%d corrections\n", len(cs)) 285 for _, corr := range cs { 286 fmt.Printf("Running [%s]\n", corr.Msg) 287 err = corr.F() 288 c.notifier.Notify(d.Name, "certs", corr.Msg, err, false) 289 if err != nil { 290 return err 291 } 292 } 293 return nil 294 } 295 296 func (c *certManager) CleanUp(domain, token, keyAuth string) error { 297 // do nothing for now. We will do a final clean up step at the very end. 298 return nil 299 } 300 301 func (c *certManager) finalCleanUp() error { 302 log.Println("Cleaning up all records we made") 303 var lastError error 304 for _, d := range c.originalDomains { 305 if err := c.getAndRunCorrections(d); err != nil { 306 log.Printf("ERROR cleaning up: %s", err) 307 lastError = err 308 } 309 } 310 return lastError 311 }