Merge pull request #1646 from pi-hole/new/performance_test
Add performance tests and prevent online-brute forcing
This commit is contained in:
commit
d75599c7ab
|
@ -509,7 +509,8 @@ int api_auth(struct ftl_conn *api)
|
|||
// else: Login attempt
|
||||
// - Client tries to authenticate using a password, or
|
||||
// - There no password on this machine
|
||||
if(empty_password ? true : verify_password(password, config.webserver.api.pwhash.v.s))
|
||||
const enum password_result result = empty_password ? true : verify_password(password, config.webserver.api.pwhash.v.s, true);
|
||||
if(result == PASSWORD_CORRECT)
|
||||
{
|
||||
// Accepted
|
||||
|
||||
|
@ -604,6 +605,14 @@ int api_auth(struct ftl_conn *api)
|
|||
log_warn("No free API seats available, not authenticating client");
|
||||
}
|
||||
}
|
||||
else if(result == PASSWORD_RATE_LIMITED)
|
||||
{
|
||||
// Rate limited
|
||||
return send_json_error(api, 429,
|
||||
"too_many_requests",
|
||||
"Too many requests",
|
||||
"login rate limiting");
|
||||
}
|
||||
else
|
||||
{
|
||||
log_debug(DEBUG_API, "API: Password incorrect: '%s'", password);
|
||||
|
|
|
@ -287,7 +287,7 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
|
|||
char *pwhash = strlen(elem->valuestring) > 0 ? create_password(elem->valuestring) : strdup("");
|
||||
|
||||
// Verify that the password hash is valid
|
||||
const bool verfied = verify_password(elem->valuestring, pwhash);
|
||||
const bool verfied = verify_password(elem->valuestring, pwhash, false) == PASSWORD_CORRECT;
|
||||
|
||||
if(!verfied)
|
||||
{
|
||||
|
|
|
@ -79,6 +79,14 @@ components:
|
|||
allOf:
|
||||
- $ref: 'common.yaml#/components/errors/unauthorized'
|
||||
- $ref: 'common.yaml#/components/schemas/took'
|
||||
'429':
|
||||
description: Too Many Requests
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: 'common.yaml#/components/errors/too_many_requests'
|
||||
- $ref: 'common.yaml#/components/schemas/took'
|
||||
delete:
|
||||
summary: Delete session
|
||||
tags:
|
||||
|
|
|
@ -53,6 +53,26 @@ components:
|
|||
nullable: true
|
||||
description: "No additional data available"
|
||||
example: null
|
||||
too_many_requests:
|
||||
type: object
|
||||
description: "Too many requests (rate limiting)"
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
description: "Machine-readable error type"
|
||||
example: "too_many_requests"
|
||||
message:
|
||||
type: string
|
||||
description: "Human-readable error message"
|
||||
example: "Too many requests"
|
||||
hint:
|
||||
type: string
|
||||
nullable: true
|
||||
description: "No additional data available"
|
||||
example: null
|
||||
headers:
|
||||
Location:
|
||||
description: Location of created resource
|
||||
|
|
12
src/args.c
12
src/args.c
|
@ -58,6 +58,8 @@
|
|||
#include "tools/dhcp-discover.h"
|
||||
// run_arp_scan()
|
||||
#include "tools/arp-scan.h"
|
||||
// run_performance_test()
|
||||
#include "config/password.h"
|
||||
|
||||
// defined in dnsmasq.c
|
||||
extern void print_dnsmasq_version(const char *yellow, const char *green, const char *bold, const char *normal);
|
||||
|
@ -355,6 +357,14 @@ void parse_args(int argc, char* argv[])
|
|||
exit(run_dhcp_discover());
|
||||
}
|
||||
|
||||
// Password hashing performance test
|
||||
if(argc > 1 && (strcmp(argv[1], "--perf") == 0 || strcmp(argv[1], "performance") == 0))
|
||||
{
|
||||
// Enable stdout printing
|
||||
cli_mode = true;
|
||||
exit(run_performance_test());
|
||||
}
|
||||
|
||||
// ARP scanning mode
|
||||
if(argc > 1 && strcmp(argv[1], "arp-scan") == 0)
|
||||
{
|
||||
|
@ -817,6 +827,8 @@ void parse_args(int argc, char* argv[])
|
|||
printf("\t interfaces and scan 10x more often\n");
|
||||
printf("\t%s--totp%s Generate valid TOTP token for 2FA\n", green, normal);
|
||||
printf("\t authentication (if enabled)\n");
|
||||
printf("\t%s--perf%s Run performance-tests based on the\n", green, normal);
|
||||
printf("\t BALLOON password-hashing algorithm\n");
|
||||
printf("\t%s--%s [OPTIONS]%s Pass OPTIONS to internal dnsmasq resolver\n", green, cyan, normal);
|
||||
printf("\t%s-h%s, %shelp%s Display this help and exit\n\n", green, normal, green, normal);
|
||||
exit(EXIT_SUCCESS);
|
||||
|
|
|
@ -162,7 +162,7 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
|
|||
char *pwhash = strlen(value) > 0 ? create_password(value) : strdup("");
|
||||
|
||||
// Verify that the password hash is valid
|
||||
const bool verfied = verify_password(value, pwhash);
|
||||
const bool verfied = verify_password(value, pwhash, false) == PASSWORD_CORRECT;
|
||||
|
||||
if(!verfied)
|
||||
{
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
#include "password.h"
|
||||
// genrandom() with fallback
|
||||
#include "daemon.h"
|
||||
// sleepms()
|
||||
#include "timers.h"
|
||||
|
||||
// Randomness generator
|
||||
#include "webserver/x509.h"
|
||||
|
@ -305,16 +307,39 @@ char * __attribute__((malloc)) create_password(const char *password)
|
|||
return balloon_password(password, salt, true);
|
||||
}
|
||||
|
||||
bool verify_password(const char *password, const char* pwhash)
|
||||
char verify_password(const char *password, const char* pwhash, const bool rate_limiting)
|
||||
{
|
||||
// No password supplied
|
||||
if(password == NULL || password[0] == '\0')
|
||||
return false;
|
||||
return PASSWORD_INCORRECT;
|
||||
|
||||
// No password set
|
||||
if(pwhash == NULL || pwhash[0] == '\0')
|
||||
return true;
|
||||
return PASSWORD_CORRECT;
|
||||
|
||||
// Check if there has already been one login attempt within this second
|
||||
static time_t last_password_attempt = 0;
|
||||
static unsigned int num_password_attempts = 0;
|
||||
if(rate_limiting &&
|
||||
last_password_attempt > 0 &&
|
||||
last_password_attempt == time(NULL))
|
||||
{
|
||||
// Check if we have reached the maximum number of attempts
|
||||
if(++num_password_attempts > MAX_PASSWORD_ATTEMPTS_PER_SECOND)
|
||||
{
|
||||
// Rate limit reached
|
||||
sleepms(250);
|
||||
return PASSWORD_RATE_LIMITED;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset counter
|
||||
num_password_attempts = 1;
|
||||
last_password_attempt = time(NULL);
|
||||
}
|
||||
|
||||
// Check password hash format
|
||||
if(pwhash[0] == '$')
|
||||
{
|
||||
// Parse PHC string
|
||||
|
@ -323,9 +348,9 @@ bool verify_password(const char *password, const char* pwhash)
|
|||
uint8_t *salt = NULL;
|
||||
uint8_t *config_hash = NULL;
|
||||
if(!parse_PHC_string(pwhash, &s_cost, &t_cost, &salt, &config_hash))
|
||||
return false;
|
||||
return PASSWORD_INCORRECT;
|
||||
if(salt == NULL || config_hash == NULL)
|
||||
return false;
|
||||
return PASSWORD_INCORRECT;
|
||||
char *supplied = balloon_password(password, salt, false);
|
||||
const bool result = memcmp(config_hash, supplied, SHA256_DIGEST_SIZE) == 0;
|
||||
|
||||
|
@ -336,7 +361,7 @@ bool verify_password(const char *password, const char* pwhash)
|
|||
if(config_hash != NULL)
|
||||
free(config_hash);
|
||||
|
||||
return result;
|
||||
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -361,6 +386,187 @@ bool verify_password(const char *password, const char* pwhash)
|
|||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
|
||||
}
|
||||
}
|
||||
|
||||
static double sqroot(double square)
|
||||
{
|
||||
double root = square / 3.0;
|
||||
if (square <= 0) return 0.0;
|
||||
for (unsigned int i=0; i<32; i++)
|
||||
root = (root + square / root) / 2;
|
||||
return root;
|
||||
}
|
||||
|
||||
static int performance_test_task(const size_t s_cost, const size_t t_cost, const uint8_t password[],
|
||||
const size_t pwlen, uint8_t salt[SALT_LEN],
|
||||
double *elapsed1, double *elapsed2)
|
||||
{
|
||||
struct timespec start, end, end2;
|
||||
// Scratch buffer scratch is a user allocated working space required by
|
||||
// the algorithm. To determine the required size of the scratch buffer
|
||||
// use the utility function balloon_itch. Output of BALLOON algorithm
|
||||
// will be written into the output buffer dst that has to be at least
|
||||
// digest_size bytes long.
|
||||
const size_t scratch_size = balloon_itch(SHA256_DIGEST_SIZE, s_cost);
|
||||
uint8_t *scratch = calloc(scratch_size, sizeof(uint8_t));
|
||||
if(scratch == NULL)
|
||||
{
|
||||
printf("Could not allocate %zu bytes of memory for test!\n", scratch_size);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Record starting time
|
||||
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||
|
||||
// Compute hash of given password password salted with salt and write
|
||||
// the result into the output buffer dst
|
||||
balloon_sha256(s_cost, t_cost, pwlen, password, SALT_LEN, salt, scratch, scratch);
|
||||
|
||||
// Record end time
|
||||
clock_gettime(CLOCK_MONOTONIC, &end);
|
||||
|
||||
// Compute hash of given password password salted with salt and write
|
||||
// the result into the output buffer dst
|
||||
balloon_sha256(s_cost, t_cost, pwlen, password, SALT_LEN, salt, scratch, scratch);
|
||||
|
||||
// Record end time
|
||||
clock_gettime(CLOCK_MONOTONIC, &end2);
|
||||
|
||||
// Free allocated memory
|
||||
free(scratch);
|
||||
|
||||
// Compute elapsed time
|
||||
*elapsed1 = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1000000000.0;
|
||||
*elapsed2 = (end2.tv_sec - end.tv_sec) + (end2.tv_nsec - end.tv_nsec) / 1000000000.0;
|
||||
char prefix[2] = { 0 };
|
||||
double formatted = 0.0;
|
||||
format_memory_size(prefix, (unsigned long long)scratch_size, &formatted);
|
||||
const double avg = (*elapsed1 + *elapsed2)/2;
|
||||
const double stdev = sqroot(((*elapsed1 - avg)*(*elapsed1 - avg) + (*elapsed2 - avg)*(*elapsed2 - avg))/2);
|
||||
printf("s = %5zu, t = %5zu took %6.1f +/- %4.1f ms (scratch buffer %6.1f%1sB) -> %.0f\n",
|
||||
s_cost, t_cost, 1e3*avg, 1e3*stdev, formatted, prefix, 1.0*(s_cost*t_cost)/avg);
|
||||
|
||||
// Break if test took longer than two seconds
|
||||
if(avg > 2)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Run performance tests until individual test result gets beyond 3 seconds
|
||||
int run_performance_test(void)
|
||||
{
|
||||
struct timespec start, end;
|
||||
// Record starting time
|
||||
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||
|
||||
// Test password
|
||||
const uint8_t password[] = "abcdefghijklmnopqrstuvwxyz0123456789!\"§$%&/()=?";
|
||||
|
||||
// Generate a 128 bit random salt
|
||||
// genrandom() returns cryptographically secure random data
|
||||
uint8_t salt[SALT_LEN] = { 0 };
|
||||
if(getrandom(salt, sizeof(salt), 0) < 0)
|
||||
{
|
||||
printf("Could not generate random salt!\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
printf("Running time-performance test:\n");
|
||||
size_t t_t_cost = 16;
|
||||
const size_t t_s_cost = 512;
|
||||
cJSON *time_test = cJSON_CreateArray();
|
||||
unsigned int i = 0;
|
||||
while(true)
|
||||
{
|
||||
double elapsed1 = 0.0, elapsed2 = 0.0;
|
||||
const int ret = performance_test_task(t_s_cost, t_t_cost, password, sizeof(password), salt,
|
||||
&elapsed1, &elapsed2);
|
||||
|
||||
if(ret == -1)
|
||||
return EXIT_FAILURE;
|
||||
|
||||
if(i > 0)
|
||||
{
|
||||
// We do not want to include the first test in the
|
||||
// average as the first call is slower
|
||||
cJSON_AddItemToArray(time_test, cJSON_CreateNumber(1.0*(t_s_cost*t_t_cost)/elapsed1));
|
||||
cJSON_AddItemToArray(time_test, cJSON_CreateNumber(1.0*(t_s_cost*t_t_cost)/elapsed2));
|
||||
}
|
||||
|
||||
if(ret == 1)
|
||||
break;
|
||||
|
||||
// Double time costs
|
||||
t_t_cost *= 2;
|
||||
i++;
|
||||
}
|
||||
|
||||
printf("\nRunning space-performance test:\n");
|
||||
const size_t s_t_cost = 512;
|
||||
size_t s_s_cost = 8;
|
||||
cJSON *space_test = cJSON_CreateArray();
|
||||
while(true)
|
||||
{
|
||||
double elapsed1 = 0.0, elapsed2 = 0.0;
|
||||
const int ret = performance_test_task(s_s_cost, s_t_cost, password, sizeof(password), salt,
|
||||
&elapsed1, &elapsed2);
|
||||
|
||||
cJSON_AddItemToArray(space_test, cJSON_CreateNumber(1.0*(s_t_cost*s_s_cost)/elapsed1));
|
||||
cJSON_AddItemToArray(space_test, cJSON_CreateNumber(1.0*(s_t_cost*s_s_cost)/elapsed2));
|
||||
|
||||
if(ret == -1)
|
||||
return EXIT_FAILURE;
|
||||
else if(ret == 1)
|
||||
break;
|
||||
|
||||
// Double space costs
|
||||
s_s_cost *= 2;
|
||||
}
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &end);
|
||||
const double elapsed = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1000000000.0;
|
||||
|
||||
// Compute average time and space costs from data in cJSON arrays
|
||||
cJSON *item = NULL;
|
||||
double t_avg_sum1 = 0.0;
|
||||
cJSON_ArrayForEach(item, time_test)
|
||||
{
|
||||
t_avg_sum1 += item->valuedouble;
|
||||
}
|
||||
t_avg_sum1 /= cJSON_GetArraySize(time_test);
|
||||
|
||||
double s_avg_sum1 = 0.0;
|
||||
cJSON_ArrayForEach(item, space_test)
|
||||
{
|
||||
s_avg_sum1 += item->valuedouble;
|
||||
}
|
||||
s_avg_sum1 /= cJSON_GetArraySize(space_test);
|
||||
|
||||
// Get standard deviations
|
||||
double t_stdev_sum1 = 0.0;
|
||||
cJSON_ArrayForEach(item, time_test)
|
||||
{
|
||||
t_stdev_sum1 += (item->valuedouble - t_avg_sum1)*(item->valuedouble - t_avg_sum1);
|
||||
}
|
||||
t_stdev_sum1 = sqroot(t_stdev_sum1/cJSON_GetArraySize(time_test));
|
||||
|
||||
double s_stdev_sum1 = 0.0;
|
||||
cJSON_ArrayForEach(item, space_test)
|
||||
{
|
||||
s_stdev_sum1 += (item->valuedouble - s_avg_sum1)*(item->valuedouble - s_avg_sum1);
|
||||
}
|
||||
s_stdev_sum1 = sqroot(s_stdev_sum1/cJSON_GetArraySize(space_test));
|
||||
|
||||
// Free allocated memory
|
||||
cJSON_Delete(time_test);
|
||||
cJSON_Delete(space_test);
|
||||
|
||||
// Print results
|
||||
printf("\nAverage time-performance index: %9.0f +/- %.0f (s = %zu)\n", t_avg_sum1, t_stdev_sum1, t_s_cost);
|
||||
printf("Average space-performance index: %9.0f +/- %.0f (t = %zu)\n", s_avg_sum1, s_stdev_sum1, s_t_cost);
|
||||
printf("\nTotal test time: %.1f seconds\n\n", elapsed);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,16 @@
|
|||
|
||||
void sha256_raw_to_hex(uint8_t *data, char *buffer);
|
||||
char *create_password(const char *password) __attribute__((malloc));
|
||||
bool verify_password(const char *password, const char *pwhash);
|
||||
char verify_password(const char *password, const char *pwhash, const bool rate_limiting);
|
||||
int run_performance_test(void);
|
||||
|
||||
enum password_result {
|
||||
PASSWORD_INCORRECT = 0,
|
||||
PASSWORD_CORRECT = 1,
|
||||
PASSWORD_RATE_LIMITED = -1
|
||||
} __attribute__((packed));
|
||||
|
||||
// The maximum number of password attempts per second
|
||||
#define MAX_PASSWORD_ATTEMPTS_PER_SECOND 3
|
||||
|
||||
#endif //PASSWORD_H
|
||||
|
|
|
@ -43,9 +43,10 @@ class FTLAPI():
|
|||
self.verbose = False
|
||||
|
||||
# Login to FTL API
|
||||
self.login(password)
|
||||
if self.session is None or 'valid' not in self.session or not self.session['valid']:
|
||||
raise Exception("Could not login to FTL API")
|
||||
if password is not None:
|
||||
self.login(password)
|
||||
if self.session is None or 'valid' not in self.session or not self.session['valid']:
|
||||
raise Exception("Could not login to FTL API")
|
||||
|
||||
def login(self, password: str = None):
|
||||
# Check if we even need to login
|
||||
|
@ -65,6 +66,8 @@ class FTLAPI():
|
|||
return
|
||||
|
||||
response = self.POST("/api/auth", {"password": password})
|
||||
if "error" in response:
|
||||
raise Exception("FTL returned error: " + json.dumps(response["error"]))
|
||||
if 'session' not in response:
|
||||
raise Exception("FTL returned invalid response item")
|
||||
self.session = response["session"]
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Script that sends a number of randomly generated passwords to the
|
||||
# /api/auth endpoint checking that rate limiting is enforced
|
||||
import random
|
||||
import string
|
||||
from libs.FTLAPI import FTLAPI
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create FTLAPI object
|
||||
ftl = FTLAPI("http://127.0.0.1:8080")
|
||||
|
||||
# Try to login with random passwords
|
||||
for i in range(0, 100):
|
||||
pw = "".join(random.choices(string.printable, k=random.randint(1, 64)))
|
||||
try:
|
||||
ftl.login(pw)
|
||||
except Exception as e:
|
||||
if "too_many_requests" in str(e):
|
||||
print("Rate-limited on attempt no. " + str(i))
|
||||
exit(0)
|
||||
else:
|
||||
print("Unexpected error: " + str(e))
|
||||
exit(1)
|
||||
print("Rate-limiting was not enforced")
|
||||
exit(1)
|
|
@ -130,6 +130,11 @@ kill "$(pidof pihole-FTL)"
|
|||
# Restore umask
|
||||
umask "$OLDUMASK"
|
||||
|
||||
# Run performance tests
|
||||
if ! su pihole -s /bin/sh -c "/home/pihole/pihole-FTL --perf"; then
|
||||
echo "pihole-FTL --perf failed to start"
|
||||
fi
|
||||
|
||||
# Remove copied file
|
||||
rm /home/pihole/pihole-FTL
|
||||
|
||||
|
|
Loading…
Reference in New Issue