1
2
3
4
5
6
7
8 package acmetest
9
10 import (
11 "context"
12 "crypto"
13 "crypto/ecdsa"
14 "crypto/elliptic"
15 "crypto/rand"
16 "crypto/rsa"
17 "crypto/tls"
18 "crypto/x509"
19 "crypto/x509/pkix"
20 "encoding/asn1"
21 "encoding/base64"
22 "encoding/json"
23 "encoding/pem"
24 "fmt"
25 "io"
26 "math/big"
27 "net"
28 "net/http"
29 "net/http/httptest"
30 "path"
31 "strconv"
32 "strings"
33 "sync"
34 "testing"
35 "time"
36
37 "golang.org/x/crypto/acme"
38 )
39
40
41 type CAServer struct {
42 rootKey crypto.Signer
43 rootCert []byte
44 rootTemplate *x509.Certificate
45
46 t *testing.T
47 server *httptest.Server
48 issuer pkix.Name
49 challengeTypes []string
50 url string
51 roots *x509.CertPool
52 eabRequired bool
53
54 mu sync.Mutex
55 certCount int
56 acctRegistered bool
57 domainAddr map[string]string
58 domainGetCert map[string]getCertificateFunc
59 domainHandler map[string]http.Handler
60 validAuthz map[string]*authorization
61 authorizations []*authorization
62 orders []*order
63 errors []error
64 }
65
66 type getCertificateFunc func(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
67
68
69
70 func NewCAServer(t *testing.T) *CAServer {
71 ca := &CAServer{t: t,
72 challengeTypes: []string{"fake-01", "tls-alpn-01", "http-01"},
73 domainAddr: make(map[string]string),
74 domainGetCert: make(map[string]getCertificateFunc),
75 domainHandler: make(map[string]http.Handler),
76 validAuthz: make(map[string]*authorization),
77 }
78
79 ca.server = httptest.NewUnstartedServer(http.HandlerFunc(ca.handle))
80
81 r, err := rand.Int(rand.Reader, big.NewInt(1000000))
82 if err != nil {
83 panic(fmt.Sprintf("rand.Int: %v", err))
84 }
85 ca.issuer = pkix.Name{
86 Organization: []string{"Test Acme Co"},
87 CommonName: "Root CA " + r.String(),
88 }
89
90 return ca
91 }
92
93 func (ca *CAServer) generateRoot() {
94 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
95 if err != nil {
96 panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err))
97 }
98 tmpl := &x509.Certificate{
99 SerialNumber: big.NewInt(1),
100 Subject: ca.issuer,
101 NotBefore: time.Now(),
102 NotAfter: time.Now().Add(365 * 24 * time.Hour),
103 KeyUsage: x509.KeyUsageCertSign,
104 BasicConstraintsValid: true,
105 IsCA: true,
106 }
107 der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
108 if err != nil {
109 panic(fmt.Sprintf("x509.CreateCertificate: %v", err))
110 }
111 cert, err := x509.ParseCertificate(der)
112 if err != nil {
113 panic(fmt.Sprintf("x509.ParseCertificate: %v", err))
114 }
115 ca.roots = x509.NewCertPool()
116 ca.roots.AddCert(cert)
117 ca.rootKey = key
118 ca.rootCert = der
119 ca.rootTemplate = tmpl
120 }
121
122
123 func (ca *CAServer) IssuerName(name pkix.Name) *CAServer {
124 if ca.url != "" {
125 panic("IssuerName must be called before Start")
126 }
127 ca.issuer = name
128 return ca
129 }
130
131
132 func (ca *CAServer) ChallengeTypes(types ...string) *CAServer {
133 if ca.url != "" {
134 panic("ChallengeTypes must be called before Start")
135 }
136 ca.challengeTypes = types
137 return ca
138 }
139
140
141 func (ca *CAServer) URL() string {
142 if ca.url == "" {
143 panic("URL called before Start")
144 }
145 return ca.url
146 }
147
148
149 func (ca *CAServer) Roots() *x509.CertPool {
150 if ca.url == "" {
151 panic("Roots called before Start")
152 }
153 return ca.roots
154 }
155
156
157 func (ca *CAServer) ExternalAccountRequired() *CAServer {
158 if ca.url != "" {
159 panic("ExternalAccountRequired must be called before Start")
160 }
161 ca.eabRequired = true
162 return ca
163 }
164
165
166
167 func (ca *CAServer) Start() *CAServer {
168 if ca.url == "" {
169 ca.generateRoot()
170 ca.server.Start()
171 ca.t.Cleanup(ca.server.Close)
172 ca.url = ca.server.URL
173 }
174 return ca
175 }
176
177 func (ca *CAServer) serverURL(format string, arg ...interface{}) string {
178 return ca.server.URL + fmt.Sprintf(format, arg...)
179 }
180
181 func (ca *CAServer) addr(domain string) (string, bool) {
182 ca.mu.Lock()
183 defer ca.mu.Unlock()
184 addr, ok := ca.domainAddr[domain]
185 return addr, ok
186 }
187
188 func (ca *CAServer) getCert(domain string) (getCertificateFunc, bool) {
189 ca.mu.Lock()
190 defer ca.mu.Unlock()
191 f, ok := ca.domainGetCert[domain]
192 return f, ok
193 }
194
195 func (ca *CAServer) getHandler(domain string) (http.Handler, bool) {
196 ca.mu.Lock()
197 defer ca.mu.Unlock()
198 h, ok := ca.domainHandler[domain]
199 return h, ok
200 }
201
202 func (ca *CAServer) httpErrorf(w http.ResponseWriter, code int, format string, a ...interface{}) {
203 s := fmt.Sprintf(format, a...)
204 ca.t.Errorf(format, a...)
205 http.Error(w, s, code)
206 }
207
208
209
210 func (ca *CAServer) Resolve(domain, addr string) {
211 ca.mu.Lock()
212 defer ca.mu.Unlock()
213 ca.domainAddr[domain] = addr
214 }
215
216
217
218 func (ca *CAServer) ResolveGetCertificate(domain string, f getCertificateFunc) {
219 ca.mu.Lock()
220 defer ca.mu.Unlock()
221 ca.domainGetCert[domain] = f
222 }
223
224
225
226 func (ca *CAServer) ResolveHandler(domain string, h http.Handler) {
227 ca.mu.Lock()
228 defer ca.mu.Unlock()
229 ca.domainHandler[domain] = h
230 }
231
232 type discovery struct {
233 NewNonce string `json:"newNonce"`
234 NewAccount string `json:"newAccount"`
235 NewOrder string `json:"newOrder"`
236 NewAuthz string `json:"newAuthz"`
237
238 Meta discoveryMeta `json:"meta,omitempty"`
239 }
240
241 type discoveryMeta struct {
242 ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
243 }
244
245 type challenge struct {
246 URI string `json:"uri"`
247 Type string `json:"type"`
248 Token string `json:"token"`
249 }
250
251 type authorization struct {
252 Status string `json:"status"`
253 Challenges []challenge `json:"challenges"`
254
255 domain string
256 id int
257 }
258
259 type order struct {
260 Status string `json:"status"`
261 AuthzURLs []string `json:"authorizations"`
262 FinalizeURL string `json:"finalize"`
263 CertURL string `json:"certificate"`
264
265 leaf []byte
266 }
267
268 func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) {
269 ca.t.Logf("%s %s", r.Method, r.URL)
270 w.Header().Set("Replay-Nonce", "nonce")
271
272
273 switch {
274 default:
275 ca.httpErrorf(w, http.StatusBadRequest, "unrecognized r.URL.Path: %s", r.URL.Path)
276
277
278 case r.URL.Path == "/":
279 resp := &discovery{
280 NewNonce: ca.serverURL("/new-nonce"),
281 NewAccount: ca.serverURL("/new-account"),
282 NewOrder: ca.serverURL("/new-order"),
283 Meta: discoveryMeta{
284 ExternalAccountRequired: ca.eabRequired,
285 },
286 }
287 if err := json.NewEncoder(w).Encode(resp); err != nil {
288 panic(fmt.Sprintf("discovery response: %v", err))
289 }
290
291
292 case r.URL.Path == "/new-nonce":
293
294 return
295
296
297 case r.URL.Path == "/new-account":
298 ca.mu.Lock()
299 defer ca.mu.Unlock()
300 if ca.acctRegistered {
301 ca.httpErrorf(w, http.StatusServiceUnavailable, "multiple accounts are not implemented")
302 return
303 }
304 ca.acctRegistered = true
305
306 var req struct {
307 ExternalAccountBinding json.RawMessage
308 }
309
310 if err := decodePayload(&req, r.Body); err != nil {
311 ca.httpErrorf(w, http.StatusBadRequest, err.Error())
312 return
313 }
314
315 if ca.eabRequired && len(req.ExternalAccountBinding) == 0 {
316 ca.httpErrorf(w, http.StatusBadRequest, "registration failed: no JWS for EAB")
317 return
318 }
319
320
321 w.Header().Set("Location", ca.serverURL("/accounts/1"))
322 w.WriteHeader(http.StatusCreated)
323 w.Write([]byte("{}"))
324
325
326 case r.URL.Path == "/new-order":
327 var req struct {
328 Identifiers []struct{ Value string }
329 }
330 if err := decodePayload(&req, r.Body); err != nil {
331 ca.httpErrorf(w, http.StatusBadRequest, err.Error())
332 return
333 }
334 ca.mu.Lock()
335 defer ca.mu.Unlock()
336 o := &order{Status: acme.StatusPending}
337 for _, id := range req.Identifiers {
338 z := ca.authz(id.Value)
339 o.AuthzURLs = append(o.AuthzURLs, ca.serverURL("/authz/%d", z.id))
340 }
341 orderID := len(ca.orders)
342 ca.orders = append(ca.orders, o)
343 w.Header().Set("Location", ca.serverURL("/orders/%d", orderID))
344 w.WriteHeader(http.StatusCreated)
345 if err := json.NewEncoder(w).Encode(o); err != nil {
346 panic(err)
347 }
348
349
350 case strings.HasPrefix(r.URL.Path, "/orders/"):
351 ca.mu.Lock()
352 defer ca.mu.Unlock()
353 o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/orders/"))
354 if err != nil {
355 ca.httpErrorf(w, http.StatusBadRequest, err.Error())
356 return
357 }
358 if err := json.NewEncoder(w).Encode(o); err != nil {
359 panic(err)
360 }
361
362
363 case strings.HasPrefix(r.URL.Path, "/challenge/"):
364 parts := strings.Split(r.URL.Path, "/")
365 typ, id := parts[len(parts)-2], parts[len(parts)-1]
366 ca.mu.Lock()
367 supported := false
368 for _, suppTyp := range ca.challengeTypes {
369 if suppTyp == typ {
370 supported = true
371 }
372 }
373 a, err := ca.storedAuthz(id)
374 ca.mu.Unlock()
375 if !supported {
376 ca.httpErrorf(w, http.StatusBadRequest, "unsupported challenge: %v", typ)
377 return
378 }
379 if err != nil {
380 ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: %v", err)
381 return
382 }
383 ca.validateChallenge(a, typ)
384 w.Write([]byte("{}"))
385
386
387 case strings.HasPrefix(r.URL.Path, "/authz/"):
388 var req struct{ Status string }
389 decodePayload(&req, r.Body)
390 deactivate := req.Status == "deactivated"
391 ca.mu.Lock()
392 defer ca.mu.Unlock()
393 authz, err := ca.storedAuthz(strings.TrimPrefix(r.URL.Path, "/authz/"))
394 if err != nil {
395 ca.httpErrorf(w, http.StatusNotFound, "%v", err)
396 return
397 }
398 if deactivate {
399
400 authz.Status = "deactivated"
401 ca.t.Logf("authz %d is now %s", authz.id, authz.Status)
402 ca.updatePendingOrders()
403 }
404 if err := json.NewEncoder(w).Encode(authz); err != nil {
405 panic(fmt.Sprintf("encoding authz %d: %v", authz.id, err))
406 }
407
408
409 case strings.HasPrefix(r.URL.Path, "/new-cert/"):
410 ca.mu.Lock()
411 defer ca.mu.Unlock()
412 orderID := strings.TrimPrefix(r.URL.Path, "/new-cert/")
413 o, err := ca.storedOrder(orderID)
414 if err != nil {
415 ca.httpErrorf(w, http.StatusBadRequest, err.Error())
416 return
417 }
418 if o.Status != acme.StatusReady {
419 ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status)
420 return
421 }
422
423 var req struct {
424 CSR string `json:"csr"`
425 }
426 decodePayload(&req, r.Body)
427 b, _ := base64.RawURLEncoding.DecodeString(req.CSR)
428 csr, err := x509.ParseCertificateRequest(b)
429 if err != nil {
430 ca.httpErrorf(w, http.StatusBadRequest, err.Error())
431 return
432 }
433
434 der, err := ca.leafCert(csr)
435 if err != nil {
436 ca.httpErrorf(w, http.StatusBadRequest, "new-cert response: ca.leafCert: %v", err)
437 return
438 }
439 o.leaf = der
440 o.CertURL = ca.serverURL("/issued-cert/%s", orderID)
441 o.Status = acme.StatusValid
442 if err := json.NewEncoder(w).Encode(o); err != nil {
443 panic(err)
444 }
445
446
447 case strings.HasPrefix(r.URL.Path, "/issued-cert/"):
448 ca.mu.Lock()
449 defer ca.mu.Unlock()
450 o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/issued-cert/"))
451 if err != nil {
452 ca.httpErrorf(w, http.StatusBadRequest, err.Error())
453 return
454 }
455 if o.Status != acme.StatusValid {
456 ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status)
457 return
458 }
459 w.Header().Set("Content-Type", "application/pem-certificate-chain")
460 pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: o.leaf})
461 pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: ca.rootCert})
462 }
463 }
464
465
466
467 func (ca *CAServer) storedOrder(i string) (*order, error) {
468 idx, err := strconv.Atoi(i)
469 if err != nil {
470 return nil, fmt.Errorf("storedOrder: %v", err)
471 }
472 if idx < 0 {
473 return nil, fmt.Errorf("storedOrder: invalid order index %d", idx)
474 }
475 if idx > len(ca.orders)-1 {
476 return nil, fmt.Errorf("storedOrder: no such order %d", idx)
477 }
478
479 ca.updatePendingOrders()
480 return ca.orders[idx], nil
481 }
482
483
484
485 func (ca *CAServer) storedAuthz(i string) (*authorization, error) {
486 idx, err := strconv.Atoi(i)
487 if err != nil {
488 return nil, fmt.Errorf("storedAuthz: %v", err)
489 }
490 if idx < 0 {
491 return nil, fmt.Errorf("storedAuthz: invalid authz index %d", idx)
492 }
493 if idx > len(ca.authorizations)-1 {
494 return nil, fmt.Errorf("storedAuthz: no such authz %d", idx)
495 }
496 return ca.authorizations[idx], nil
497 }
498
499
500
501 func (ca *CAServer) authz(identifier string) *authorization {
502 authz, ok := ca.validAuthz[identifier]
503 if !ok {
504 authzId := len(ca.authorizations)
505 authz = &authorization{
506 id: authzId,
507 domain: identifier,
508 Status: acme.StatusPending,
509 }
510 for _, typ := range ca.challengeTypes {
511 authz.Challenges = append(authz.Challenges, challenge{
512 Type: typ,
513 URI: ca.serverURL("/challenge/%s/%d", typ, authzId),
514 Token: challengeToken(authz.domain, typ, authzId),
515 })
516 }
517 ca.authorizations = append(ca.authorizations, authz)
518 }
519 return authz
520 }
521
522
523
524 func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) {
525 ca.certCount++
526 leaf := &x509.Certificate{
527 SerialNumber: big.NewInt(int64(ca.certCount)),
528 Subject: pkix.Name{Organization: []string{"Test Acme Co"}},
529 NotBefore: time.Now(),
530 NotAfter: time.Now().Add(90 * 24 * time.Hour),
531 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
532 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
533 DNSNames: csr.DNSNames,
534 BasicConstraintsValid: true,
535 }
536 if len(csr.DNSNames) == 0 {
537 leaf.DNSNames = []string{csr.Subject.CommonName}
538 }
539 return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey)
540 }
541
542
543 func (ca *CAServer) LeafCert(name, keyType string, notBefore, notAfter time.Time) *tls.Certificate {
544 if ca.url == "" {
545 panic("LeafCert called before Start")
546 }
547
548 ca.mu.Lock()
549 defer ca.mu.Unlock()
550 var pk crypto.Signer
551 switch keyType {
552 case "RSA":
553 var err error
554 pk, err = rsa.GenerateKey(rand.Reader, 1024)
555 if err != nil {
556 ca.t.Fatal(err)
557 }
558 case "ECDSA":
559 var err error
560 pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
561 if err != nil {
562 ca.t.Fatal(err)
563 }
564 default:
565 panic("LeafCert: unknown key type")
566 }
567 ca.certCount++
568 leaf := &x509.Certificate{
569 SerialNumber: big.NewInt(int64(ca.certCount)),
570 Subject: pkix.Name{Organization: []string{"Test Acme Co"}},
571 NotBefore: notBefore,
572 NotAfter: notAfter,
573 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
574 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
575 DNSNames: []string{name},
576 BasicConstraintsValid: true,
577 }
578 der, err := x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, pk.Public(), ca.rootKey)
579 if err != nil {
580 ca.t.Fatal(err)
581 }
582 return &tls.Certificate{
583 Certificate: [][]byte{der},
584 PrivateKey: pk,
585 }
586 }
587
588 func (ca *CAServer) validateChallenge(authz *authorization, typ string) {
589 var err error
590 switch typ {
591 case "tls-alpn-01":
592 err = ca.verifyALPNChallenge(authz)
593 case "http-01":
594 err = ca.verifyHTTPChallenge(authz)
595 default:
596 panic(fmt.Sprintf("validation of %q is not implemented", typ))
597 }
598 ca.mu.Lock()
599 defer ca.mu.Unlock()
600 if err != nil {
601 authz.Status = "invalid"
602 } else {
603 authz.Status = "valid"
604 ca.validAuthz[authz.domain] = authz
605 }
606 ca.t.Logf("validated %q for %q, err: %v", typ, authz.domain, err)
607 ca.t.Logf("authz %d is now %s", authz.id, authz.Status)
608
609 ca.updatePendingOrders()
610 }
611
612 func (ca *CAServer) updatePendingOrders() {
613
614
615
616
617 for i, o := range ca.orders {
618 if o.Status != acme.StatusPending {
619 continue
620 }
621
622 countValid, countInvalid := ca.validateAuthzURLs(o.AuthzURLs, i)
623 if countInvalid > 0 {
624 o.Status = acme.StatusInvalid
625 ca.t.Logf("order %d is now invalid", i)
626 continue
627 }
628 if countValid == len(o.AuthzURLs) {
629 o.Status = acme.StatusReady
630 o.FinalizeURL = ca.serverURL("/new-cert/%d", i)
631 ca.t.Logf("order %d is now ready", i)
632 }
633 }
634 }
635
636 func (ca *CAServer) validateAuthzURLs(urls []string, orderNum int) (countValid, countInvalid int) {
637 for _, zurl := range urls {
638 z, err := ca.storedAuthz(path.Base(zurl))
639 if err != nil {
640 ca.t.Logf("no authz %q for order %d", zurl, orderNum)
641 continue
642 }
643 if z.Status == acme.StatusInvalid {
644 countInvalid++
645 }
646 if z.Status == acme.StatusValid {
647 countValid++
648 }
649 }
650 return countValid, countInvalid
651 }
652
653 func (ca *CAServer) verifyALPNChallenge(a *authorization) error {
654 const acmeALPNProto = "acme-tls/1"
655
656 addr, haveAddr := ca.addr(a.domain)
657 getCert, haveGetCert := ca.getCert(a.domain)
658 if !haveAddr && !haveGetCert {
659 return fmt.Errorf("no resolution information for %q", a.domain)
660 }
661 if haveAddr && haveGetCert {
662 return fmt.Errorf("overlapping resolution information for %q", a.domain)
663 }
664
665 var crt *x509.Certificate
666 switch {
667 case haveAddr:
668 conn, err := tls.Dial("tcp", addr, &tls.Config{
669 ServerName: a.domain,
670 InsecureSkipVerify: true,
671 NextProtos: []string{acmeALPNProto},
672 MinVersion: tls.VersionTLS12,
673 })
674 if err != nil {
675 return err
676 }
677 if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto {
678 return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto)
679 }
680 if n := len(conn.ConnectionState().PeerCertificates); n != 1 {
681 return fmt.Errorf("len(PeerCertificates) = %d; want 1", n)
682 }
683 crt = conn.ConnectionState().PeerCertificates[0]
684 case haveGetCert:
685 hello := &tls.ClientHelloInfo{
686 ServerName: a.domain,
687
688 CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305},
689 SupportedProtos: []string{acme.ALPNProto},
690 SupportedVersions: []uint16{tls.VersionTLS12},
691 }
692 c, err := getCert(hello)
693 if err != nil {
694 return err
695 }
696 crt, err = x509.ParseCertificate(c.Certificate[0])
697 if err != nil {
698 return err
699 }
700 }
701
702 if err := crt.VerifyHostname(a.domain); err != nil {
703 return fmt.Errorf("verifyALPNChallenge: VerifyHostname: %v", err)
704 }
705
706 oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
707 for _, x := range crt.Extensions {
708 if x.Id.Equal(oid) {
709
710 return nil
711 }
712 }
713 return fmt.Errorf("verifyTokenCert: no id-pe-acmeIdentifier extension found")
714 }
715
716 func (ca *CAServer) verifyHTTPChallenge(a *authorization) error {
717 addr, haveAddr := ca.addr(a.domain)
718 handler, haveHandler := ca.getHandler(a.domain)
719 if !haveAddr && !haveHandler {
720 return fmt.Errorf("no resolution information for %q", a.domain)
721 }
722 if haveAddr && haveHandler {
723 return fmt.Errorf("overlapping resolution information for %q", a.domain)
724 }
725
726 token := challengeToken(a.domain, "http-01", a.id)
727 path := "/.well-known/acme-challenge/" + token
728
729 var body string
730 switch {
731 case haveAddr:
732 t := &http.Transport{
733 DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
734 return (&net.Dialer{}).DialContext(ctx, network, addr)
735 },
736 }
737 req, err := http.NewRequest("GET", "http://"+a.domain+path, nil)
738 if err != nil {
739 return err
740 }
741 res, err := t.RoundTrip(req)
742 if err != nil {
743 return err
744 }
745 if res.StatusCode != http.StatusOK {
746 return fmt.Errorf("http token: w.Code = %d; want %d", res.StatusCode, http.StatusOK)
747 }
748 b, err := io.ReadAll(res.Body)
749 if err != nil {
750 return err
751 }
752 body = string(b)
753 case haveHandler:
754 r := httptest.NewRequest("GET", path, nil)
755 r.Host = a.domain
756 w := httptest.NewRecorder()
757 handler.ServeHTTP(w, r)
758 if w.Code != http.StatusOK {
759 return fmt.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK)
760 }
761 body = w.Body.String()
762 }
763
764 if !strings.HasPrefix(body, token) {
765 return fmt.Errorf("http token value = %q; want 'token-http-01.' prefix", body)
766 }
767 return nil
768 }
769
770 func decodePayload(v interface{}, r io.Reader) error {
771 var req struct{ Payload string }
772 if err := json.NewDecoder(r).Decode(&req); err != nil {
773 return err
774 }
775 payload, err := base64.RawURLEncoding.DecodeString(req.Payload)
776 if err != nil {
777 return err
778 }
779 return json.Unmarshal(payload, v)
780 }
781
782 func challengeToken(domain, challType string, authzID int) string {
783 return fmt.Sprintf("token-%s-%s-%d", domain, challType, authzID)
784 }
785
786 func unique(a []string) []string {
787 seen := make(map[string]bool)
788 var res []string
789 for _, s := range a {
790 if s != "" && !seen[s] {
791 seen[s] = true
792 res = append(res, s)
793 }
794 }
795 return res
796 }
797
View as plain text