blob: 55d702b76a2f70f03d471aff7f80f6a031c4fa8c [file] [log] [blame]
Alex Vaghinf9e20702019-09-11 10:47:38 -04001// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// The acmeprober program runs against an actual ACME CA implementation.
6// It spins up an HTTP server to fulfill authorization challenges
7// or execute a DNS script to provision a response to dns-01 challenge.
8//
9// For http-01 and tls-alpn-01 challenge types this requires the ACME CA
10// to be able to reach the HTTP server.
11//
12// A usage example:
13//
14// go run prober.go \
15// -d https://acme-staging-v02.api.letsencrypt.org/directory \
16// -f order \
17// -t http-01 \
18// -a :8080 \
19// -domain some.example.org
20//
21// The above assumes a TCP tunnel from some.example.org:80 to 0.0.0.0:8080
22// in order for the test to be able to fulfill http-01 challenge.
23// To test tls-alpn-01 challenge, 443 port would need to be tunneled
24// to 0.0.0.0:8080.
25// When running with dns-01 challenge type, use -s argument instead of -a.
26package main
27
28import (
29 "context"
30 "crypto"
31 "crypto/ecdsa"
32 "crypto/elliptic"
33 "crypto/rand"
34 "crypto/tls"
35 "crypto/x509"
36 "encoding/pem"
37 "errors"
38 "flag"
39 "fmt"
40 "log"
41 "net"
42 "net/http"
43 "os"
44 "os/exec"
45 "strings"
46 "time"
47
48 "golang.org/x/crypto/acme"
49)
50
51var (
52 // ACME CA directory URL.
53 // Let's Encrypt v1 prod: https://acme-v01.api.letsencrypt.org/directory
54 // Let's Encrypt v2 prod: https://acme-v02.api.letsencrypt.org/directory
55 // Let's Encrypt v2 staging: https://acme-staging-v02.api.letsencrypt.org/directory
56 // See the following for more CAs implementing ACME protocol:
57 // https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment#CAs_&_PKIs_that_offer_ACME_certificates
58 directory = flag.String("d", "", "ACME directory URL.")
59 reginfo = flag.String("r", "", "ACME account registration info.")
60 flow = flag.String("f", "", "Flow to run: order, preauthz (RFC8555) or preauthz02 (draft-02).")
61 chaltyp = flag.String("t", "", "Challenge type: tls-alpn-01, http-01 or dns-01.")
62 addr = flag.String("a", "", "Local server address for tls-alpn-01 and http-01.")
63 dnsscript = flag.String("s", "", "Script to run for provisioning dns-01 challenges.")
64 domain = flag.String("domain", "", "Space separate domain identifiers.")
65 ipaddr = flag.String("ip", "", "Space separate IP address identifiers.")
66)
67
68func main() {
69 flag.Usage = func() {
70 fmt.Fprintln(flag.CommandLine.Output(), `
71The prober program runs against an actual ACME CA implementation.
72It spins up an HTTP server to fulfill authorization challenges
73or execute a DNS script to provision a response to dns-01 challenge.
74
75For http-01 and tls-alpn-01 challenge types this requires the ACME CA
76to be able to reach the HTTP server.
77
78A usage example:
79
80 go run prober.go \
81 -d https://acme-staging-v02.api.letsencrypt.org/directory \
82 -f order \
83 -t http-01 \
84 -a :8080 \
85 -domain some.example.org
86
87The above assumes a TCP tunnel from some.example.org:80 to 0.0.0.0:8080
88in order for the test to be able to fulfill http-01 challenge.
89To test tls-alpn-01 challenge, 443 port would need to be tunneled
90to 0.0.0.0:8080.
91When running with dns-01 challenge type, use -s argument instead of -a.
92 `)
93 flag.PrintDefaults()
94 }
95 flag.Parse()
96
97 identifiers := acme.DomainIDs(strings.Fields(*domain)...)
98 identifiers = append(identifiers, acme.IPIDs(strings.Fields(*ipaddr)...)...)
99 if len(identifiers) == 0 {
100 log.Fatal("at least one domain or IP addr identifier is required")
101 }
102
103 // Duration of the whole run.
104 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
105 defer cancel()
106
107 // Create and register a new account.
108 akey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
109 if err != nil {
110 log.Fatal(err)
111 }
112 cl := &acme.Client{Key: akey, DirectoryURL: *directory}
113 a := &acme.Account{Contact: strings.Fields(*reginfo)}
114 if _, err := cl.Register(ctx, a, acme.AcceptTOS); err != nil {
115 log.Fatalf("Register: %v", err)
116 }
117
118 // Run the desired flow test.
119 p := &prober{
120 client: cl,
121 chalType: *chaltyp,
122 localAddr: *addr,
123 dnsScript: *dnsscript,
124 }
125 switch *flow {
126 case "order":
127 p.runOrder(ctx, identifiers)
128 case "preauthz":
129 p.runPreauthz(ctx, identifiers)
130 case "preauthz02":
131 p.runPreauthzLegacy(ctx, identifiers)
132 default:
133 log.Fatalf("unknown flow: %q", *flow)
134 }
135 if len(p.errors) > 0 {
136 os.Exit(1)
137 }
138}
139
140type prober struct {
141 client *acme.Client
142 chalType string
143 localAddr string
144 dnsScript string
145
146 errors []error
147}
148
149func (p *prober) errorf(format string, a ...interface{}) {
150 err := fmt.Errorf(format, a...)
151 log.Print(err)
152 p.errors = append(p.errors, err)
153}
154
155func (p *prober) runOrder(ctx context.Context, identifiers []acme.AuthzID) {
156 // Create a new order and pick a challenge.
157 // Note that Let's Encrypt will reply with 400 error:malformed
158 // "NotBefore and NotAfter are not supported" when providing a NotAfter
159 // value like WithOrderNotAfter(time.Now().Add(24 * time.Hour)).
160 o, err := p.client.AuthorizeOrder(ctx, identifiers)
161 if err != nil {
162 log.Fatalf("AuthorizeOrder: %v", err)
163 }
164
165 var zurls []string
166 for _, u := range o.AuthzURLs {
167 z, err := p.client.GetAuthorization(ctx, u)
168 if err != nil {
169 log.Fatalf("GetAuthorization(%q): %v", u, err)
170 }
171 log.Printf("%+v", z)
172 if z.Status != acme.StatusPending {
173 log.Printf("authz status is %q; skipping", z.Status)
174 continue
175 }
176 if err := p.fulfill(ctx, z); err != nil {
177 log.Fatalf("fulfill(%s): %v", z.URI, err)
178 }
179 zurls = append(zurls, z.URI)
180 log.Printf("authorized for %+v", z.Identifier)
181 }
182
183 log.Print("all challenges are done")
184 if _, err := p.client.WaitOrder(ctx, o.URI); err != nil {
185 log.Fatalf("WaitOrder(%q): %v", o.URI, err)
186 }
187 csr, certkey := newCSR(identifiers)
188 der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
189 if err != nil {
190 log.Fatalf("CreateOrderCert: %v", err)
191 }
192 log.Printf("cert URL: %s", curl)
193 if err := checkCert(der, identifiers); err != nil {
194 p.errorf("invalid cert: %v", err)
195 }
196
197 // Deactivate all authorizations we satisfied earlier.
198 for _, v := range zurls {
199 if err := p.client.RevokeAuthorization(ctx, v); err != nil {
200 p.errorf("RevokAuthorization(%q): %v", v, err)
201 continue
202 }
203 }
204 // Deactivate the account. We don't need it for any further calls.
205 if err := p.client.DeactivateReg(ctx); err != nil {
206 p.errorf("DeactivateReg: %v", err)
207 }
208 // Try revoking the issued cert using its private key.
209 if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
210 p.errorf("RevokeCert: %v", err)
211 }
212}
213
214func (p *prober) runPreauthz(ctx context.Context, identifiers []acme.AuthzID) {
215 dir, err := p.client.Discover(ctx)
Lars Lehtonen0a567562019-11-08 14:37:52 -0800216 if err != nil {
217 log.Fatalf("Discover: %v", err)
218 }
Alex Vaghinf9e20702019-09-11 10:47:38 -0400219 if dir.AuthzURL == "" {
220 log.Fatal("CA does not support pre-authorization")
221 }
222
223 var zurls []string
224 for _, id := range identifiers {
225 z, err := authorize(ctx, p.client, id)
226 if err != nil {
227 log.Fatalf("AuthorizeID(%+v): %v", z, err)
228 }
229 if z.Status == acme.StatusValid {
230 log.Printf("authz %s is valid; skipping", z.URI)
231 continue
232 }
233 if err := p.fulfill(ctx, z); err != nil {
234 log.Fatalf("fulfill(%s): %v", z.URI, err)
235 }
236 zurls = append(zurls, z.URI)
237 log.Printf("authorized for %+v", id)
238 }
239
240 // We should be all set now.
241 // Expect all authorizations to be satisfied.
242 log.Print("all challenges are done")
243 o, err := p.client.AuthorizeOrder(ctx, identifiers)
244 if err != nil {
245 log.Fatalf("AuthorizeOrder: %v", err)
246 }
247 waitCtx, cancel := context.WithTimeout(ctx, time.Minute)
248 defer cancel()
249 if _, err := p.client.WaitOrder(waitCtx, o.URI); err != nil {
250 log.Fatalf("WaitOrder(%q): %v", o.URI, err)
251 }
252 csr, certkey := newCSR(identifiers)
253 der, curl, err := p.client.CreateOrderCert(ctx, o.FinalizeURL, csr, true)
254 if err != nil {
255 log.Fatalf("CreateOrderCert: %v", err)
256 }
257 log.Printf("cert URL: %s", curl)
258 if err := checkCert(der, identifiers); err != nil {
259 p.errorf("invalid cert: %v", err)
260 }
261
262 // Deactivate all authorizations we satisfied earlier.
263 for _, v := range zurls {
264 if err := p.client.RevokeAuthorization(ctx, v); err != nil {
265 p.errorf("RevokeAuthorization(%q): %v", v, err)
266 continue
267 }
268 }
269 // Deactivate the account. We don't need it for any further calls.
270 if err := p.client.DeactivateReg(ctx); err != nil {
271 p.errorf("DeactivateReg: %v", err)
272 }
273 // Try revoking the issued cert using its private key.
274 if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
275 p.errorf("RevokeCert: %v", err)
276 }
277}
278
279func (p *prober) runPreauthzLegacy(ctx context.Context, identifiers []acme.AuthzID) {
280 var zurls []string
281 for _, id := range identifiers {
282 z, err := authorize(ctx, p.client, id)
283 if err != nil {
284 log.Fatalf("AuthorizeID(%+v): %v", id, err)
285 }
286 if z.Status == acme.StatusValid {
287 log.Printf("authz %s is valid; skipping", z.URI)
288 continue
289 }
290 if err := p.fulfill(ctx, z); err != nil {
291 log.Fatalf("fulfill(%s): %v", z.URI, err)
292 }
293 zurls = append(zurls, z.URI)
294 log.Printf("authorized for %+v", id)
295 }
296
297 // We should be all set now.
298 log.Print("all authorizations are done")
299 csr, certkey := newCSR(identifiers)
300 der, curl, err := p.client.CreateCert(ctx, csr, 48*time.Hour, true)
301 if err != nil {
302 log.Fatalf("CreateCert: %v", err)
303 }
304 log.Printf("cert URL: %s", curl)
305 if err := checkCert(der, identifiers); err != nil {
306 p.errorf("invalid cert: %v", err)
307 }
308
309 // Deactivate all authorizations we satisfied earlier.
310 for _, v := range zurls {
311 if err := p.client.RevokeAuthorization(ctx, v); err != nil {
312 p.errorf("RevokAuthorization(%q): %v", v, err)
313 continue
314 }
315 }
316 // Try revoking the issued cert using its private key.
317 if err := p.client.RevokeCert(ctx, certkey, der[0], acme.CRLReasonCessationOfOperation); err != nil {
318 p.errorf("RevokeCert: %v", err)
319 }
320
321}
322
323func (p *prober) fulfill(ctx context.Context, z *acme.Authorization) error {
324 var chal *acme.Challenge
325 for i, c := range z.Challenges {
326 log.Printf("challenge %d: %+v", i, c)
327 if c.Type == p.chalType {
328 log.Printf("picked %s for authz %s", c.URI, z.URI)
329 chal = c
330 }
331 }
332 if chal == nil {
333 return fmt.Errorf("challenge type %q wasn't offered for authz %s", p.chalType, z.URI)
334 }
335
336 switch chal.Type {
337 case "tls-alpn-01":
338 return p.runTLSALPN01(ctx, z, chal)
339 case "http-01":
340 return p.runHTTP01(ctx, z, chal)
341 case "dns-01":
342 return p.runDNS01(ctx, z, chal)
343 default:
344 return fmt.Errorf("unknown challenge type %q", chal.Type)
345 }
346}
347
348func (p *prober) runTLSALPN01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
349 tokenCert, err := p.client.TLSALPN01ChallengeCert(chal.Token, z.Identifier.Value)
350 if err != nil {
351 return fmt.Errorf("TLSALPN01ChallengeCert: %v", err)
352 }
353 s := &http.Server{
354 Addr: p.localAddr,
355 TLSConfig: &tls.Config{
356 NextProtos: []string{acme.ALPNProto},
357 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
358 log.Printf("hello: %+v", hello)
359 return &tokenCert, nil
360 },
361 },
362 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
363 log.Printf("%s %s", r.Method, r.URL)
364 w.WriteHeader(http.StatusNotFound)
365 }),
366 }
367 go s.ListenAndServeTLS("", "")
368 defer s.Close()
369
370 if _, err := p.client.Accept(ctx, chal); err != nil {
371 return fmt.Errorf("Accept(%q): %v", chal.URI, err)
372 }
373 _, zerr := p.client.WaitAuthorization(ctx, z.URI)
374 return zerr
375}
376
377func (p *prober) runHTTP01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
378 body, err := p.client.HTTP01ChallengeResponse(chal.Token)
379 if err != nil {
380 return fmt.Errorf("HTTP01ChallengeResponse: %v", err)
381 }
382 s := &http.Server{
383 Addr: p.localAddr,
384 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
385 log.Printf("%s %s", r.Method, r.URL)
386 if r.URL.Path != p.client.HTTP01ChallengePath(chal.Token) {
387 w.WriteHeader(http.StatusNotFound)
388 return
389 }
390 w.Write([]byte(body))
391 }),
392 }
393 go s.ListenAndServe()
394 defer s.Close()
395
396 if _, err := p.client.Accept(ctx, chal); err != nil {
397 return fmt.Errorf("Accept(%q): %v", chal.URI, err)
398 }
399 _, zerr := p.client.WaitAuthorization(ctx, z.URI)
400 return zerr
401}
402
403func (p *prober) runDNS01(ctx context.Context, z *acme.Authorization, chal *acme.Challenge) error {
404 token, err := p.client.DNS01ChallengeRecord(chal.Token)
405 if err != nil {
406 return fmt.Errorf("DNS01ChallengeRecord: %v", err)
407 }
408
409 name := fmt.Sprintf("_acme-challenge.%s", z.Identifier.Value)
410 cmd := exec.CommandContext(ctx, p.dnsScript, name, token)
411 cmd.Stdin = os.Stdin
412 cmd.Stdout = os.Stdout
413 cmd.Stderr = os.Stderr
414 if err := cmd.Run(); err != nil {
415 return fmt.Errorf("%s: %v", p.dnsScript, err)
416 }
417
418 if _, err := p.client.Accept(ctx, chal); err != nil {
419 return fmt.Errorf("Accept(%q): %v", chal.URI, err)
420 }
421 _, zerr := p.client.WaitAuthorization(ctx, z.URI)
422 return zerr
423}
424
425func authorize(ctx context.Context, client *acme.Client, id acme.AuthzID) (*acme.Authorization, error) {
426 if id.Type == "ip" {
427 return client.AuthorizeIP(ctx, id.Value)
428 }
429 return client.Authorize(ctx, id.Value)
430}
431
432func checkCert(derChain [][]byte, id []acme.AuthzID) error {
433 if len(derChain) == 0 {
434 return errors.New("cert chain is zero bytes")
435 }
436 for i, b := range derChain {
437 crt, err := x509.ParseCertificate(b)
438 if err != nil {
439 return fmt.Errorf("%d: ParseCertificate: %v", i, err)
440 }
441 log.Printf("%d: serial: 0x%s", i, crt.SerialNumber)
442 log.Printf("%d: subject: %s", i, crt.Subject)
443 log.Printf("%d: issuer: %s", i, crt.Issuer)
444 log.Printf("%d: expires in %.1f day(s)", i, time.Until(crt.NotAfter).Hours()/24)
445 if i > 0 { // not a leaf cert
446 continue
447 }
448 p := &pem.Block{Type: "CERTIFICATE", Bytes: b}
449 log.Printf("%d: leaf:\n%s", i, pem.EncodeToMemory(p))
450 for _, v := range id {
451 if err := crt.VerifyHostname(v.Value); err != nil {
452 return err
453 }
454 }
455 }
456 return nil
457}
458
459func newCSR(identifiers []acme.AuthzID) ([]byte, crypto.Signer) {
460 var csr x509.CertificateRequest
461 for _, id := range identifiers {
462 switch id.Type {
463 case "dns":
464 csr.DNSNames = append(csr.DNSNames, id.Value)
465 case "ip":
466 csr.IPAddresses = append(csr.IPAddresses, net.ParseIP(id.Value))
467 default:
468 panic(fmt.Sprintf("newCSR: unknown identifier type %q", id.Type))
469 }
470 }
471 k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
472 if err != nil {
473 panic(fmt.Sprintf("newCSR: ecdsa.GenerateKey for a cert: %v", err))
474 }
475 b, err := x509.CreateCertificateRequest(rand.Reader, &csr, k)
476 if err != nil {
477 panic(fmt.Sprintf("newCSR: x509.CreateCertificateRequest: %v", err))
478 }
479 return b, k
480}