1
0
mirror of https://github.com/go-acme/lego.git synced 2024-11-28 17:41:10 +02:00

nearlyfreespeech: fix authentication (#1999)

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
Robert Obryk 2023-08-21 00:04:35 +02:00 committed by GitHub
parent 406dad30fe
commit a423bb7411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 104 additions and 20 deletions

View File

@ -28,6 +28,8 @@ type Client struct {
login string
apiKey string
signer *Signer
baseURL *url.URL
HTTPClient *http.Client
}
@ -38,6 +40,7 @@ func NewClient(login string, apiKey string) *Client {
return &Client{
login: login,
apiKey: apiKey,
signer: NewSigner(),
baseURL: baseURL,
HTTPClient: &http.Client{Timeout: 10 * time.Second},
}
@ -74,7 +77,7 @@ func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Val
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set(authenticationHeader, c.createSignature(endpoint.Path, payload))
req.Header.Set(authenticationHeader, c.signer.Sign(endpoint.Path, payload, c.login, c.apiKey))
resp, err := c.HTTPClient.Do(req)
if err != nil {
@ -90,25 +93,6 @@ func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Val
return nil
}
func (c Client) createSignature(uri string, body string) string {
// This is the only part of this that needs to be serialized.
salt := make([]byte, 16)
for i := 0; i < 16; i++ {
salt[i] = saltBytes[rand.Intn(len(saltBytes))]
}
// Header is "login;timestamp;salt;hash".
// hash is SHA1("login;timestamp;salt;api-key;request-uri;body-hash")
// and body-hash is SHA1(body).
bodyHash := sha1.Sum([]byte(body))
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", c.login, timestamp, salt, c.apiKey, uri, bodyHash)
return fmt.Sprintf("%s;%s;%s;%02x", c.login, timestamp, salt, sha1.Sum([]byte(hashInput)))
}
func parseError(req *http.Request, resp *http.Response) error {
raw, _ := io.ReadAll(resp.Body)
@ -120,3 +104,38 @@ func parseError(req *http.Request, resp *http.Response) error {
return errAPI
}
type Signer struct {
saltShaker func() []byte
clock func() time.Time
}
func NewSigner() *Signer {
return &Signer{saltShaker: getRandomSalt, clock: time.Now}
}
func (c Signer) Sign(uri string, body, login, apiKey string) string {
// Header is "login;timestamp;salt;hash".
// hash is SHA1("login;timestamp;salt;api-key;request-uri;body-hash")
// and body-hash is SHA1(body).
bodyHash := sha1.Sum([]byte(body))
timestamp := strconv.FormatInt(c.clock().Unix(), 10)
// Workaround for https://golang.org/issue/58605
uri = "/" + strings.TrimLeft(uri, "/")
hashInput := fmt.Sprintf("%s;%s;%s;%s;%s;%02x", login, timestamp, c.saltShaker(), apiKey, uri, bodyHash)
return fmt.Sprintf("%s;%s;%s;%02x", login, timestamp, c.saltShaker(), sha1.Sum([]byte(hashInput)))
}
func getRandomSalt() []byte {
// This is the only part of this that needs to be serialized.
salt := make([]byte, 16)
for i := 0; i < 16; i++ {
salt[i] = saltBytes[rand.Intn(len(saltBytes))]
}
return salt
}

View File

@ -9,7 +9,9 @@ import (
"net/url"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -24,6 +26,9 @@ func setupTest(t *testing.T) (*Client, *http.ServeMux) {
client.HTTPClient = server.Client()
client.baseURL, _ = url.Parse(server.URL)
client.signer.saltShaker = func() []byte { return []byte("0123456789ABCDEF") }
client.signer.clock = func() time.Time { return time.Unix(1692475113, 0) }
return client, mux
}
@ -147,3 +152,63 @@ func TestClient_RemoveRecord_error(t *testing.T) {
err := client.RemoveRecord(context.Background(), "example.com", record)
require.Error(t, err)
}
func TestSigner_Sign(t *testing.T) {
testCases := []struct {
desc string
path string
now int64
salt string
expected string
}{
{
desc: "basic",
path: "/path",
now: 1692475113,
salt: "0123456789ABCDEF",
expected: "user;1692475113;0123456789ABCDEF;417a9988c7ad7919b297884dd120b5808d8a1e6f",
},
{
desc: "another date",
path: "/path",
now: 1692567766,
salt: "0123456789ABCDEF",
expected: "user;1692567766;0123456789ABCDEF;b5c28286fd2e1a45a7c576dc2a6430116f721502",
},
{
desc: "another salt",
path: "/path",
now: 1692475113,
salt: "FEDCBA9876543210",
expected: "user;1692475113;FEDCBA9876543210;0f766822bda4fdc09829be4e1ea5e27ae3ae334e",
},
{
desc: "empty path",
path: "",
now: 1692475113,
salt: "0123456789ABCDEF",
expected: "user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1",
},
{
desc: "root path",
path: "/",
now: 1692475113,
salt: "0123456789ABCDEF",
expected: "user;1692475113;0123456789ABCDEF;c7c241a4d15d04d92805631d58d4d72ac1c339a1",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
signer := NewSigner()
signer.saltShaker = func() []byte { return []byte(test.salt) }
signer.clock = func() time.Time { return time.Unix(test.now, 0) }
sign := signer.Sign(test.path, "data", "user", "secret")
assert.Equal(t, test.expected, sign)
})
}
}