Created
April 20, 2017 02:22
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!") | |
} |
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
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:
So my solution looks like: