Skip to content

Commit a423bb7

Browse files
robrykldez
andauthored
nearlyfreespeech: fix authentication (go-acme#1999)
Co-authored-by: Fernandez Ludovic <[email protected]>
1 parent 406dad3 commit a423bb7

File tree

2 files changed

+101
-17
lines changed

2 files changed

+101
-17
lines changed

providers/dns/nearlyfreespeech/internal/client.go

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type Client struct {
2828
login string
2929
apiKey string
3030

31+
signer *Signer
32+
3133
baseURL *url.URL
3234
HTTPClient *http.Client
3335
}
@@ -38,6 +40,7 @@ func NewClient(login string, apiKey string) *Client {
3840
return &Client{
3941
login: login,
4042
apiKey: apiKey,
43+
signer: NewSigner(),
4144
baseURL: baseURL,
4245
HTTPClient: &http.Client{Timeout: 10 * time.Second},
4346
}
@@ -74,7 +77,7 @@ func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Val
7477
}
7578

7679
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
77-
req.Header.Set(authenticationHeader, c.createSignature(endpoint.Path, payload))
80+
req.Header.Set(authenticationHeader, c.signer.Sign(endpoint.Path, payload, c.login, c.apiKey))
7881

7982
resp, err := c.HTTPClient.Do(req)
8083
if err != nil {
@@ -90,33 +93,49 @@ func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Val
9093
return nil
9194
}
9295

93-
func (c Client) createSignature(uri string, body string) string {
94-
// This is the only part of this that needs to be serialized.
95-
salt := make([]byte, 16)
96-
for i := 0; i < 16; i++ {
97-
salt[i] = saltBytes[rand.Intn(len(saltBytes))]
96+
func parseError(req *http.Request, resp *http.Response) error {
97+
raw, _ := io.ReadAll(resp.Body)
98+
99+
errAPI := &APIError{}
100+
err := json.Unmarshal(raw, errAPI)
101+
if err != nil {
102+
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
98103
}
99104

105+
return errAPI
106+
}
107+
108+
type Signer struct {
109+
saltShaker func() []byte
110+
clock func() time.Time
111+
}
112+
113+
func NewSigner() *Signer {
114+
return &Signer{saltShaker: getRandomSalt, clock: time.Now}
115+
}
116+
117+
func (c Signer) Sign(uri string, body, login, apiKey string) string {
100118
// Header is "login;timestamp;salt;hash".
101119
// hash is SHA1("login;timestamp;salt;api-key;request-uri;body-hash")
102120
// and body-hash is SHA1(body).
103121

104122
bodyHash := sha1.Sum([]byte(body))
105-
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
123+
timestamp := strconv.FormatInt(c.clock().Unix(), 10)
106124

107-
hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", c.login, timestamp, salt, c.apiKey, uri, bodyHash)
125+
// Workaround for https://golang.org/issue/58605
126+
uri = "/" + strings.TrimLeft(uri, "/")
108127

109-
return fmt.Sprintf("%s;%s;%s;%02x", c.login, timestamp, salt, sha1.Sum([]byte(hashInput)))
110-
}
128+
hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", login, timestamp, c.saltShaker(), apiKey, uri, bodyHash)
111129

112-
func parseError(req *http.Request, resp *http.Response) error {
113-
raw, _ := io.ReadAll(resp.Body)
130+
return fmt.Sprintf("%s;%s;%s;%02x", login, timestamp, c.saltShaker(), sha1.Sum([]byte(hashInput)))
131+
}
114132

115-
errAPI := &APIError{}
116-
err := json.Unmarshal(raw, errAPI)
117-
if err != nil {
118-
return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
133+
func getRandomSalt() []byte {
134+
// This is the only part of this that needs to be serialized.
135+
salt := make([]byte, 16)
136+
for i := 0; i < 16; i++ {
137+
salt[i] = saltBytes[rand.Intn(len(saltBytes))]
119138
}
120139

121-
return errAPI
140+
return salt
122141
}

providers/dns/nearlyfreespeech/internal/client_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"net/url"
1010
"os"
1111
"testing"
12+
"time"
1213

14+
"github.com/stretchr/testify/assert"
1315
"github.com/stretchr/testify/require"
1416
)
1517

@@ -24,6 +26,9 @@ func setupTest(t *testing.T) (*Client, *http.ServeMux) {
2426
client.HTTPClient = server.Client()
2527
client.baseURL, _ = url.Parse(server.URL)
2628

29+
client.signer.saltShaker = func() []byte { return []byte("0123456789ABCDEF") }
30+
client.signer.clock = func() time.Time { return time.Unix(1692475113, 0) }
31+
2732
return client, mux
2833
}
2934

@@ -147,3 +152,63 @@ func TestClient_RemoveRecord_error(t *testing.T) {
147152
err := client.RemoveRecord(context.Background(), "example.com", record)
148153
require.Error(t, err)
149154
}
155+
156+
func TestSigner_Sign(t *testing.T) {
157+
testCases := []struct {
158+
desc string
159+
path string
160+
now int64
161+
salt string
162+
expected string
163+
}{
164+
{
165+
desc: "basic",
166+
path: "/path",
167+
now: 1692475113,
168+
salt: "0123456789ABCDEF",
169+
expected: "user;1692475113;0123456789ABCDEF;417a9988c7ad7919b297884dd120b5808d8a1e6f",
170+
},
171+
{
172+
desc: "another date",
173+
path: "/path",
174+
now: 1692567766,
175+
salt: "0123456789ABCDEF",
176+
expected: "user;1692567766;0123456789ABCDEF;b5c28286fd2e1a45a7c576dc2a6430116f721502",
177+
},
178+
{
179+
desc: "another salt",
180+
path: "/path",
181+
now: 1692475113,
182+
salt: "FEDCBA9876543210",
183+
expected: "user;1692475113;FEDCBA9876543210;0f766822bda4fdc09829be4e1ea5e27ae3ae334e",
184+
},
185+
{
186+
desc: "empty path",
187+
path: "",
188+
now: 1692475113,
189+
salt: "0123456789ABCDEF",
190+
expected: "user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1",
191+
},
192+
{
193+
desc: "root path",
194+
path: "/",
195+
now: 1692475113,
196+
salt: "0123456789ABCDEF",
197+
expected: "user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1",
198+
},
199+
}
200+
201+
for _, test := range testCases {
202+
test := test
203+
t.Run(test.desc, func(t *testing.T) {
204+
t.Parallel()
205+
signer := NewSigner()
206+
signer.saltShaker = func() []byte { return []byte(test.salt) }
207+
signer.clock = func() time.Time { return time.Unix(test.now, 0) }
208+
209+
sign := signer.Sign(test.path, "data", "user", "secret")
210+
211+
assert.Equal(t, test.expected, sign)
212+
})
213+
}
214+
}

0 commit comments

Comments
 (0)