forked from TrueCloudLab/lego
nearlyfreespeech: fix authentication (#1999)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
parent
406dad30fe
commit
a423bb7411
2 changed files with 104 additions and 20 deletions
|
@ -28,6 +28,8 @@ type Client struct {
|
||||||
login string
|
login string
|
||||||
apiKey string
|
apiKey string
|
||||||
|
|
||||||
|
signer *Signer
|
||||||
|
|
||||||
baseURL *url.URL
|
baseURL *url.URL
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
@ -38,6 +40,7 @@ func NewClient(login string, apiKey string) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
login: login,
|
login: login,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
signer: NewSigner(),
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
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("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)
|
resp, err := c.HTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -90,25 +93,6 @@ func (c Client) doRequest(ctx context.Context, endpoint *url.URL, params url.Val
|
||||||
return nil
|
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 {
|
func parseError(req *http.Request, resp *http.Response) error {
|
||||||
raw, _ := io.ReadAll(resp.Body)
|
raw, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
@ -120,3 +104,38 @@ func parseError(req *http.Request, resp *http.Response) error {
|
||||||
|
|
||||||
return errAPI
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,9 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,6 +26,9 @@ func setupTest(t *testing.T) (*Client, *http.ServeMux) {
|
||||||
client.HTTPClient = server.Client()
|
client.HTTPClient = server.Client()
|
||||||
client.baseURL, _ = url.Parse(server.URL)
|
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
|
return client, mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,3 +152,63 @@ func TestClient_RemoveRecord_error(t *testing.T) {
|
||||||
err := client.RemoveRecord(context.Background(), "example.com", record)
|
err := client.RemoveRecord(context.Background(), "example.com", record)
|
||||||
require.Error(t, err)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue