Git Product home page Git Product logo

go-acme's Introduction

DEPRECATED

Use golang.org/x/autocert instead.

A Let's Encrypt client for Go

About

This is a client package for Let's Encrypt.

Rather than being a "one click TLS" service like Let's Encrypt's command line tool, this package exposes the functionality defined by the ACME spec. It is up to the user to determine which challenges they support and how they wish to complete them.

Since the ACME spec is still a draft and Let's Encrypt has yet to enter public beta, this package should be regarded as experimental (though it should still work!).

Read more about the package in this blog post.

Example usage

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "crypto/x509/pkix"
    "log"

    "github.com/ericchiang/letsencrypt"
)

var supportedChallenges = []string{
    letsencrypt.ChallengeHTTP,
    letsencrypt.ChallengeTLSSNI,
}

func main() {
    cli, err := letsencrypt.NewClient("http://localhost:4000/directory")
    if err != nil {
        log.Fatal("failed to create client:", err)
    }

    // Create a private key for your account and register
    accountKey, err := rsa.GenerateKey(rand.Reader, 4096)
    if err != nil {
        log.Fatal(err)
    }
    if _, err := cli.NewRegistration(accountKey); err != nil {
        log.Fatal("new registration failed:", err)
    }

    // ask for a set of challenges for a given domain
    auth, _, err := cli.NewAuthorization(accountKey, "dns", "example.org")
    if err != nil {
        log.Fatal(err)
    }
    chals := auth.Combinations(supportedChallenges...)
    if len(chals) == 0 {
        log.Fatal("no supported challenge combinations")
    }

    /*
        review challenge combinations and complete them
    */

    // create a certificate request for your domain
    csr, certKey, err := newCSR()
    if err != nil {
        log.Fatal(err)
    }

    // Request a certificate for your domain
    cert, err := cli.NewCertificate(accountKey, csr)
    if err != nil {
        log.Fatal(err)
    }
    // We've got a certificate. Let's Encrypt!
}

func newCSR() (*x509.CertificateRequest, *rsa.PrivateKey, error) {
    certKey, err := rsa.GenerateKey(rand.Reader, 4096)
    if err != nil {
        return nil, nil, err
    }
    template := &x509.CertificateRequest{
        SignatureAlgorithm: x509.SHA256WithRSA,
        PublicKeyAlgorithm: x509.RSA,
        PublicKey:          &certKey.PublicKey,
        Subject:            pkix.Name{CommonName: "example.org"},
        DNSNames:           []string{"example.org"},
    }
    csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, certKey)
    if err != nil {
        return nil, nil, err
    }
    csr, err := x509.ParseCertificateRequest(csrDER)
    if err != nil {
        return nil, nil, err
    }
    return csr, certKey, nil
}

Challenges

HTTP

HTTP challenges (http-01) require provising an HTTP resource at a given path on your domain.

chal := chals[0]
if chal.Type != ChallengeHTTP {
    log.Fatal("this isn't an HTTP challenge!")
}

path, resource, err := chal.HTTP(accountKey)
if err != nil {
    log.Fatal(err)
}

go func() {
    // Listen on HTTP for a request at the given path.
    hf := func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != path {
            http.NotFound(w, r)
            return
        }
        io.WriteString(w, resource)
    }
    log.Fatal(http.ListenAndServe(":80", http.HandlerFunc(hf)))
}()

// Tell the server the challenge is ready and poll the server for updates.
if err := cli.ChallengeReady(accountKey, chal); err != nil {
    // oh no, you failed the challenge
    log.Fatal(err)
}
// The challenge has been verified!

TLS SNI

TLS SNI challenges (tls-sni-01) require responding to a given TLS Server Name Indication request with a specific certificate. These server names will not be for the actual domain begin validated, so the challenge can be completed without certificate errors for users.

chal := chals[0]
if chal.Type != ChallengeTLSSNI {
    log.Fatal("this isn't an TLS SNI challenge!")
}

certs, err := chal.TLSSNI(accountKey)
if err != nil {
    log.Fatal(err)
}

go func() {
    // Configure a custom response function for SNI requests.
    getCertificate := func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
        if cert, ok := certs[clientHello.ServerName]; ok {
            return cert, nil
        }
        return nil, nil
    }
    s := &http.Server{
        Addr:      ":443",
        TLSConfig: &tls.Config{GetCertificate: getCertificate},
        Handler:   http.HandlerFunc(http.NotFound),
    }
    log.Fatal(s.ListenAndServeTLS("self-signed.cert", "self-signed.key"))
}()

// Tell the server the challenge is ready and poll the server for updates.
if err := cli.ChallengeReady(accountKey, chal); err != nil {
    // oh no, you failed the challenge
    log.Fatal(err)
}
// The challenge has been verified!

Running the tests

The test suite runs against an installation of Let's Encrypt's boulder. Follow instructions in that repo for running in development mode on 127.0.0.1:4000.

Boulder will not issue cerficiates for non-public domains (e.g. .localdomain). In addition it keeps a blacklist of domains to not issue certificates for.

Before running boulder, you must edit the base blacklist to allow example.org and localhost.localdomain.

$GOPATH$src/github.com/letsencrypt/boulder/cmd/policy-loader/base-rules.json

In order to masqurade as a public domain, the tests require adding an entry to /etc/hosts to manually change Boulder's DNS resolution. Specifically, have example.org resolve to 127.0.0.1.

$ sudo cat /etc/hosts
127.0.0.1       localhost.localdomain localhost
127.0.0.1       example.org example
::1     localhost6.localdomain6 localhost6

If you hit rate limits, shut down the Boulder instance and reload the database with ./test/create_db.sh.

go-acme's People

Contributors

alexzorin avatar c4milo avatar caramelmilk avatar cristiangraz avatar ericchiang avatar jameshartig avatar schmich avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-acme's Issues

I have a question about renew certificate

when I call RenewCertificate , Letsencrypt will return a same cert or not. Which case is successful?

there is a comment here, in https://github.com/ericchiang/letsencrypt/blob/master/acme.go#L327 :

// RenewCertificate attempts to renew an existing certificate.
// Let's Encrypt may return the same certificate. You should load your
// current x509.Certificate and use the Equal method to compare to the "new"
// certificate. If it's identical, you'll need to run NewCertificate and/or
// start a new certificate flow.
func (c *Client) RenewCertificate(certURI string) (*CertificateResponse, error) {
    resp, err := c.client.Get(certURI)
    if err != nil {
        return nil, fmt.Errorf("renew certificate http error: %s", err)
    }

    if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
        return nil, errors.New("certificate not available. Start a new certificate flow")
    }

    certResp, err := handleCertificateResponse(resp)
    if err == nil {
        if certResp.URI == "" {
            certResp.URI = certURI
        }
    }

    return certResp, err
}

but there is also a comment here in https://github.com/xenolf/lego/blob/master/acme/client.go#L398 :

// RenewCertificate takes a CertificateResource and tries to renew the certificate.
// If the renewal process succeeds, the new certificate will ge returned in a new 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 a new certificate gets generated.
// If bundle is true, the []byte contains both the issuer certificate and
// your issued certificate as a bundle.
// For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil.
func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (CertificateResource, error) {
    // Input certificate is PEM encoded. Decode it here as we may need the decoded
    // cert later on in the renewal process. The input may be a bundle or a single certificate.
    certificates, err := parsePEMBundle(cert.Certificate)
    if err != nil {
        return CertificateResource{}, err
    }

    x509Cert := certificates[0]
    if x509Cert.IsCA {
        return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain)
    }

    // This is just meant to be informal for the user.
    timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
    logf("[INFO][%s] acme: Trying renewal 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 := httpGet(cert.CertURL)
    if err != nil {
        return CertificateResource{}, err
    }
    defer resp.Body.Close()
    serverCertBytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return CertificateResource{}, err
    }

    serverCert, err := x509.ParseCertificate(serverCertBytes)
    if err != nil {
        return CertificateResource{}, 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) {
        logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain)
        issuedCert := pemEncode(derCertificateBytes(serverCertBytes))
        // If bundle is true, we want to return a certificate bundle.
        // To do this, we need the issuer certificate.
        if bundle {
            // The issuer certificate link is always supplied via an "up" link
            // in the response headers of a new certificate.
            links := parseLinks(resp.Header["Link"])
            issuerCert, err := c.getIssuerCertificate(links["up"])
            if err != nil {
                // If we fail to acquire the issuer cert, return the issued certificate - do not fail.
                logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err)
            } else {
                // Success - append the issuer cert to the issued cert.
                issuerCert = pemEncode(derCertificateBytes(issuerCert))
                issuedCert = append(issuedCert, issuerCert...)
            }
        }

        cert.Certificate = issuedCert
        return cert, nil
    }

    // If the certificate is the same, then we need to request a new certificate.
    // Start by checking to see if the certificate was based off a CSR, and
    // use that if it's defined.
    if len(cert.CSR) > 0 {
        csr, err := pemDecodeTox509CSR(cert.CSR)
        if err != nil {
            return CertificateResource{}, err
        }
        newCert, failures := c.ObtainCertificateForCSR(*csr, bundle)
        return newCert, failures[cert.Domain]
    }

    var privKey crypto.PrivateKey
    if cert.PrivateKey != nil {
        privKey, err = parsePEMPrivateKey(cert.PrivateKey)
        if err != nil {
            return CertificateResource{}, err
        }
    }

    var domains []string
    var failures map[string]error
    // check for SAN certificate
    if len(x509Cert.DNSNames) > 1 {
        domains = append(domains, x509Cert.Subject.CommonName)
        for _, sanDomain := range x509Cert.DNSNames {
            if sanDomain == x509Cert.Subject.CommonName {
                continue
            }
            domains = append(domains, sanDomain)
        }
    } else {
        domains = append(domains, x509Cert.Subject.CommonName)
    }

    newCert, failures := c.ObtainCertificate(domains, bundle, privKey)
    return newCert, failures[cert.Domain]
}

My problem is , when I try to renew, I always get the same certificate, but the cert's expire date never changed, so I think this is a result of failing renew?

Suggest reverting encoding/base64 vendoring

In 9be2964 you vendored encoding/base64, but what this means is that the entire golang repo gets cloned, which takes quite a while to download.

Suggest either just requiring 1.5+ or if not, copying the required code into an internal package.

Renewing Certificates requires certificate url

Do you have any examples for renewing certificates? From what I can tell, you need the certificate URL returned when requesting the certificate. The library doesn't currently return that.

I'd be helpful to have the Location and Content-Location headers returned from calling NewCertificate. I'd like to store those. What do you think about changing the method signature to:

NewCertificate(accountKey interface{}, csr *x509.CertificateRequest) (*x509.Certificate, Location string, ContentLocation string, error) {

or

NewCertificate(accountKey interface{}, csr *x509.CertificateRequest) (*x509.Certificate, http.Header, error) {

I'm having trouble getting boot2docker to actually forward requests to my host machine so I can verify challenges and get to that step, so haven't been able to verify (otherwise I'd submit a PR).

Please rename project as it violates ISRG trademark

We (ISRG / Let's Encrypt) do not allow clients to use our service's name in their client name. We recommend you use the term "ACME" instead, in reference to the protocol. This is better because there are no trademark issues and the protocol is not specific to our CA so your client should work with other ACME-enabled CAs in the future.

I know this can be a pain but we have to insist that the name change. Thanks for understanding.

Authorization.Combinations appears to be broken

We are getting 0 (zero) supported challenges returned from Authorization.Challenges depending on what order the challenges are listed in the response from Boulder (using the Let's Encrypt "staging" server).

We are calling:

chals := auth.Combinations(letsencrypt.ChallengeHTTP)

and getting an empty slice in return.

Here is a %#v dump of the Authorization struct from Boulder, where the library successfully finds the HTTP challenge:

2015/11/24 13:45:25 Challenges: letsencrypt.Authorization{Identifier:struct { Type string "json:"type""; Value string "json:"value"" }{Type:"dns", Value:"MYDOMAIN.TLD"}, Status:"pending", Expires:time.Time{sec:63584534725, nsec:669278071, loc:(_time.Location)(0xa40800)}, Challenges:[]letsencrypt.Challenge{letsencrypt.Challenge{ID:0, Type:"tls-sni-01", URI:"https://acme-staging.api.letsencrypt.org/acme/challenge/Phcunxr4uNcjMr_uwXi_UNy-Aj-TThKDY4_qQYyEiIc/736693", Status:"pending", Validated:time.Time{sec:0, nsec:0, loc:(_time.Location)(nil)}, Error:(_letsencrypt.Error)(nil), Token:"_CYm7ZGvn-B4HT-09wcfjKM_RDrZsjOk0EX9W9yfvDU", KeyAuthorization:"", N:0, Certs:[]string(nil), AccountKey:(_jose.JsonWebKey)(nil), Authorization:(_letsencrypt.JWSValidation)(nil)}, letsencrypt.Challenge{ID:0, Type:"http-01", URI:"https://acme-staging.api.letsencrypt.org/acme/challenge/Phcunxr4uNcjMr_uwXi_UNy-Aj-TThKDY4_qQYyEiIc/736694", Status:"pending", Validated:time.Time{sec:0, nsec:0, loc:(_time.Location)(nil)}, Error:(_letsencrypt.Error)(nil), Token:"bvLXXINiw2b-fYU4NIA3aqG5d-CkUyhCzvs74dnlO2U", KeyAuthorization:"", N:0, Certs:[]string(nil), AccountKey:(_jose.JsonWebKey)(nil), Authorization:(*letsencrypt.JWSValidation)(nil)}}, Combs:[][]int{[]int{0}, []int{1}}}

And here is one that fails to find the http-01 type, even though it is in there.

2015/11/24 13:43:57 Challenges: letsencrypt.Authorization{Identifier:struct { Type string "json:"type""; Value string "json:"value"" }{Type:"dns", Value:"MYDOMAIN.TLD"}, Status:"pending", Expires:time.Time{sec:63584534637, nsec:599494120, loc:(_time.Location)(0xa40800)}, Challenges:[]letsencrypt.Challenge{letsencrypt.Challenge{ID:0, Type:"http-01", URI:"https://acme-staging.api.letsencrypt.org/acme/challenge/tcxd_fNpMb4ux7JW_3VBLsJltGaNefhi0fv_sX0i8N0/736689", Status:"pending", Validated:time.Time{sec:0, nsec:0, loc:(_time.Location)(nil)}, Error:(_letsencrypt.Error)(nil), Token:"EMX9y_ge-HBej8zzt2HttUhX8_QeOI_HoUADJlNSXn8", KeyAuthorization:"", N:0, Certs:[]string(nil), AccountKey:(_jose.JsonWebKey)(nil), Authorization:(_letsencrypt.JWSValidation)(nil)}, letsencrypt.Challenge{ID:0, Type:"tls-sni-01", URI:"https://acme-staging.api.letsencrypt.org/acme/challenge/tcxd_fNpMb4ux7JW_3VBLsJltGaNefhi0fv_sX0i8N0/736690", Status:"pending", Validated:time.Time{sec:0, nsec:0, loc:(_time.Location)(nil)}, Error:(_letsencrypt.Error)(nil), Token:"xp9XshTGRC0wRb8jGqp2gukphD9Purh5h9Wpk84jP9Y", KeyAuthorization:"", N:0, Certs:[]string(nil), AccountKey:(_jose.JsonWebKey)(nil), Authorization:(*letsencrypt.JWSValidation)(nil)}}, Combs:[][]int{[]int{1}, []int{0}}}

Godeps path

In this file: https://github.com/ericchiang/letsencrypt/blob/master/Godeps/Godeps.json#L2

Shouldn't the Godeps ImportPath be:

github.com/ericchiang/letsencrypt rather than github.com/ericchiang/acme?

When I run go get, all of the import paths are referencing the non-existent "github.com/ericchiang/acme/Godeps/_workspace/src/github.com/square/go-jose/cipher" (for example) path. I had to manually change to "github.com/ericchiang/letsencrypt/Godeps/_workspace/src/github.com/square/go-jose/cipher" to get things working (for all Godeps imports).

Contact submission workflow?

I'm a bit confused how the contact flow works here. The ACME RFC protocol overview says that the ACME client should send along human contact information in the initial new registration request (https://letsencrypt.github.io/acme-spec/#rfc.section.4) but it seems like the only way to get the ACME server contact information with this library is to register a new key, then set the contact information on the returned Registration object, then update the registration.

I assume I'm more likely to be wrong about this than right, so what am I missing?

Thanks!

State that Go 1.5 is required

You may want to state in the README that Go 1.5 is required, as you are using a member of encoding/base64 that was introduced in 1.5.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.