- Add admin account list, create, role, delete, and password workflows - Execute identity-checked mutations through the existing dispatcher - Bound queued credential lifetime and wipe transient secrets - Add explicit password generation with saved-value acknowledgement - Handle self-revocation and uncertain outcomes without automatic retries - Register optional account routes without disrupting terminal transports - Expand host regressions and document contracts and pending target checks Validated host suites and pio run; hardware validation remains pending.
262 lines
9.6 KiB
C
262 lines
9.6 KiB
C
/* SPDX-License-Identifier: GPL-3.0-only */
|
|
#include "web_auth_parse.h"
|
|
|
|
#include <string.h>
|
|
|
|
static void wipe(void *buffer, size_t length)
|
|
{
|
|
volatile uint8_t *p = buffer;
|
|
while (length--) *p++ = 0;
|
|
}
|
|
|
|
static bool alnum_ascii(unsigned char c)
|
|
{
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9');
|
|
}
|
|
|
|
static bool authority(const char *text, size_t length, char *out)
|
|
{
|
|
if (!text || !length || length > WEB_AUTH_ORIGIN_CAPACITY - 5U) return false;
|
|
if (length >= 4U && memcmp(text + length - 4U, ":443", 4U) == 0) length -= 4U;
|
|
if (!length || length > WEB_AUTH_ORIGIN_CAPACITY - 9U) return false;
|
|
size_t label = 0;
|
|
for (size_t i = 0; i < length; ++i) {
|
|
unsigned char c = (unsigned char)text[i];
|
|
if (c == '.') {
|
|
if (!label || text[i - 1U] == '-') return false;
|
|
label = 0;
|
|
} else {
|
|
if (!alnum_ascii(c) && c != '-') return false;
|
|
if ((!label && c == '-') || ++label > 63U) return false;
|
|
}
|
|
out[i] = c >= 'A' && c <= 'Z' ? (char)(c + ('a' - 'A')) : (char)c;
|
|
}
|
|
if (!label || text[length - 1U] == '-') return false;
|
|
out[length] = 0;
|
|
return true;
|
|
}
|
|
|
|
bool web_auth_parse_origin(const char *host, size_t host_length,
|
|
const char *origin, size_t origin_length,
|
|
char canonical[WEB_AUTH_ORIGIN_CAPACITY])
|
|
{
|
|
if (!canonical) return false;
|
|
memset(canonical, 0, WEB_AUTH_ORIGIN_CAPACITY);
|
|
char other[WEB_AUTH_ORIGIN_CAPACITY] = {0};
|
|
if (!origin || origin_length < 9U || origin_length > WEB_AUTH_ORIGIN_CAPACITY + 3U ||
|
|
memcmp(origin, "https://", 8U) != 0 ||
|
|
!authority(host, host_length, canonical + 8U) ||
|
|
!authority(origin + 8U, origin_length - 8U, other) ||
|
|
strcmp(canonical + 8U, other) != 0) {
|
|
memset(canonical, 0, WEB_AUTH_ORIGIN_CAPACITY);
|
|
return false;
|
|
}
|
|
memcpy(canonical, "https://", 8U);
|
|
return true;
|
|
}
|
|
|
|
static bool cookie_name_char(unsigned char c)
|
|
{
|
|
return alnum_ascii(c) || (c && strchr("!#$%&'*+-.^_`|~", c));
|
|
}
|
|
|
|
bool web_auth_parse_optional_cookie(const char *header, size_t length, const char *name,
|
|
char token[WEB_AUTH_TOKEN_LENGTH + 1U], bool *present)
|
|
{
|
|
if (!present) return false;
|
|
*present = false;
|
|
if (!token) return false;
|
|
memset(token, 0, WEB_AUTH_TOKEN_LENGTH + 1U);
|
|
if (!header || !name || !*name || !length || length > WEB_AUTH_COOKIE_HEADER_MAX)
|
|
return false;
|
|
size_t pos = 0, selected = 0, name_length = strlen(name);
|
|
bool found = false;
|
|
while (pos < length) {
|
|
while (pos < length && header[pos] == ' ') ++pos;
|
|
size_t start = pos;
|
|
while (pos < length && cookie_name_char((unsigned char)header[pos])) ++pos;
|
|
size_t key_length = pos - start;
|
|
if (!key_length || pos == length || header[pos++] != '=') return false;
|
|
size_t value = pos;
|
|
while (pos < length && header[pos] != ';') {
|
|
unsigned char c = (unsigned char)header[pos++];
|
|
if (c < 0x21 || c > 0x7e || c == '"' || c == ',' || c == '\\') return false;
|
|
}
|
|
if (key_length == name_length && memcmp(header + start, name, key_length) == 0) {
|
|
if (found || pos - value != WEB_AUTH_TOKEN_LENGTH) return false;
|
|
for (size_t i = value; i < pos; ++i)
|
|
if (!((header[i] >= '0' && header[i] <= '9') ||
|
|
(header[i] >= 'a' && header[i] <= 'f'))) return false;
|
|
found = true;
|
|
selected = value;
|
|
}
|
|
if (pos < length && ++pos == length) return false;
|
|
}
|
|
if (found) memcpy(token, header + selected, WEB_AUTH_TOKEN_LENGTH);
|
|
*present = found;
|
|
return true;
|
|
}
|
|
|
|
bool web_auth_parse_cookie(const char *header, size_t length, const char *name,
|
|
char token[WEB_AUTH_TOKEN_LENGTH + 1U])
|
|
{
|
|
bool present = false;
|
|
return web_auth_parse_optional_cookie(header, length, name, token, &present) && present;
|
|
}
|
|
|
|
typedef struct { const uint8_t *data; size_t length; size_t pos; } json_cursor_t;
|
|
|
|
static void whitespace(json_cursor_t *c)
|
|
{
|
|
while (c->pos < c->length) {
|
|
uint8_t b = c->data[c->pos];
|
|
if (b != ' ' && b != '\t' && b != '\r' && b != '\n') break;
|
|
++c->pos;
|
|
}
|
|
}
|
|
|
|
static bool take(json_cursor_t *c, uint8_t byte)
|
|
{
|
|
whitespace(c);
|
|
if (c->pos == c->length || c->data[c->pos] != byte) return false;
|
|
++c->pos;
|
|
return true;
|
|
}
|
|
|
|
static bool hex4(json_cursor_t *c, uint32_t *value)
|
|
{
|
|
*value = 0;
|
|
for (unsigned i = 0; i < 4; ++i) {
|
|
if (c->pos == c->length) return false;
|
|
uint8_t b = c->data[c->pos++];
|
|
unsigned digit;
|
|
if (b >= '0' && b <= '9') digit = b - '0';
|
|
else if (b >= 'a' && b <= 'f') digit = b - 'a' + 10U;
|
|
else if (b >= 'A' && b <= 'F') digit = b - 'A' + 10U;
|
|
else return false;
|
|
*value = (*value << 4) | digit;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool codepoint(json_cursor_t *c, uint32_t *value)
|
|
{
|
|
if (c->pos == c->length) return false;
|
|
uint8_t b = c->data[c->pos++];
|
|
if (b == '\\') {
|
|
if (c->pos == c->length) return false;
|
|
b = c->data[c->pos++];
|
|
switch (b) {
|
|
case '"': case '\\': case '/': *value = b; return true;
|
|
case 'b': *value = 8; return true;
|
|
case 'f': *value = 12; return true;
|
|
case 'n': *value = 10; return true;
|
|
case 'r': *value = 13; return true;
|
|
case 't': *value = 9; return true;
|
|
case 'u': break;
|
|
default: return false;
|
|
}
|
|
if (!hex4(c, value)) return false;
|
|
if (*value >= 0xd800 && *value <= 0xdbff) {
|
|
uint32_t low;
|
|
if (c->length - c->pos < 2U || c->data[c->pos++] != '\\' ||
|
|
c->data[c->pos++] != 'u' || !hex4(c, &low) ||
|
|
low < 0xdc00 || low > 0xdfff) return false;
|
|
*value = 0x10000 + ((*value - 0xd800) << 10) + low - 0xdc00;
|
|
}
|
|
return *value && !(*value >= 0xd800 && *value <= 0xdfff);
|
|
}
|
|
if (b < 0x20) return false;
|
|
if (b < 0x80) { *value = b; return true; }
|
|
unsigned extra;
|
|
uint32_t minimum;
|
|
if (b >= 0xc2 && b <= 0xdf) { extra = 1; minimum = 0x80; *value = b & 0x1f; }
|
|
else if (b >= 0xe0 && b <= 0xef) { extra = 2; minimum = 0x800; *value = b & 0x0f; }
|
|
else if (b >= 0xf0 && b <= 0xf4) { extra = 3; minimum = 0x10000; *value = b & 7; }
|
|
else return false;
|
|
while (extra--) {
|
|
if (c->pos == c->length) return false;
|
|
b = c->data[c->pos++];
|
|
if ((b & 0xc0) != 0x80) return false;
|
|
*value = (*value << 6) | (b & 0x3f);
|
|
}
|
|
return *value >= minimum && *value <= 0x10ffff &&
|
|
!(*value >= 0xd800 && *value <= 0xdfff);
|
|
}
|
|
|
|
static bool string(json_cursor_t *c, uint8_t *out, size_t capacity, size_t *length)
|
|
{
|
|
*length = 0;
|
|
if (!take(c, '"')) return false;
|
|
while (c->pos < c->length && c->data[c->pos] != '"') {
|
|
uint32_t cp;
|
|
if (!codepoint(c, &cp)) return false;
|
|
size_t bytes = cp < 0x80 ? 1U : cp < 0x800 ? 2U : cp < 0x10000 ? 3U : 4U;
|
|
if (bytes > capacity - *length) return false;
|
|
if (bytes == 1U) out[(*length)++] = (uint8_t)cp;
|
|
else {
|
|
out[(*length)++] = (uint8_t)((bytes == 2U ? 0xc0 : bytes == 3U ? 0xe0 : 0xf0) |
|
|
(cp >> (6U * (bytes - 1U))));
|
|
for (size_t i = bytes - 1U; i > 0; --i)
|
|
out[(*length)++] = (uint8_t)(0x80 | ((cp >> (6U * (i - 1U))) & 0x3f));
|
|
}
|
|
}
|
|
if (c->pos == c->length) return false;
|
|
++c->pos;
|
|
out[*length] = 0;
|
|
return true;
|
|
}
|
|
|
|
bool web_auth_parse_json_string(const char *body, size_t length, size_t *position,
|
|
uint8_t *output, size_t capacity, size_t *decoded_length)
|
|
{
|
|
if (output && capacity) wipe(output, capacity);
|
|
if (decoded_length) *decoded_length = 0;
|
|
if (!body || !position || *position > length || !output || !capacity || !decoded_length)
|
|
return false;
|
|
json_cursor_t c = { (const uint8_t *)body, length, *position };
|
|
if (!string(&c, output, capacity - 1U, decoded_length)) {
|
|
wipe(output, capacity);
|
|
*decoded_length = 0;
|
|
return false;
|
|
}
|
|
*position = c.pos;
|
|
return true;
|
|
}
|
|
|
|
bool web_auth_parse_login(const char *body, size_t length,
|
|
web_auth_credentials_t *credentials)
|
|
{
|
|
if (!credentials) return false;
|
|
wipe(credentials, sizeof(*credentials));
|
|
if (!body || !length || length > WEB_AUTH_LOGIN_BODY_MAX) return false;
|
|
json_cursor_t c = { (const uint8_t *)body, length, 0 };
|
|
unsigned seen = 0;
|
|
if (!take(&c, '{')) return false;
|
|
for (unsigned field = 0; field < 2; ++field) {
|
|
uint8_t key[9] = {0};
|
|
size_t key_length;
|
|
if ((field && !take(&c, ',')) || !string(&c, key, 8U, &key_length) ||
|
|
!take(&c, ':')) goto invalid;
|
|
unsigned bit;
|
|
uint8_t *output;
|
|
size_t *output_length, capacity;
|
|
if (key_length == 8U && memcmp(key, "username", 8U) == 0) {
|
|
bit = 1; output = credentials->username;
|
|
output_length = &credentials->username_length; capacity = WEB_AUTH_USERNAME_MAX;
|
|
} else if (key_length == 8U && memcmp(key, "password", 8U) == 0) {
|
|
bit = 2; output = credentials->password;
|
|
output_length = &credentials->password_length; capacity = WEB_AUTH_PASSWORD_MAX;
|
|
} else goto invalid;
|
|
if ((seen & bit) || !string(&c, output, capacity, output_length)) goto invalid;
|
|
seen |= bit;
|
|
}
|
|
if (!take(&c, '}')) goto invalid;
|
|
whitespace(&c);
|
|
if (c.pos == c.length && seen == 3U) return true;
|
|
invalid:
|
|
wipe(credentials, sizeof(*credentials));
|
|
return false;
|
|
}
|