123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- // Copyright 2019 The Go Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- // The acmeprober program runs against an actual ACME CA implementation.
- // It spins up an HTTP server to fulfill authorization challenges
- // or execute a DNS script to provision a response to dns-01 challenge.
- //
- // For http-01 and tls-alpn-01 challenge types this requires the ACME CA
- // to be able to reach the HTTP server.
- //
- // A usage example:
- //
- // go run prober.go \
- // -d https://acme-staging-v02.api.letsencrypt.org/directory \
- // -f order \
- // -t http-01 \
- // -a :8080 \
- // -domain some.example.org
- //
- // The above assumes a TCP tunnel from some.example.org:80 to 0.0.0.0:8080
- // in order for the test to be able to fulfill http-01 challenge.
- // To test tls-alpn-01 challenge, 443 port would need to be tunneled
- // to 0.0.0.0:8080.
- // When running with dns-01 challenge type, use -s argument instead of -a.
- package main
- import (
- "context"
- "crypto"
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/tls"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "flag"
- "fmt"
- "log"
- "net"
- "net/http"
- "os"
- "os/exec"
- "strings"
- "time"
- "golang.org/x/crypto/acme"
- )
- var (
- // ACME CA directory URL.
- // Let's Encrypt v1 prod: https://acme-v01.api.letsencrypt.org/directory
- // Let's Encrypt v2 prod: https://acme-v02.api.letsencrypt.org/directory
- // Let's Encrypt v2 staging: https://acme-staging-v02.api.letsencrypt.org/directory
- // See the following for more CAs implementing ACME protocol:
- // https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment#CAs_&_PKIs_that_offer_ACME_certificates
- directory = flag.String("d", "", "ACME directory URL.")
- reginfo = flag.String("r", "", "ACME account registration info.")
- flow = flag.String("f", "", "Flow to run: order, preauthz (RFC8555) or preauthz02 (draft-02).")
- chaltyp = flag.String("t", "", "Challenge type: tls-alpn-01, http-01 or dns-01.")
- addr = flag.String("a", "", "Local server address for tls-alpn-01 and http-01.")
- dnsscript = flag.String("s", "", "Script to run for provisioning dns-01 challenges.")
- domain = flag.String("domain", "", "Space separate domain identifiers.")
- ipaddr = flag.String("ip", "", "Space separate IP address identifiers.")
- )
- func main() {
- flag.Usage = func() {
- fmt.Fprintln(flag.CommandLine.Output(), `
- The prober program runs against an actual ACME CA implementation.
- It spins up an HTTP server to fulfill authorization challenges
- or execute a DNS script to provision a response to dns-01 challenge.
- For http-01 and tls-alpn-01 challenge types this requires the ACME CA
- to be able to reach the HTTP server.
- A usage example:
- go run prober.go \
- -d https://acme-staging-v02.api.letsencrypt.org/directory \
- -f order \
- -t http-01 \
- -a :8080 \
- -domain some.example.org
- The above assumes a TCP tunnel from some.example.org:80 to 0.0.0.0:8080
- in order for the test to be able to fulfill http-01 challenge.
- To test tls-alpn-01 challenge, 443 port would need to be tunneled
- to 0.0.0.0:8080.
- When running with dns-01 challenge type, use -s argument instead of -a.
- `)
- flag.PrintDefaults()
- }
- flag.Parse()
- identifiers := acme.DomainIDs(strings.Fields(*domain)...)
- identifiers = append(identifiers, acme.IPIDs(strings.Fields(*ipaddr)...)...)
- if len(identifiers) == 0 {
- log.Fatal("at least one domain or IP addr identifier is required")
- }
- // Duration of the whole run.
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
- defer cancel()
- // Create and register a new account.
- akey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- log.Fatal(err)
- }
- cl := &acme.Client{Key: akey, DirectoryURL: *directory}
- a := &acme.Account{Contact: strings.Fields(*reginfo)}
- if _, err := cl.Register(ctx, a, acme.AcceptTOS); err != nil {
- log.Fatalf("Register: %v", err)
- }
- // Run the desired flow test.
- p := &prober{
- client: cl,
- chalType: *chaltyp,
- localAddr: *addr,
- dnsScript: *dnsscript,
- }
- switch *flow {
- case "order":
- p.runOrder(ctx, identifiers)
- case "preauthz":
- p.runPreauthz(ctx, identifiers)
- case "preauthz02":
- p.runPreauthzLegacy(ctx, identifiers)
- default:
- log.Fatalf("unknown flow: %q", *flow)
- }
- if len(p.errors) > 0 {
- os.Exit(1)
- }
- }
- type prober struct {
- client *acme.Client
- chalType string
- localAddr string
- dnsScript string
- errors []error
- }
- func (p *prober) errorf(format string, a ...interface{}) {
- err := fmt.Errorf(format, a...)
- log.Print(err)
- p.errors = append(p.errors, err)
- }
- func (p *prober) runOrder(ctx context.Context, identifiers []acme.AuthzID) {
- // Create a new order and pick a challenge.
- // Note that Let's Encrypt will reply with 400 error:malformed
- // "NotBefore and NotAfter are not supported" when providing a NotAfter
- // value like WithOrderNotAfter(time.Now().Add(24 * time.Hour)).
- o, err := p.client.AuthorizeOrder(ctx, identifiers)
- if err != nil {
- log.Fatalf("AuthorizeOrder: %v", err)
- }
- var zurls []string
- for _, u := range o.AuthzURLs {
- z, err := p.client.GetAuthorization(ctx, u)
- if err != nil {
- log.Fatalf("GetAuthorization(%q): %v", u, err)
- }
- log.Printf("%+v", z)
- if z.Status != acme.StatusPending {
- log.Printf("authz status is %q; skipping", z.Status)
- continue
- }
- if err := p.fulfill(ctx, z); err != nil {
- log.Fatalf("fulfill(%s): %v", z.URI, err)
- }
- zurls = append(zurls, z.URI)
- log.Printf("authorized for %+v", z.Identifier)
- }
- log.Print("all challenges are done")
- if _, err := p.client.WaitOrder(ctx, o.URI); err != nil {
- log.Fatalf("WaitOrder(%q): %v", o.URI, err)
- }
- csr, certkey := newCSR(identifiers)
- der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
- if err != nil {
- log.Fatalf("CreateOrderCert: %v", err)
- }
- log.Printf("cert URL: %s", curl)
- if err := checkCert(der, identifiers); err != nil {
- p.errorf("invalid cert: %v", err)
- }
- // Deactivate all authorizations we satisfied earlier.
- for _, v := range zurls {
- if err := p.client.RevokeAuthorization(ctx, v); err != nil {
- p.errorf("RevokAuthorization(%q): %v", v, err)
- continue
- }
- }
- // Deactivate the account. We don't need it for any further calls.
- if err := p.client.DeactivateReg(ctx); err != nil {
- p.errorf("DeactivateReg: %v", err)
- }
- // Try revoking the issued cert using its private key.
- if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
- p.errorf("RevokeCert: %v", err)
- }
- }
- func (p *prober) runPreauthz(ctx context.Context, identifiers []acme.AuthzID) {
- dir, err := p.client.Discover(ctx)
- if dir.AuthzURL == "" {
- log.Fatal("CA does not support pre-authorization")
- }
- var zurls []string
- for _, id := range identifiers {
- z, err := authorize(ctx, p.client, id)
- if err != nil {
- log.Fatalf("AuthorizeID(%+v): %v", z, err)
- }
- if z.Status == acme.StatusValid {
- log.Printf("authz %s is valid; skipping", z.URI)
- continue
- }
- if err := p.fulfill(ctx, z); err != nil {
- log.Fatalf("fulfill(%s): %v", z.URI, err)
- }
- zurls = append(zurls, z.URI)
- log.Printf("authorized for %+v", id)
- }
- // We should be all set now.
- // Expect all authorizations to be satisfied.
- log.Print("all challenges are done")
- o, err := p.client.AuthorizeOrder(ctx, identifiers)
- if err != nil {
- log.Fatalf("AuthorizeOrder: %v", err)
- }
- waitCtx, cancel := context.WithTimeout(ctx, time.Minute)
- defer cancel()
- if _, err := p.client.WaitOrder(waitCtx, o.URI); err != nil {
- log.Fatalf("WaitOrder(%q): %v", o.URI, err)
- }
- csr, certkey := newCSR(identifiers)
- der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
- if err != nil {
- log.Fatalf("CreateOrderCert: %v", err)
- }
- log.Printf("cert URL: %s", curl)
- if err := checkCert(der, identifiers); err != nil {
- p.errorf("invalid cert: %v", err)
- }
- // Deactivate all authorizations we satisfied earlier.
- for _, v := range zurls {
- if err := p.client.RevokeAuthorization(ctx, v); err != nil {
- p.errorf("RevokeAuthorization(%q): %v", v, err)
- continue
- }
- }
- // Deactivate the account. We don't need it for any further calls.
- if err := p.client.DeactivateReg(ctx); err != nil {
- p.errorf("DeactivateReg: %v", err)
- }
- // Try revoking the issued cert using its private key.
- if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
- p.errorf("RevokeCert: %v", err)
- }
- }
- func (p *prober) runPreauthzLegacy(ctx context.Context, identifiers []acme.AuthzID) {
- var zurls []string
- for _, id := range identifiers {
- z, err := authorize(ctx, p.client, id)
- if err != nil {
- log.Fatalf("AuthorizeID(%+v): %v", id, err)
- }
- if z.Status == acme.StatusValid {
- log.Printf("authz %s is valid; skipping", z.URI)
- continue
- }
- if err := p.fulfill(ctx, z); err != nil {
- log.Fatalf("fulfill(%s): %v", z.URI, err)
- }
- zurls = append(zurls, z.URI)
- log.Printf("authorized for %+v", id)
- }
- // We should be all set now.
- log.Print("all authorizations are done")
- csr, certkey := newCSR(identifiers)
- der, curl, err := p.client.CreateCert(ctx, csr, 48*time.Hour, true)
- if err != nil {
- log.Fatalf("CreateCert: %v", err)
- }
- log.Printf("cert URL: %s", curl)
- if err := checkCert(der, identifiers); err != nil {
- p.errorf("invalid cert: %v", err)
- }
- // Deactivate all authorizations we satisfied earlier.
- for _, v := range zurls {
- if err := p.client.RevokeAuthorization(ctx, v); err != nil {
- p.errorf("RevokAuthorization(%q): %v", v, err)
- continue
- }
- }
- // Try revoking the issued cert using its private key.
- if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
- p.errorf("RevokeCert: %v", err)
- }
- }
- func (p *prober) fulfill(ctx context.Context, z *acme.Authorization) error {
- var chal *acme.Challenge
- for i, c := range z.Challenges {
- log.Printf("challenge %d: %+v", i, c)
- if c.Type == p.chalType {
- log.Printf("picked %s for authz %s", c.URI, z.URI)
- chal = c
- }
- }
- if chal == nil {
- return fmt.Errorf("challenge type %q wasn't offered for authz %s", p.chalType, z.URI)
- }
- switch chal.Type {
- case "tls-alpn-01":
- return p.runTLSALPN01(ctx, z, chal)
- case "http-01":
- return p.runHTTP01(ctx, z, chal)
- case "dns-01":
- return p.runDNS01(ctx, z, chal)
- default:
- return fmt.Errorf("unknown challenge type %q", chal.Type)
- }
- }
- func (p *prober) runTLSALPN01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
- tokenCert, err := p.client.TLSALPN01ChallengeCert(chal.Token, z.Identifier.Value)
- if err != nil {
- return fmt.Errorf("TLSALPN01ChallengeCert: %v", err)
- }
- s := &http.Server{
- Addr: p.localAddr,
- TLSConfig: &tls.Config{
- NextProtos: []string{acme.ALPNProto},
- GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
- log.Printf("hello: %+v", hello)
- return &tokenCert, nil
- },
- },
- Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- log.Printf("%s %s", r.Method, r.URL)
- w.WriteHeader(http.StatusNotFound)
- }),
- }
- go s.ListenAndServeTLS("", "")
- defer s.Close()
- if _, err := p.client.Accept(ctx, chal); err != nil {
- return fmt.Errorf("Accept(%q): %v", chal.URI, err)
- }
- _, zerr := p.client.WaitAuthorization(ctx, z.URI)
- return zerr
- }
- func (p *prober) runHTTP01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
- body, err := p.client.HTTP01ChallengeResponse(chal.Token)
- if err != nil {
- return fmt.Errorf("HTTP01ChallengeResponse: %v", err)
- }
- s := &http.Server{
- Addr: p.localAddr,
- Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- log.Printf("%s %s", r.Method, r.URL)
- if r.URL.Path != p.client.HTTP01ChallengePath(chal.Token) {
- w.WriteHeader(http.StatusNotFound)
- return
- }
- w.Write([]byte(body))
- }),
- }
- go s.ListenAndServe()
- defer s.Close()
- if _, err := p.client.Accept(ctx, chal); err != nil {
- return fmt.Errorf("Accept(%q): %v", chal.URI, err)
- }
- _, zerr := p.client.WaitAuthorization(ctx, z.URI)
- return zerr
- }
- func (p *prober) runDNS01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
- token, err := p.client.DNS01ChallengeRecord(chal.Token)
- if err != nil {
- return fmt.Errorf("DNS01ChallengeRecord: %v", err)
- }
- name := fmt.Sprintf("_acme-challenge.%s", z.Identifier.Value)
- cmd := exec.CommandContext(ctx, p.dnsScript, name, token)
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("%s: %v", p.dnsScript, err)
- }
- if _, err := p.client.Accept(ctx, chal); err != nil {
- return fmt.Errorf("Accept(%q): %v", chal.URI, err)
- }
- _, zerr := p.client.WaitAuthorization(ctx, z.URI)
- return zerr
- }
- func authorize(ctx context.Context, client *acme.Client, id acme.AuthzID) (*acme.Authorization, error) {
- if id.Type == "ip" {
- return client.AuthorizeIP(ctx, id.Value)
- }
- return client.Authorize(ctx, id.Value)
- }
- func checkCert(derChain [][]byte, id []acme.AuthzID) error {
- if len(derChain) == 0 {
- return errors.New("cert chain is zero bytes")
- }
- for i, b := range derChain {
- crt, err := x509.ParseCertificate(b)
- if err != nil {
- return fmt.Errorf("%d: ParseCertificate: %v", i, err)
- }
- log.Printf("%d: serial: 0x%s", i, crt.SerialNumber)
- log.Printf("%d: subject: %s", i, crt.Subject)
- log.Printf("%d: issuer: %s", i, crt.Issuer)
- log.Printf("%d: expires in %.1f day(s)", i, time.Until(crt.NotAfter).Hours()/24)
- if i > 0 { // not a leaf cert
- continue
- }
- p := &pem.Block{Type: "CERTIFICATE", Bytes: b}
- log.Printf("%d: leaf:\n%s", i, pem.EncodeToMemory(p))
- for _, v := range id {
- if err := crt.VerifyHostname(v.Value); err != nil {
- return err
- }
- }
- }
- return nil
- }
- func newCSR(identifiers []acme.AuthzID) ([]byte, crypto.Signer) {
- var csr x509.CertificateRequest
- for _, id := range identifiers {
- switch id.Type {
- case "dns":
- csr.DNSNames = append(csr.DNSNames, id.Value)
- case "ip":
- csr.IPAddresses = append(csr.IPAddresses, net.ParseIP(id.Value))
- default:
- panic(fmt.Sprintf("newCSR: unknown identifier type %q", id.Type))
- }
- }
- k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- panic(fmt.Sprintf("newCSR: ecdsa.GenerateKey for a cert: %v", err))
- }
- b, err := x509.CreateCertificateRequest(rand.Reader, &csr, k)
- if err != nil {
- panic(fmt.Sprintf("newCSR: x509.CreateCertificateRequest: %v", err))
- }
- return b, k
- }
|