File: challenge.go

package info (click to toggle)
golang-github-eggsampler-acme 3.6.1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 312 kB
  • sloc: makefile: 47
file content (102 lines) | stat: -rw-r--r-- 3,243 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package acme

import (
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"net/http"
	"time"
)

// EncodeDNS01KeyAuthorization encodes a key authorization and provides a value to be put in the TXT record for the _acme-challenge DNS entry.
func EncodeDNS01KeyAuthorization(keyAuth string) string {
	h := sha256.Sum256([]byte(keyAuth))
	return base64.RawURLEncoding.EncodeToString(h[:])
}

// Helper function to determine whether a challenge is "finished" by its status.
func checkUpdatedChallengeStatus(challenge Challenge) (bool, error) {
	switch challenge.Status {
	case "pending":
		// Challenge objects are created in the "pending" state.
		// TODO: https://github.com/letsencrypt/boulder/issues/3346
		// return true, errors.New("acme: unexpected 'pending' challenge state")
		return false, nil

	case "processing":
		// They transition to the "processing" state when the client responds to the
		//   challenge and the server begins attempting to validate that the client has completed the challenge.
		return false, nil

	case "valid":
		// If validation is successful, the challenge moves to the "valid" state
		return true, nil

	case "invalid":
		// if there is an error, the challenge moves to the "invalid" state.
		if challenge.Error.Type != "" {
			return true, challenge.Error
		}
		return true, errors.New("acme: challenge is invalid, no error provided")

	default:
		return true, fmt.Errorf("acme: unknown challenge status: %s", challenge.Status)
	}
}

// UpdateChallenge responds to a challenge to indicate to the server to complete the challenge.
func (c Client) UpdateChallenge(account Account, challenge Challenge) (Challenge, error) {
	resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, struct{}{}, &challenge, http.StatusOK)
	if err != nil {
		return challenge, err
	}

	if loc := resp.Header.Get("Location"); loc != "" {
		challenge.URL = loc
	}
	challenge.AuthorizationURL = fetchLink(resp, "up")

	if finished, err := checkUpdatedChallengeStatus(challenge); finished {
		return challenge, err
	}

	pollInterval, pollTimeout := c.getPollingDurations()
	end := time.Now().Add(pollTimeout)
	for {
		if time.Now().After(end) {
			return challenge, errors.New("acme: challenge update timeout")
		}
		time.Sleep(pollInterval)

		resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK)
		if err != nil {
			// i don't think it's worth exiting the loop on this error
			// it could just be connectivity issue that's resolved before the timeout duration
			continue
		}

		if loc := resp.Header.Get("Location"); loc != "" {
			challenge.URL = loc
		}
		challenge.AuthorizationURL = fetchLink(resp, "up")

		if finished, err := checkUpdatedChallengeStatus(challenge); finished {
			return challenge, err
		}
	}
}

// FetchChallenge fetches an existing challenge from the given url.
func (c Client) FetchChallenge(account Account, challengeURL string) (Challenge, error) {
	challenge := Challenge{}
	resp, err := c.post(challengeURL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK)
	if err != nil {
		return challenge, err
	}

	challenge.URL = resp.Header.Get("Location")
	challenge.AuthorizationURL = fetchLink(resp, "up")

	return challenge, nil
}