diff --git a/acme/client.go b/acme/client.go index 58cb74f6..f3b00ebc 100644 --- a/acme/client.go +++ b/acme/client.go @@ -2,6 +2,7 @@ package acme import ( "crypto/rsa" + "crypto/x509" "encoding/base64" "encoding/json" "errors" @@ -187,6 +188,53 @@ func (c *Client) RevokeCertificate(certificate []byte) error { return nil } +// RenewCertificate takes a CertificateResource and tries to renew the certificate. +// If the renewal process succeeds, the new certificate will replace the old one in the CertResource. +// Please be aware that this function will return a new certificate in ANY case that is not an error. +// If the server does not provide us with a new cert on a GET request to the CertURL +// this function will start a new-cert flow where the old (provided) cert will get REVOKED +// and a new certificate gets generated. +func (c *Client) RenewCertificate(cert *CertificateResource) (*CertificateResource, error) { + // Input certificate is PEM encoded. Decode it here as we may need the decoded + // cert later on in the renewal process. + x509Cert, err := pemDecodeTox509(cert.Certificate) + if err != nil { + return nil, err + } + + // This is just meant to be informal for the user. + timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) + logger().Printf("[%s] Trying to renew certificate with %d hours remaining.", cert.Domain, int(timeLeft.Hours())) + + // The first step of renewal is to check if we get a renewed cert + // directly from the cert URL. + resp, err := http.Get(cert.CertURL) + serverCertBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + serverCert, err := x509.ParseCertificate(serverCertBytes) + if err != nil { + return nil, err + } + + // If the server responds with a different certificate we are effectively renewed. + // TODO: Further test if we can actually use the new certificate (Our private key works) + if !x509Cert.Equal(serverCert) { + logger().Printf("[%s] The server responded with a renewed certificate.", cert.Domain) + cert.Certificate = pemEncode(derCertificateBytes(serverCertBytes)) + return cert, nil + } + + newCerts, err := c.ObtainCertificates([]string{cert.Domain}) + if err != nil { + return nil, err + } + + return &newCerts[0], nil +} + // Looks through the challenge combinations to find a solvable match. // Then solves the challenges in series and returns. func (c *Client) solveChallenges(challenges []*authorizationResource) error { diff --git a/cli.go b/cli.go index 4b5e5348..99a48ace 100644 --- a/cli.go +++ b/cli.go @@ -50,6 +50,11 @@ func main() { Usage: "Revoke a certificate", Action: revoke, }, + { + Name: "renew", + Usage: "Renew a certificate", + Action: renew, + }, } app.Flags = []cli.Flag{ diff --git a/cli_handlers.go b/cli_handlers.go index 97b4e9e1..eca8d30d 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -158,3 +158,52 @@ func revoke(c *cli.Context) { } } } + +func renew(c *cli.Context) { + conf, _, client := setup(c) + + for _, domain := range c.GlobalStringSlice("domains") { + // load the cert resource from files. + // We store the certificate, private key and metadata in different files + // as web servers would not be able to work with a combined file. + certPath := path.Join(conf.CertPath(), domain+".crt") + privPath := path.Join(conf.CertPath(), domain+".key") + metaPath := path.Join(conf.CertPath(), domain+".json") + + certBytes, err := ioutil.ReadFile(certPath) + if err != nil { + logger().Printf("Error while loading the certificate for domain %s\n\t%v", domain, err) + return + } + + keyBytes, err := ioutil.ReadFile(privPath) + if err != nil { + logger().Printf("Error while loading the private key for domain %s\n\t%v", domain, err) + return + } + + metaBytes, err := ioutil.ReadFile(metaPath) + if err != nil { + logger().Printf("Error while loading the meta data for domain %s\n\t%v", domain, err) + return + } + + var certRes acme.CertificateResource + err = json.Unmarshal(metaBytes, &certRes) + if err != nil { + logger().Printf("Error while marshalling the meta data for domain %s\n\t%v", domain, err) + return + } + + certRes.PrivateKey = keyBytes + certRes.Certificate = certBytes + + newCert, err := client.RenewCertificate(&certRes) + if err != nil { + logger().Printf("%v", err) + return + } + + saveCertRes(newCert, conf) + } +}