github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/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/json" 7 "encoding/pem" 8 "fmt" 9 "io/ioutil" 10 "log" 11 "net/url" 12 "os" 13 "path/filepath" 14 "sort" 15 "strings" 16 "time" 17 18 "github.com/StackExchange/dnscontrol/models" 19 "github.com/StackExchange/dnscontrol/pkg/nameservers" 20 "github.com/xenolf/lego/acmev2" 21 ) 22 23 type CertConfig struct { 24 CertName string `json:"cert_name"` 25 Names []string `json:"names"` 26 } 27 28 type Client interface { 29 IssueOrRenewCert(config *CertConfig, renewUnder int, verbose bool) (bool, error) 30 } 31 32 type certManager struct { 33 directory string 34 email string 35 acmeDirectory string 36 acmeHost string 37 cfg *models.DNSConfig 38 checkedDomains map[string]bool 39 40 account *account 41 client *acme.Client 42 } 43 44 const ( 45 LetsEncryptLive = "https://acme-v02.api.letsencrypt.org/directory" 46 LetsEncryptStage = "https://acme-staging-v02.api.letsencrypt.org/directory" 47 ) 48 49 func New(cfg *models.DNSConfig, directory string, email string, server string) (Client, error) { 50 u, err := url.Parse(server) 51 if err != nil || u.Host == "" { 52 return nil, fmt.Errorf("ACME directory '%s' is not a valid URL", server) 53 } 54 c := &certManager{ 55 directory: directory, 56 email: email, 57 acmeDirectory: server, 58 acmeHost: u.Host, 59 cfg: cfg, 60 checkedDomains: map[string]bool{}, 61 } 62 63 if err := c.loadOrCreateAccount(); err != nil { 64 return nil, err 65 } 66 c.client.ExcludeChallenges([]acme.Challenge{acme.HTTP01}) 67 c.client.SetChallengeProvider(acme.DNS01, c) 68 return c, nil 69 } 70 71 // IssueOrRenewCert will obtain a certificate with the given name if it does not exist, 72 // or renew it if it is close enough to the expiration date. 73 // It will return true if it issued or updated the certificate. 74 func (c *certManager) IssueOrRenewCert(cfg *CertConfig, renewUnder int, verbose bool) (bool, error) { 75 if !verbose { 76 acme.Logger = log.New(ioutil.Discard, "", 0) 77 } 78 79 log.Printf("Checking certificate [%s]", cfg.CertName) 80 if err := os.MkdirAll(filepath.Dir(c.certFile(cfg.CertName, "json")), perms); err != nil { 81 return false, err 82 } 83 existing, err := c.readCertificate(cfg.CertName) 84 if err != nil { 85 return false, err 86 } 87 88 var action = func() (acme.CertificateResource, error) { 89 return c.client.ObtainCertificate(cfg.Names, true, nil, true) 90 } 91 92 if existing == nil { 93 log.Println("No existing cert found. Issuing new...") 94 } else { 95 names, daysLeft, err := getCertInfo(existing.Certificate) 96 if err != nil { 97 return false, err 98 } 99 log.Printf("Found existing cert. %0.2f days remaining.", daysLeft) 100 namesOK := dnsNamesEqual(cfg.Names, names) 101 if daysLeft >= float64(renewUnder) && namesOK { 102 log.Println("Nothing to do") 103 //nothing to do 104 return false, nil 105 } 106 if !namesOK { 107 log.Println("DNS Names don't match expected set. Reissuing.") 108 } else { 109 log.Println("Renewing cert") 110 action = func() (acme.CertificateResource, error) { 111 return c.client.RenewCertificate(*existing, true, true) 112 } 113 } 114 } 115 116 certResource, err := action() 117 if err != nil { 118 return false, err 119 } 120 fmt.Printf("Obtained certificate for %s\n", cfg.CertName) 121 return true, c.writeCertificate(cfg.CertName, &certResource) 122 } 123 124 // filename for certifiacte / key / json file 125 func (c *certManager) certFile(name, ext string) string { 126 return filepath.Join(c.directory, "certificates", name, name+"."+ext) 127 } 128 129 func (c *certManager) writeCertificate(name string, cr *acme.CertificateResource) error { 130 jDAt, err := json.MarshalIndent(cr, "", " ") 131 if err != nil { 132 return err 133 } 134 if err = ioutil.WriteFile(c.certFile(name, "json"), jDAt, perms); err != nil { 135 return err 136 } 137 if err = ioutil.WriteFile(c.certFile(name, "crt"), cr.Certificate, perms); err != nil { 138 return err 139 } 140 return ioutil.WriteFile(c.certFile(name, "key"), cr.PrivateKey, perms) 141 } 142 143 func getCertInfo(pemBytes []byte) (names []string, remaining float64, err error) { 144 block, _ := pem.Decode(pemBytes) 145 if block == nil { 146 return nil, 0, fmt.Errorf("Invalid certificate pem data") 147 } 148 cert, err := x509.ParseCertificate(block.Bytes) 149 if err != nil { 150 return nil, 0, err 151 } 152 var daysLeft = float64(cert.NotAfter.Sub(time.Now())) / float64(time.Hour*24) 153 return cert.DNSNames, daysLeft, nil 154 } 155 156 // checks two lists of sans to make sure they have all the same names in them. 157 func dnsNamesEqual(a []string, b []string) bool { 158 if len(a) != len(b) { 159 return false 160 } 161 sort.Strings(a) 162 sort.Strings(b) 163 for i, s := range a { 164 if b[i] != s { 165 return false 166 } 167 } 168 return true 169 } 170 171 func (c *certManager) readCertificate(name string) (*acme.CertificateResource, error) { 172 f, err := os.Open(c.certFile(name, "json")) 173 if err != nil && os.IsNotExist(err) { 174 // if json does not exist, nothing does 175 return nil, nil 176 } 177 if err != nil { 178 return nil, err 179 } 180 defer f.Close() 181 dec := json.NewDecoder(f) 182 cr := &acme.CertificateResource{} 183 if err = dec.Decode(cr); err != nil { 184 return nil, err 185 } 186 // load cert 187 crtBytes, err := ioutil.ReadFile(c.certFile(name, "crt")) 188 if err != nil { 189 return nil, err 190 } 191 cr.Certificate = crtBytes 192 return cr, nil 193 } 194 195 func (c *certManager) Present(domain, token, keyAuth string) (e error) { 196 d := c.cfg.DomainContainingFQDN(domain) 197 // fix NS records for this domain's DNS providers 198 // only need to do this once per domain 199 const metaKey = "x-fixed-nameservers" 200 if d.Metadata[metaKey] == "" { 201 nsList, err := nameservers.DetermineNameservers(d) 202 if err != nil { 203 return err 204 } 205 d.Nameservers = nsList 206 nameservers.AddNSRecords(d) 207 d.Metadata[metaKey] = "true" 208 } 209 // copy now so we can add txt record safely, and just run unmodified version later to cleanup 210 d, err := d.Copy() 211 if err != nil { 212 return err 213 } 214 if err := c.ensureNoPendingCorrections(d); err != nil { 215 return err 216 } 217 fqdn, val, _ := acme.DNS01Record(domain, keyAuth) 218 fmt.Println(fqdn, val) 219 txt := &models.RecordConfig{Type: "TXT"} 220 txt.SetTargetTXT(val) 221 txt.SetLabelFromFQDN(fqdn, d.Name) 222 d.Records = append(d.Records, txt) 223 224 return getAndRunCorrections(d) 225 } 226 227 func (c *certManager) ensureNoPendingCorrections(d *models.DomainConfig) error { 228 // only need to check a domain once per app run 229 if c.checkedDomains[d.Name] { 230 return nil 231 } 232 corrections, err := getCorrections(d) 233 if err != nil { 234 return err 235 } 236 if len(corrections) != 0 { 237 // TODO: maybe allow forcing through this check. 238 for _, c := range corrections { 239 fmt.Println(c.Msg) 240 } 241 return fmt.Errorf("Found %d pending corrections for %s. Not going to proceed issuing certificates", len(corrections), d.Name) 242 } 243 return nil 244 } 245 246 // IgnoredProviders is a lit of provider names that should not be used to fill challenges. 247 var IgnoredProviders = map[string]bool{} 248 249 func getCorrections(d *models.DomainConfig) ([]*models.Correction, error) { 250 cs := []*models.Correction{} 251 for _, p := range d.DNSProviderInstances { 252 if IgnoredProviders[p.Name] { 253 continue 254 } 255 dc, err := d.Copy() 256 if err != nil { 257 return nil, err 258 } 259 corrections, err := p.Driver.GetDomainCorrections(dc) 260 if err != nil { 261 return nil, err 262 } 263 for _, c := range corrections { 264 c.Msg = fmt.Sprintf("[%s] %s", p.Name, strings.TrimSpace(c.Msg)) 265 } 266 cs = append(cs, corrections...) 267 } 268 return cs, nil 269 } 270 271 func getAndRunCorrections(d *models.DomainConfig) error { 272 cs, err := getCorrections(d) 273 if err != nil { 274 return err 275 } 276 fmt.Printf("%d corrections\n", len(cs)) 277 for _, c := range cs { 278 fmt.Printf("Running [%s]\n", c.Msg) 279 err = c.F() 280 if err != nil { 281 return err 282 } 283 } 284 return nil 285 } 286 287 func (c *certManager) CleanUp(domain, token, keyAuth string) error { 288 d := c.cfg.DomainContainingFQDN(domain) 289 return getAndRunCorrections(d) 290 }