diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index bea31a7..99a4ffe 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -33,6 +33,13 @@ jobs: run: | make test + - name: Run Auth Privilege Boundary Regression + run: | + gcc -std=c99 -Wall -Wextra -I. -I./include -I./core \ + tests/test_auth_privilege_boundary.c -L. -lqihse \ + -o tests/test_auth_privilege_boundary + LD_LIBRARY_PATH=$PWD ./tests/test_auth_privilege_boundary + - name: Run Python SDK Tests run: | export LD_LIBRARY_PATH=$PWD diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c53a27c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,35 @@ +# QIHSE Repository Rules + +These rules are architectural invariants, not suggestions. A code path that violates them is incorrect even if the feature works, benchmarks well, or existing tests pass. Existing violations are defects and must not be copied into new code. + +## Security invariants + +### 1. No classified read primitive without a user/security context + +Any primitive capable of materializing, enumerating, searching, streaming, exporting, serializing, snapshotting, backing up, iterating over, or otherwise disclosing data that may carry classification or SCI metadata MUST accept or inherit an explicit authenticated security context (`qihse_user_t *` or an equivalent typed context). + +Protocol, SDK, compatibility, export, backup, iterator, and internal adapter layers MUST propagate that identity to the lowest data-retrieval layer. They MUST NOT fall back to a context-free/raw read primitive for classified-capable data. + +`NULL` MUST NOT accidentally become an authorization bypass. If QIHSE supports an explicitly security-disabled operating mode, that mode must be represented deliberately in configuration/context state rather than inferred from a forgotten user argument. + +### 2. No principal may create or modify a principal above itself + +A principal MUST NOT create, promote, or modify another principal to a privilege level greater than its own. + +This applies to role, classification level, SCI compartments, account-management capability, authentication bypasses, hardware-token policy, delegated user-creation capability, and any future privilege-bearing field. + +`can_create_users` delegates account creation only. It does not delegate Operator authority and MUST NOT permit creation or promotion of a principal above the creator. + +### 3. Every new protocol adapter requires a low-clearance/high-data negative test + +Every new protocol, wire-format adapter, compatibility layer, or externally reachable data-access surface MUST include an automated negative authorization test before merge. + +The test MUST authenticate or construct a low-clearance principal, place or reference data above that principal's clearance and/or outside its SCI compartments, attempt access through the new adapter, and assert denial with no protected payload disclosure. + +Where the adapter exposes them, the test SHOULD cover both the normal query path and bypass-prone forms such as direct-ID lookup, enumeration, export, bulk read, snapshot/backup, iterator, or raw compatibility commands. + +The negative test MUST run in CI. A successful happy-path test is not a substitute. + +## Merge rule + +Changes that violate any invariant above are merge blockers. Performance, compatibility, internal-only deployment, prototype status, or convenience are not exceptions. If a new capability cannot preserve an invariant yet, keep it behind an explicitly insecure/development-only boundary rather than weakening the invariant silently. diff --git a/core/qihse_auth.c b/core/qihse_auth.c index 5910d86..dd3b0ed 100644 --- a/core/qihse_auth.c +++ b/core/qihse_auth.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -87,7 +88,21 @@ typedef struct { time_t lockout_until; } auth_rate_limit_t; +/* qihse_user_t is public and mutable, so privilege-bearing fields on a + * caller-provided object are not authoritative. Administrative decisions use + * this private state and resolve the presented pointer back to a registered + * principal. */ +typedef struct { + bool active; + uint16_t role; + uint16_t classification_level; + uint16_t sci_compartments; + bool requires_hardware_token; + bool can_create_users; +} authz_state_t; + static qihse_user_t* users[MAX_USERS]; +static authz_state_t authz_states[MAX_USERS]; static pthread_mutex_t auth_mutex = PTHREAD_MUTEX_INITIALIZER; static uint32_t active_user_count = 0; static auth_rate_limit_t rate_limits[MAX_USERS]; @@ -111,10 +126,37 @@ static void compute_sha384_hex(const char *input, char *output) { OPENSSL_cleanse(hash, sizeof(hash)); } +/* auth_mutex must be held. */ +static bool resolve_authoritative_user_locked(const qihse_user_t* presented, + uint32_t* out_user_id, + authz_state_t* out_state) { + if (!presented) return false; + for (uint32_t i = 0; i < MAX_USERS; i++) { + if (users[i] == presented && authz_states[i].active) { + if (out_user_id) *out_user_id = i; + if (out_state) *out_state = authz_states[i]; + return true; + } + } + return false; +} + +static void set_authz_state_locked(uint32_t user_id, const qihse_user_t* user) { + if (user_id >= MAX_USERS || !user) return; + authz_states[user_id].active = true; + authz_states[user_id].role = user->role; + authz_states[user_id].classification_level = user->classification_level; + authz_states[user_id].sci_compartments = user->sci_compartments; + authz_states[user_id].requires_hardware_token = user->requires_hardware_token; + authz_states[user_id].can_create_users = user->can_create_users; +} + void qihse_auth_init(void) { qihse_audit_init(); pthread_mutex_lock(&auth_mutex); memset(users, 0, sizeof(users)); + memset(authz_states, 0, sizeof(authz_states)); + memset(rate_limits, 0, sizeof(rate_limits)); active_user_count = 0; // PRE-SEED THE SYSTEM OPERATOR (User ID 0) @@ -134,14 +176,22 @@ void qihse_auth_init(void) { mlock(op, sizeof(qihse_user_t)); #endif users[0] = op; + set_authz_state_locked(0, op); active_user_count = 1; } pthread_mutex_unlock(&auth_mutex); } -qihse_user_t* qihse_auth_create_user(qihse_user_t* creator, uint32_t user_id, uint16_t role, uint16_t classif, uint16_t sci, const char* plaintext_password, bool requires_hw_token) { +qihse_user_t* qihse_auth_create_user(qihse_user_t* creator, uint32_t user_id, + uint16_t role, uint16_t classif, + uint16_t sci, const char* plaintext_password, + bool requires_hw_token) { if (user_id >= MAX_USERS) return NULL; + if (!plaintext_password || strlen(plaintext_password) < 12) { + fprintf(stderr, "[SECURITY ERROR] Password for User ID %u rejected: minimum 12 characters required.\n", user_id); + return NULL; + } pthread_mutex_lock(&auth_mutex); @@ -155,24 +205,50 @@ qihse_user_t* qihse_auth_create_user(qihse_user_t* creator, uint32_t user_id, ui return NULL; } } + + uint32_t creator_id = 0xFFFFFFFFu; + authz_state_t creator_authz; + if (!resolve_authoritative_user_locked(creator, &creator_id, &creator_authz)) { + qihse_audit_log("USER_CREATE_DENIED_INVALID_CREATOR", creator_id, user_id, classif, sci); + pthread_mutex_unlock(&auth_mutex); + return NULL; + } - // Enforce that only an OPERATOR or delegated user can create new users. - if (!creator || (creator->role != QIHSE_ROLE_OPERATOR && !creator->can_create_users)) { + // Enforce that only an authoritative OPERATOR or delegated user can create new users. + if (creator_authz.role != QIHSE_ROLE_OPERATOR && !creator_authz.can_create_users) { + qihse_audit_log("USER_CREATE_DENIED_DELEGATION", creator_id, user_id, classif, sci); + pthread_mutex_unlock(&auth_mutex); + return NULL; + } + + // Role numbers are ordered from most to least privileged (OPERATOR=0). + if (role < creator_authz.role) { + qihse_audit_log("USER_CREATE_DENIED_ROLE", creator_id, user_id, classif, sci); pthread_mutex_unlock(&auth_mutex); return NULL; } - // Ensure the creator possesses the clearance they are attempting to grant - if (classif > creator->classification_level) { + // Ensure the creator possesses the clearance they are attempting to grant. + if (classif > creator_authz.classification_level) { + qihse_audit_log("USER_CREATE_DENIED_CLEARANCE", creator_id, user_id, classif, sci); pthread_mutex_unlock(&auth_mutex); return NULL; } - if ((sci & creator->sci_compartments) != sci) { + if ((sci & creator_authz.sci_compartments) != sci) { + qihse_audit_log("USER_CREATE_DENIED_SCI", creator_id, user_id, classif, sci); + pthread_mutex_unlock(&auth_mutex); + return NULL; + } + + // Delegation must not weaken an enforced hardware-token requirement. + if (creator_authz.role != QIHSE_ROLE_OPERATOR && + creator_authz.requires_hardware_token && !requires_hw_token) { + qihse_audit_log("USER_CREATE_DENIED_TOKEN_POLICY", creator_id, user_id, classif, sci); pthread_mutex_unlock(&auth_mutex); return NULL; } - // Do not allow overwriting an active session; must explicitly destroy first + // Do not allow overwriting an active session; must explicitly destroy first. if (users[user_id] != NULL) { pthread_mutex_unlock(&auth_mutex); return NULL; @@ -189,37 +265,26 @@ qihse_user_t* qihse_auth_create_user(qihse_user_t* creator, uint32_t user_id, ui if (role == QIHSE_ROLE_OPERATOR) { // Operators get God Mode - u->classification_level = 0xFFFF; // Max possible clearance - u->sci_compartments = 0xFFFF; // All compartments + u->classification_level = 0xFFFF; + u->sci_compartments = 0xFFFF; } else { u->classification_level = classif; u->sci_compartments = sci; } - // Handle Password - if (plaintext_password) { - if (strlen(plaintext_password) < 12) { - fprintf(stderr, "[SECURITY ERROR] Password for User ID %u rejected: minimum 12 characters required.\n", user_id); - free(u); - pthread_mutex_unlock(&auth_mutex); - return NULL; - } - compute_sha384_hex(plaintext_password, u->password_hash); - } else { - compute_sha384_hex("", u->password_hash); - } - + compute_sha384_hex(plaintext_password, u->password_hash); u->requires_hardware_token = requires_hw_token; u->can_create_users = false; snprintf(u->username, 64, "User_%u", user_id); users[user_id] = u; + set_authz_state_locked(user_id, u); active_user_count++; #ifndef _WIN32 mlock(u, sizeof(qihse_user_t)); #endif - qihse_audit_log("USER_CREATE", creator->user_id, user_id, u->classification_level, u->sci_compartments); + qihse_audit_log("USER_CREATE", creator_id, user_id, u->classification_level, u->sci_compartments); pthread_mutex_unlock(&auth_mutex); return u; @@ -255,21 +320,28 @@ void qihse_auth_destroy_user(uint32_t user_id) { pthread_mutex_lock(&auth_mutex); if (users[user_id]) { - qihse_audit_log("USER_DESTROY", 0, user_id, users[user_id]->classification_level, users[user_id]->sci_compartments); + qihse_audit_log("USER_DESTROY", 0, user_id, + authz_states[user_id].classification_level, + authz_states[user_id].sci_compartments); OPENSSL_cleanse(users[user_id], sizeof(qihse_user_t)); #ifndef _WIN32 munlock(users[user_id], sizeof(qihse_user_t)); #endif free(users[user_id]); users[user_id] = NULL; + memset(&authz_states[user_id], 0, sizeof(authz_states[user_id])); + memset(&rate_limits[user_id], 0, sizeof(rate_limits[user_id])); active_user_count--; } pthread_mutex_unlock(&auth_mutex); } -bool qihse_auth_modify_user(qihse_user_t* operator_user, uint32_t target_user_id, const char* new_username, const char* new_password, int new_requires_hw_token, int new_can_create_users) { - if (!operator_user || operator_user->role != QIHSE_ROLE_OPERATOR) { - qihse_audit_log("USER_MODIFY_DENIED", operator_user ? operator_user->user_id : 0xFFFFFFFF, target_user_id, 0, 0); +bool qihse_auth_modify_user(qihse_user_t* operator_user, uint32_t target_user_id, + const char* new_username, const char* new_password, + int new_requires_hw_token, int new_can_create_users) { + if (target_user_id >= MAX_USERS) return false; + if (new_password != NULL && strlen(new_password) < 12) { + fprintf(stderr, "[SECURITY ERROR] Password for User ID %u rejected: minimum 12 characters required.\n", target_user_id); return false; } @@ -278,11 +350,18 @@ bool qihse_auth_modify_user(qihse_user_t* operator_user, uint32_t target_user_id return false; } - if (target_user_id >= MAX_USERS) return false; - pthread_mutex_lock(&auth_mutex); + uint32_t operator_id = 0xFFFFFFFFu; + authz_state_t operator_authz; + if (!resolve_authoritative_user_locked(operator_user, &operator_id, &operator_authz) || + operator_authz.role != QIHSE_ROLE_OPERATOR) { + pthread_mutex_unlock(&auth_mutex); + qihse_audit_log("USER_MODIFY_DENIED", operator_id, target_user_id, 0, 0); + return false; + } + qihse_user_t* target = users[target_user_id]; - if (!target) { + if (!target || !authz_states[target_user_id].active) { pthread_mutex_unlock(&auth_mutex); return false; } @@ -297,14 +376,20 @@ bool qihse_auth_modify_user(qihse_user_t* operator_user, uint32_t target_user_id } if (new_requires_hw_token != -1) { - target->requires_hardware_token = (new_requires_hw_token != 0); + bool value = (new_requires_hw_token != 0); + target->requires_hardware_token = value; + authz_states[target_user_id].requires_hardware_token = value; } if (new_can_create_users != -1) { - target->can_create_users = (new_can_create_users != 0); + bool value = (new_can_create_users != 0); + target->can_create_users = value; + authz_states[target_user_id].can_create_users = value; } - qihse_audit_log("USER_MODIFY", operator_user->user_id, target_user_id, target->classification_level, target->sci_compartments); + qihse_audit_log("USER_MODIFY", operator_id, target_user_id, + authz_states[target_user_id].classification_level, + authz_states[target_user_id].sci_compartments); pthread_mutex_unlock(&auth_mutex); return true; } diff --git a/tests/test_auth_privilege_boundary.c b/tests/test_auth_privilege_boundary.c new file mode 100644 index 0000000..255fb94 --- /dev/null +++ b/tests/test_auth_privilege_boundary.c @@ -0,0 +1,105 @@ +#include +#include + +#include "qihse_auth.h" + +int main(void) { + qihse_auth_init(); + + qihse_user_t* operator_user = qihse_auth_get_user(0); + assert(operator_user != NULL); + + /* User creation is intentionally blocked until the seeded operator password is rotated. */ + assert(qihse_auth_modify_user(operator_user, 0, NULL, "SecureOpPass1!", -1, -1)); + + qihse_user_t* analyst = qihse_auth_create_user( + operator_user, 1, QIHSE_ROLE_ANALYST, 5, 0x0003, + "AnalystPass123!", false); + assert(analyst != NULL); + + /* Delegate account creation without delegating Operator authority. */ + assert(qihse_auth_modify_user(operator_user, 1, NULL, NULL, -1, 1)); + assert(analyst->can_create_users); + + /* Delegated Analyst cannot mint an Operator. */ + assert(qihse_auth_create_user( + analyst, 2, QIHSE_ROLE_OPERATOR, 5, 0x0001, + "EscalatePass123!", false) == NULL); + + /* Same-level/subordinate creation remains valid for permitted subsets. */ + qihse_user_t* peer_analyst = qihse_auth_create_user( + analyst, 3, QIHSE_ROLE_ANALYST, 5, 0x0001, + "PeerAnalyst123!", false); + assert(peer_analyst != NULL); + + qihse_user_t* guest = qihse_auth_create_user( + operator_user, 4, QIHSE_ROLE_GUEST, 0, 0, + "GuestAccount123!", false); + assert(guest != NULL); + assert(qihse_auth_modify_user(operator_user, 4, NULL, NULL, -1, 1)); + + /* Delegated Guest cannot create an Analyst. */ + assert(qihse_auth_create_user( + guest, 5, QIHSE_ROLE_ANALYST, 0, 0, + "GuestEscalate123!", false) == NULL); + + /* Mutating the public user object must not alter canonical authority. */ + uint16_t saved_role = analyst->role; + uint16_t saved_classification = analyst->classification_level; + analyst->role = QIHSE_ROLE_OPERATOR; + analyst->classification_level = 0xFFFF; + assert(qihse_auth_create_user( + analyst, 6, QIHSE_ROLE_OPERATOR, 5, 0x0001, + "MutatedRole123!", false) == NULL); + assert(qihse_auth_create_user( + analyst, 7, QIHSE_ROLE_ANALYST, 6, 0x0001, + "MutatedClass123!", false) == NULL); + assert(!qihse_auth_modify_user(analyst, 3, NULL, NULL, -1, 1)); + analyst->role = saved_role; + analyst->classification_level = saved_classification; + + /* A forged stack copy is not an authenticated repository principal. */ + qihse_user_t forged_operator = *analyst; + forged_operator.user_id = 0; + forged_operator.role = QIHSE_ROLE_OPERATOR; + forged_operator.classification_level = 0xFFFF; + forged_operator.sci_compartments = 0xFFFF; + forged_operator.can_create_users = true; + assert(qihse_auth_create_user( + &forged_operator, 8, QIHSE_ROLE_OPERATOR, 0xFFFF, 0xFFFF, + "ForgedCreator123!", false) == NULL); + assert(!qihse_auth_modify_user(&forged_operator, 3, NULL, NULL, -1, 1)); + + /* Flipping can_create_users directly must not create delegated authority. */ + assert(!peer_analyst->can_create_users); + peer_analyst->can_create_users = true; + assert(qihse_auth_create_user( + peer_analyst, 9, QIHSE_ROLE_GUEST, 0, 0, + "ForgedDelegate123!", false) == NULL); + peer_analyst->can_create_users = false; + + /* A delegated user with mandatory hardware auth cannot create a child + * that weakens that requirement, even if its public flag is tampered. */ + qihse_user_t* token_analyst = qihse_auth_create_user( + operator_user, 10, QIHSE_ROLE_ANALYST, 5, 0x0003, + "TokenAnalyst123!", true); + assert(token_analyst != NULL); + assert(qihse_auth_modify_user(operator_user, 10, NULL, NULL, -1, 1)); + token_analyst->requires_hardware_token = false; + assert(qihse_auth_create_user( + token_analyst, 11, QIHSE_ROLE_ANALYST, 5, 0x0001, + "WeakTokenChild123!", false) == NULL); + token_analyst->requires_hardware_token = true; + assert(qihse_auth_create_user( + token_analyst, 12, QIHSE_ROLE_ANALYST, 5, 0x0001, + "TokenChildPass123!", true) != NULL); + + /* NULL must never become SHA384("") and create an empty-password account. */ + assert(qihse_auth_create_user( + analyst, 13, QIHSE_ROLE_ANALYST, 5, 0x0001, + NULL, false) == NULL); + assert(qihse_auth_authenticate_id(13, "") == NULL); + + puts("auth privilege-boundary regression tests passed"); + return 0; +}