Add allocation-free web auth parsers

Implement strict origin, cookie, and login JSON parsing with
fail-closed validation and output wiping. Add focused host contract
tests
and document the preparatory 8D.3 parser split.
This commit is contained in:
2026-09-05 18:22:15 +02:00
parent a62a655ac1
commit 00f226dc59
8 changed files with 512 additions and 4 deletions
+234
View File
@@ -0,0 +1,234 @@
/* 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_cookie(const char *header, size_t length, const char *name,
char token[WEB_AUTH_TOKEN_LENGTH + 1U])
{
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);
return found;
}
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_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;
}