Skip to content

Instantly share code, notes, and snippets.

@nathan-osman
Created April 20, 2017 02:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nathan-osman/cb501a32a8727f922852388cc622c2a6 to your computer and use it in GitHub Desktop.
Save nathan-osman/cb501a32a8727f922852388cc622c2a6 to your computer and use it in GitHub Desktop.
Obtain a Let's Encrypt certificate from the ACME staging server using golang.org/x/crypto/acme
package main
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
"golang.org/x/crypto/acme"
)
const (
addr = ":80"
domain = "example.com"
keyLength = 2048
keyType = "RSA PRIVATE KEY"
certType = "CERTIFICATE"
stagingURL = "https://acme-staging.api.letsencrypt.org/directory"
)
func newKey(filename string) (crypto.Signer, error) {
log.Printf("generating %d-bit RSA key...", keyLength)
k, err := rsa.GenerateKey(rand.Reader, keyLength)
if err != nil {
return nil, err
}
b := pem.EncodeToMemory(&pem.Block{
Type: keyType,
Bytes: x509.MarshalPKCS1PrivateKey(k),
})
if err = ioutil.WriteFile(filename, b, 0600); err != nil {
log.Fatal(err)
}
log.Print("generated RSA key")
return k, nil
}
func main() {
// Create a temporary directory
d, err := ioutil.TempDir("/tmp", "")
if err != nil {
log.Fatal(err)
}
log.Printf("created \"%s\"", d)
// Generate account key
aKey, err := newKey(path.Join(d, "account.pem"))
if err != nil {
log.Fatal(err)
}
// Create the ACME client
c := &acme.Client{
Key: aKey,
DirectoryURL: stagingURL,
}
// Begin with registration
log.Print("attempting registration...")
_, err = c.Register(context.TODO(), nil, func(string) bool { return true })
if err != nil {
log.Fatal(err)
}
log.Print("registration succeeded")
// Attempt authorization
auth, err := c.Authorize(context.TODO(), domain)
if err != nil {
log.Fatal(err)
}
var challenge *acme.Challenge
for _, c := range auth.Challenges {
if c.Type == "http-01" {
challenge = c
}
}
if challenge == nil {
log.Fatal("no HTTP challenge found")
}
// Determine the correct path to listen on
cPath := c.HTTP01ChallengePath(challenge.Token)
cResponse, err := c.HTTP01ChallengeResponse(challenge.Token)
if err != nil {
log.Fatal(err)
}
// Create a server that responds to the request
mux := http.NewServeMux()
mux.HandleFunc(cPath, func(w http.ResponseWriter, r *http.Request) {
b := []byte(cResponse)
w.Header().Set("Content-Length", strconv.Itoa(len(b)))
w.WriteHeader(http.StatusOK)
w.Write(b)
})
l, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal(err)
}
defer l.Close()
go func() {
http.Serve(l, mux)
}()
// Perform the challenge
log.Print("performing challenge...")
_, err = c.Accept(context.TODO(), challenge)
if err != nil {
log.Fatal(err)
}
// Wait for authorization to complete
_, err = c.WaitAuthorization(context.TODO(), auth.URI)
if err != nil {
log.Fatal(err)
}
log.Print("challenge completed")
uDomain := strings.Replace(domain, ".", "_", -1)
// Generate a key for the domain
dKey, err := newKey(fmt.Sprintf("%s.key", path.Join(d, uDomain)))
if err != nil {
log.Fatal(err)
}
// Create the CSR (certificate signing request)
csr, err := x509.CreateCertificateRequest(
rand.Reader,
&x509.CertificateRequest{
Subject: pkix.Name{CommonName: domain},
},
dKey,
)
if err != nil {
log.Fatal(err)
}
// Send the CSR and obtain the certificate
log.Print("signing the certificate")
ders, _, err := c.CreateCert(context.TODO(), csr, 90*24*time.Hour, true)
if err != nil {
log.Fatal(err)
}
log.Print("certificate signed!")
// Write the certificate bundle to disk
w, err := os.Create(path.Join(
d, fmt.Sprintf("%s.crt", uDomain),
))
if err != nil {
log.Fatal(err)
}
defer w.Close()
for _, der := range ders {
err := pem.Encode(w, &pem.Block{
Type: certType,
Bytes: der,
})
if err != nil {
log.Fatal(err)
}
}
log.Print("complete!")
}
@ann-kilzer
Copy link

ann-kilzer commented Apr 26, 2018

Thanks! This is really helpful. I found that I didn't need to provide a key in my code, only provide a value to acme.Client.DirectoryURL for the existing autocert.Manager. In the source file acme.go, the url only points to prod if nothing is specified:

if dirURL == "" {
	dirURL = LetsEncryptURL
}

So my solution looks like:

const stagingURL = "https://acme-staging.api.letsencrypt.org/directory"
...
certManager.Client = &acme.Client{
		DirectoryURL: stagingURL,
	}

@leslie-wang
Copy link

Thanks for sharing the sample. seems like need upgrade to newer version

2020/03/14 13:27:13 403 urn:acme:error:unauthorized: Account creation on ACMEv1 is disabled. Please upgrade your ACME client to a version that supports ACMEv2 / RFC 8555. See https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430 for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment