letsencrypt

package module
v0.0.0-...-0367c87 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 12, 2018 License: Apache-2.0 Imports: 25 Imported by: 9

README

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.

Documentation

Overview

Package letsencrypt implements an ACME client.

Index

Constants

View Source
const (
	ChallengeDNS    = "dns-01"
	ChallengeHTTP   = "http-01"
	ChallengeTLSSNI = "tls-sni-01"
)
View Source
const (
	StatusPending = "pending"
	StatusInvalid = "invalid"
	StatusValid   = "valid"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Authorization

type Authorization struct {
	Identifier struct {
		Type  string `json:"type"`
		Value string `json:"value"`
	} `json:"identifier"`

	Status     string      `json:"status,omitempty"`
	Expires    time.Time   `json:"expires,omitempty"`
	Challenges []Challenge `json:"challenges,omitempty"`
	Combs      [][]int     `json:"combinations,omitempty"`
}

Authorization represents a set of challenges issued by the server for the given identifier.

func (Authorization) Combinations

func (a Authorization) Combinations(supportedChallenges ...string) [][]Challenge

Combinations returns the set of challenges which the client supports. Completing one of these sets is enough to prove ownership of an identifier.

type CertificateResponse

type CertificateResponse struct {
	Certificate *x509.Certificate
	RetryAfter  int
	URI         string
	StableURI   string
	Issuer      string
}

CertificateResponse holds response items after requesting a Certificate.

func (*CertificateResponse) IsAvailable

func (c *CertificateResponse) IsAvailable() bool

IsAvailable returns bool true if CertificateResponse has a certificate available. It's a convenience function, but it helps with readability.

type Challenge

type Challenge struct {
	ID        int64     `json:"id,omitempty"`
	Type      string    `json:"type"`
	URI       string    `json:"uri"`
	Status    string    `json:"status,omitempty"`
	Validated time.Time `json:"validated,omitempty"`
	Error     *Error    `json:"error,omitempty"`

	// Data used by various challenges
	Token            string           `json:"token,omitempty"`
	KeyAuthorization string           `json:"keyAuthorization,omitempty"`
	N                int              `json:"n,omitempty"`
	Certs            []string         `json:"certs,omitempty"`
	AccountKey       *jose.JsonWebKey `json:"accountKey,omitempty"`
	Authorization    *JWSValidation   `json:"authorization,omitempty"`
}

Challenge represents a server challenge for a given domain name.

func (Challenge) DNS

func (chal Challenge) DNS(accountKey interface{}) (subdomain, txt string, err error)

DNS returns the subdomain name and the TXT value you need to set for that subdomain. The ACME server will make DNS TXT lookup on that subdomain and verify that the value matches. Keep in mind that DNS TTL's might prevent the lookup from working correctly the first few times and ChallengeReady will continue to loop if the record is missing/invalid. It is recommended that you set the record to the lowest TTL allowed by your provider.

func (Challenge) HTTP

func (chal Challenge) HTTP(accountKey interface{}) (urlPath, resource string, err error)

HTTP returns a URL path and HTTP response body that the ACME server will check when verifying the challenge.

func (Challenge) ProofOfPossession

func (chal Challenge) ProofOfPossession(accountKey, certKey interface{}) (Challenge, error)

Not yet implemented

func (Challenge) TLSSNI

func (chal Challenge) TLSSNI(accountKey interface{}) (map[string]*tls.Certificate, error)

TLSSNI returns TLS certificates for a set of server names. The ACME server will make a TLS Server Name Indication handshake with the given domain. The domain must present the returned certifiate for each name.

type Client

type Client struct {
	// PollInterval determines how quickly the client will
	// request updates on a challenge from the ACME server.
	// If unspecified, it defaults to 500 milliseconds.
	PollInterval time.Duration
	// Amount of time after the client notifies the server a challenge is
	// ready, and when it will stop checking for updates.
	// If unspecified, it defaults to 30 seconds.
	PollTimeout time.Duration
	// contains filtered or unexported fields
}

Client is a client for a single ACME server.

func NewClient

func NewClient(directoryURL string) (*Client, error)

NewClient creates a client of a ACME server by querying the server's resource directory and attempting to resolve the URL of the terms of service.

func NewClientWithTransport

func NewClientWithTransport(directoryURL string, t http.RoundTripper) (*Client, error)

NewClientWithTransport creates a client of a ACME server by querying the server's resource directory and attempting to resolve the URL of the terms of service.

func (*Client) Authorization

func (c *Client) Authorization(authURI string) (Authorization, error)

Authorization returns the authorization object associated with the given authorization URI.

func (*Client) Bundle

func (c *Client) Bundle(certResp *CertificateResponse) (bundledPEM []byte, err error)

Bundle bundles the certificate with the issuer certificate.

func (*Client) Challenge

func (c *Client) Challenge(chalURI string) (Challenge, error)

Challenge returns the challenge object associated with the given challenge URI.

func (*Client) ChallengeReady

func (c *Client) ChallengeReady(accountKey interface{}, chal Challenge) error

ChallengeReady informs the server that the provided challenge is ready for verification.

The client then begins polling the server for confirmation on the result of the status.

func (*Client) NewAuthorization

func (c *Client) NewAuthorization(accountKey interface{}, typ, val string) (auth Authorization, authURL string, err error)

NewAuthorization requests a set of challenges from the server to prove ownership of a given resource. Only known type is 'dns'.

NOTE: Currently the only way to recover an authorization object is with the returned authorization URL.

func (*Client) NewCertificate

func (c *Client) NewCertificate(accountKey interface{}, csr *x509.CertificateRequest) (*CertificateResponse, error)

NewCertificate requests a certificate from the ACME server.

csr must have already been signed by a private key.

func (*Client) NewRegistration

func (c *Client) NewRegistration(accountKey interface{}) (reg Registration, err error)

NewRegistration registers a key pair with the ACME server. If the key pair is already registered, the registration object is recovered.

func (*Client) RenewCertificate

func (c *Client) RenewCertificate(certURI string) (*CertificateResponse, error)

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 (*Client) Retry

func (c *Client) Retry(certResp *CertificateResponse) error

Retry request retries the certificate if it was unavailable when calling NewCertificate or RenewCertificate.

Note: If you are renewing a certificate, LetsEncrypt 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 request a new certificate using NewCertificate, or if your chalenges have expired, start a new certificate flow entirely.

func (*Client) RevokeCertificate

func (c *Client) RevokeCertificate(accountKey interface{}, pemBytes []byte) error

RevokeCertificate takes a PEM encoded certificate or bundle and attempts to revoke it.

func (*Client) Terms

func (c *Client) Terms() string

Terms returns the URL of the server's terms of service. All accounts registered using this client automatically accept these terms.

func (*Client) UpdateRegistration

func (c *Client) UpdateRegistration(accountKey interface{}, reg Registration) (Registration, error)

UpdateRegistration sends the updated registration object to the server.

type Error

type Error struct {
	Typ    string `json:"type"`
	Status int    `json:"status"`
	Detail string `json:"detail"`
}

A HTTP error generated by the ACME server.

func (*Error) Error

func (err *Error) Error() string

type JWSValidation

type JWSValidation struct {
	Header    *jose.JsonWebKey `json:"header,omitempty"`
	Payload   string           `json:"payload,omitempty"`
	Signature string           `json:"signature,omitempty"`
}

type Registration

type Registration struct {
	PublicKey      *jose.JsonWebKey `json:"key,omitempty"`
	Contact        []string         `json:"contact,omitempty"`
	Agreement      string           `json:"agreement,omitempty"`
	Authorizations string           `json:"authorizations,omitempty"`
	Certificates   string           `json:"certificates,omitempty"`

	Id        int       `json:"id,omitempty"`
	InitialIp string    `json:"initialIp,omitempty"`
	CreatedAt time.Time `json:"createdAt,omitempty"`

	Resource string `json:"resource,omitempty"`
}

Registration holds account information for a given key pair.

Directories

Path Synopsis
internal
base64
Package base64 implements base64 encoding as specified by RFC 4648.
Package base64 implements base64 encoding as specified by RFC 4648.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL