From 29b0b9e00721020c3f974bdde6a81fd7de872c84 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 28 Jul 2026 07:21:43 -0300 Subject: [PATCH 1/6] autls: add per-identity PSK support to ACL Extend the ACL data structures and parser to support per-identity pre-shared keys. Each ACL entry can now carry its own key material loaded from a file specified via a key=/path column. Add psk_key, psk_key_len, and key_file fields to autls_acl_entry, and a has_per_identity_keys flag to autls_acl_table. The parser recognizes key= only when followed by '/' (absolute path), preserving backward compatibility with existing ACL files that use free-form notes after the status field. When any entry has a key= path, all entries must have one (all-or-nothing enforcement prevents mixed single-PSK/per-identity configurations). Duplicate key file paths and duplicate key content across entries are both rejected to enforce the identity-key binding invariant. Add autls_acl_lookup() which returns the full entry for callers that need access to key material. Rewrite autls_acl_check() to delegate to autls_acl_lookup(), preserving the existing boolean API. Extract acl_entry_free() to cleanse per-identity key material via OPENSSL_cleanse on all free paths, including partial-construction error paths during parsing. This commit adds the library support; the server-side callback changes that use per-identity keys follow in the next commit. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- autls/autls-acl.c | 160 +++++++++++++++++++++++++++++++++++++++++----- autls/autls.h | 8 +++ 2 files changed, 152 insertions(+), 16 deletions(-) diff --git a/autls/autls-acl.c b/autls/autls-acl.c index 9836f8f04..8958bdbcf 100644 --- a/autls/autls-acl.c +++ b/autls/autls-acl.c @@ -30,6 +30,21 @@ #include #include "autls.h" +/* + * acl_entry_free - free a single ACL entry and cleanse key material + * @e: entry to free, must not be NULL + */ +static void acl_entry_free(struct autls_acl_entry *e) +{ + if (e->psk_key) { + OPENSSL_cleanse(e->psk_key, e->psk_key_len); + OPENSSL_free(e->psk_key); + } + free(e->key_file); + free(e->identity); + free(e); +} + /* * autls_acl_load - parse a TLS client authorization file * @path: path to the ACL file @@ -41,10 +56,18 @@ * host-1234 enabled prod web host * host-5678 disabled retired * + * Per-identity key mode adds a key= column with an absolute path: + * host-1234 enabled key=/etc/audit/psk/host-1234.key + * host-5678 disabled key=/etc/audit/psk/host-5678.key + * + * The key= prefix is recognized only when followed by '/' to avoid + * collisions with free-form notes. When any entry has a key= path, + * all entries must have one (no mixing). + * * Blank lines and lines starting with # are ignored. * Status must be "enabled" or "disabled" (case-insensitive). * Identity is validated via autls_validate_psk_identity(). - * Duplicate identities are rejected. + * Duplicate identities and duplicate key file paths are rejected. * File must be root-owned, not group-writable, not world-writable, * and a regular file. Opens with O_NOFOLLOW to reject symlinks and * O_NONBLOCK so a FIFO cannot block before the regular-file check. @@ -61,6 +84,7 @@ int autls_acl_load(const char *path, struct autls_acl_table **table, struct autls_acl_entry *tail = NULL; char line[512]; int lineno = 0; + int key_count = 0; int flags; *table = NULL; @@ -123,9 +147,9 @@ int autls_acl_load(const char *path, struct autls_acl_table **table, } while (fgets(line, sizeof(line), f) != NULL) { - char *identity, *status, *saveptr; + char *identity, *status, *extra, *saveptr; struct autls_acl_entry *entry, *dup; - size_t len; + size_t len, id_len; lineno++; @@ -173,9 +197,9 @@ int autls_acl_load(const char *path, struct autls_acl_table **table, goto err; } - size_t id_len = strlen(identity); + id_len = strlen(identity); - /* Check for duplicates */ + /* Check for duplicate identities */ for (dup = t->entries; dup; dup = dup->next) { if (dup->identity_len == id_len && memcmp(dup->identity, identity, id_len) == 0) { @@ -208,11 +232,60 @@ int autls_acl_load(const char *path, struct autls_acl_table **table, "%s:%d: invalid status '%s'; " "must be 'enabled' or 'disabled'", path, lineno, status); - free(entry->identity); - free(entry); + acl_entry_free(entry); goto err; } + /* Check for optional per-identity key path */ + extra = strtok_r(NULL, " \t", &saveptr); + if (extra && strncmp(extra, "key=/", 5) == 0) { + const char *key_path = extra + 4; + char *trailing; + + /* Reject trailing tokens after key= path */ + trailing = strtok_r(NULL, " \t", &saveptr); + if (trailing) { + log_fn(LOG_ERR, + "%s:%d: unexpected token after " + "key= path", path, lineno); + acl_entry_free(entry); + goto err; + } + + /* Check for duplicate key file paths */ + for (dup = t->entries; dup; dup = dup->next) { + if (dup->key_file && + strcmp(dup->key_file, key_path) == 0) { + log_fn(LOG_ERR, + "%s:%d: duplicate key file " + "path '%s'", + path, lineno, key_path); + acl_entry_free(entry); + goto err; + } + } + + entry->key_file = strdup(key_path); + if (entry->key_file == NULL) { + log_fn(LOG_ERR, + "Out of memory for key file path"); + acl_entry_free(entry); + goto err; + } + + if (autls_load_psk(key_path, &entry->psk_key, + &entry->psk_key_len, + log_fn) != 0) { + log_fn(LOG_ERR, + "%s:%d: failed to load key file " + "'%s'", path, lineno, key_path); + acl_entry_free(entry); + goto err; + } + + key_count++; + } + /* Append to list */ if (tail) tail->next = entry; @@ -229,6 +302,36 @@ int autls_acl_load(const char *path, struct autls_acl_table **table, goto err; } + /* Per-identity keys: all-or-nothing consistency check */ + if (key_count > 0 && key_count != t->count) { + log_fn(LOG_ERR, + "%s: %d of %d entries have per-identity keys; " + "all entries must have key= or none", + path, key_count, t->count); + goto err; + } + if (key_count > 0) { + struct autls_acl_entry *a, *b; + + /* Reject duplicate key content across entries */ + for (a = t->entries; a != NULL; a = a->next) { + for (b = a->next; b != NULL; b = b->next) { + if (a->psk_key_len == b->psk_key_len && + CRYPTO_memcmp(a->psk_key, b->psk_key, + a->psk_key_len) == 0) { + log_fn(LOG_ERR, + "%s: identities '%s' and " + "'%s' have identical key " + "material", + path, a->identity, + b->identity); + goto err; + } + } + } + t->has_per_identity_keys = 1; + } + fclose(f); *table = t; return 0; @@ -240,30 +343,56 @@ int autls_acl_load(const char *path, struct autls_acl_table **table, } /* - * autls_acl_check - look up an identity in the ACL table + * autls_acl_lookup - look up an identity in the ACL table * @table: parsed ACL table * @identity: identity bytes to look up * @len: length of @identity in bytes * - * Returns 1 if the identity is found and enabled, - * 0 if found but disabled, -1 if not found. + * Returns a pointer to the matching entry, or NULL if not found. + * Uses CRYPTO_memcmp for individual comparisons to prevent + * per-byte timing leaks; the overall lookup is not fully + * constant-time (early return on match, length pre-check), + * which is acceptable because PSK identities are sent in + * cleartext in TLS 1.3. */ -int autls_acl_check(const struct autls_acl_table *table, - const unsigned char *identity, size_t len) +const struct autls_acl_entry *autls_acl_lookup( + const struct autls_acl_table *table, + const unsigned char *identity, size_t len) { const struct autls_acl_entry *e; for (e = table->entries; e != NULL; e = e->next) { if (e->identity_len == len && CRYPTO_memcmp(e->identity, identity, len) == 0) - return e->enabled ? 1 : 0; + return e; } - return -1; + return NULL; +} + +/* + * autls_acl_check - look up an identity and return its status + * @table: parsed ACL table + * @identity: identity bytes to look up + * @len: length of @identity in bytes + * + * Returns 1 if the identity is found and enabled, + * 0 if found but disabled, -1 if not found. + */ +int autls_acl_check(const struct autls_acl_table *table, + const unsigned char *identity, size_t len) +{ + const struct autls_acl_entry *e = autls_acl_lookup(table, + identity, len); + if (e == NULL) + return -1; + return e->enabled ? 1 : 0; } /* * autls_acl_free - free an ACL table and all its entries * @table: table to free, may be NULL + * + * Cleanses per-identity key material before freeing. */ void autls_acl_free(struct autls_acl_table *table) { @@ -274,8 +403,7 @@ void autls_acl_free(struct autls_acl_table *table) for (e = table->entries; e != NULL; e = next) { next = e->next; - free(e->identity); - free(e); + acl_entry_free(e); } free(table); } diff --git a/autls/autls.h b/autls/autls.h index 34d31898b..361605bc9 100644 --- a/autls/autls.h +++ b/autls/autls.h @@ -87,6 +87,9 @@ struct autls_acl_entry { char *identity; size_t identity_len; int enabled; + unsigned char *psk_key; + size_t psk_key_len; + char *key_file; struct autls_acl_entry *next; }; @@ -94,11 +97,16 @@ struct autls_acl_table { struct autls_acl_entry *entries; int count; int enabled_count; + int has_per_identity_keys; }; int autls_acl_load(const char *path, struct autls_acl_table **table, autls_log_fn log_fn) __nonnull((1, 2, 3)) __wur; +const struct autls_acl_entry *autls_acl_lookup( + const struct autls_acl_table *table, + const unsigned char *identity, size_t len) + __nonnull((1, 2)) __wur; int autls_acl_check(const struct autls_acl_table *table, const unsigned char *identity, size_t len) __nonnull((1, 2)) __wur; From e0224d7be5976a3030adcab246b10ead6aeca390 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 28 Jul 2026 07:24:54 -0300 Subject: [PATCH 2/6] auditd: bind PSK key selection to ACL identity Restructure the TLS server initialization and PSK callback to select per-identity key material when available, eliminating the identity confusion vulnerability where a disabled collector could authenticate as the enabled identity using the shared global PSK. Restructure init_tls_server_context() from a single monolithic if(tls_psk_file) block into four independent steps: global PSK loading, expected identity setup, ACL loading, and callback registration. This decoupling allows per-identity key mode to operate without a global tls_psk_file. In tls_psk_find_session_cb(), replace autls_acl_check() with autls_acl_lookup() and add mode-aware key selection. In per-identity mode, always use the entry's own key material and hard-reject if it is missing (no silent fallback to the global key). In single-PSK mode, continue using server_psk_key. In reload_tls_client_acl(), add guards for per-identity-only mode ACL removal, reject mode transitions between single-PSK and per-identity on SIGHUP, enforce enabled_count using the new ACL's has_per_identity_keys flag, and evict connected clients whose identity is disabled or absent in the reloaded ACL. Client eviction follows the periodic_handler pattern: save next pointer, stop the ev_io watcher, then release and free. Add a post-handshake re-check in tls_handshake_handler() against the current ACL before admitting a client, closing the window where a SIGHUP swaps the ACL between the PSK callback and handshake completion. Warn at init and reload when the ACL has zero enabled identities. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/auditd-listen.c | 237 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 186 insertions(+), 51 deletions(-) diff --git a/src/auditd-listen.c b/src/auditd-listen.c index 61abda843..4deef11a6 100644 --- a/src/auditd-listen.c +++ b/src/auditd-listen.c @@ -720,7 +720,9 @@ static void abort_handshake(struct ev_loop *loop, } emit_tls_audit_record(&client->addr, client->ssl, - client->tls_profile_at_accept, ex_identity, + client->tls_profile_at_accept, + ex_identity ? ex_identity : + client->accepted_identity, ex_reason ? ex_reason : op, "no"); /* * Do not emit AUDIT_CRYPTO_SESSION for failed collector handshakes. @@ -1977,6 +1979,24 @@ static void tls_handshake_handler(struct ev_loop *loop, ssl_ex_idx_identity, NULL); } + /* Re-check against current ACL -- the table may have been + * swapped by a SIGHUP between the PSK callback and now. */ + if (acl_table && client->accepted_identity) { + int rc = autls_acl_check(acl_table, + (const unsigned char *) + client->accepted_identity, + strlen(client->accepted_identity)); + if (rc != 1) { + audit_msg(LOG_NOTICE, + "TLS identity '%.128s' revoked " + "by ACL reload during handshake", + client->accepted_identity); + abort_handshake(loop, client, + "identity-revoked"); + return; + } + } + if (!tls_client_authenticated(client)) { audit_msg(LOG_ERR, "TLS handshake from %s completed " @@ -2512,10 +2532,18 @@ static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, { SSL_SESSION *s; const SSL_CIPHER *cipher; + const unsigned char *selected_key = NULL; + size_t selected_key_len = 0; char safe_id[65]; - if (server_psk_key == NULL) + if (server_psk_key == NULL && + (acl_table == NULL || !acl_table->has_per_identity_keys)) { + audit_msg(LOG_CRIT, + "TLS PSK callback invoked with no key " + "material (internal state error)"); + set_psk_failure_reason(ssl, "no-key-material"); return 0; + } /* Validate identity syntax before any authorization check */ if (autls_validate_psk_identity(identity, identity_len, @@ -2532,9 +2560,10 @@ static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, /* Authorization: ACL table supersedes single identity */ if (acl_table) { - int rc = autls_acl_check(acl_table, identity, + const struct autls_acl_entry *entry = + autls_acl_lookup(acl_table, identity, identity_len); - if (rc < 0) { + if (entry == NULL) { sanitize_identity(identity, identity_len, safe_id, sizeof(safe_id)); audit_msg(LOG_ERR, @@ -2544,7 +2573,7 @@ static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, "unknown-identity"); return 0; } - if (rc == 0) { + if (!entry->enabled) { sanitize_identity(identity, identity_len, safe_id, sizeof(safe_id)); audit_msg(LOG_ERR, @@ -2554,6 +2583,25 @@ static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, "disabled-identity"); return 0; } + + /* Mode-aware key selection */ + if (acl_table->has_per_identity_keys) { + if (entry->psk_key == NULL || + entry->psk_key_len == 0) { + audit_msg(LOG_CRIT, + "TLS PSK per-identity key " + "missing (loader " + "inconsistency)"); + set_psk_failure_reason(ssl, + "missing-per-identity-key"); + return 0; + } + selected_key = entry->psk_key; + selected_key_len = entry->psk_key_len; + } else { + selected_key = server_psk_key; + selected_key_len = server_psk_key_len; + } } else if (expected_psk_identity) { if (identity_len != strlen(expected_psk_identity) || CRYPTO_memcmp(identity, expected_psk_identity, @@ -2569,6 +2617,8 @@ static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, "unknown-identity"); return 0; } + selected_key = server_psk_key; + selected_key_len = server_psk_key_len; } else { /* No authorization configured -- fail closed */ audit_msg(LOG_ERR, @@ -2594,8 +2644,11 @@ static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, return 0; } - if (!SSL_SESSION_set1_master_key(s, server_psk_key, - server_psk_key_len) || + /* set1 copies key material, so the ACL entry's psk_key can + * be safely freed by a SIGHUP reload while OpenSSL still + * uses this session for binder verification. */ + if (!SSL_SESSION_set1_master_key(s, selected_key, + selected_key_len) || !SSL_SESSION_set_cipher(s, cipher) || !SSL_SESSION_set_protocol_version(s, TLS1_3_VERSION)) { audit_msg(LOG_ERR, @@ -2743,58 +2796,69 @@ static int init_tls_server_context(struct daemon_conf *config) } } - /* PSK mode */ + /* Step 1: Load global PSK if configured */ if (config->tls_psk_file) { if (autls_load_psk(config->tls_psk_file, &server_psk_key, &server_psk_key_len, audit_msg) != 0) goto err; - SSL_CTX_set_psk_find_session_callback(tls_server_ctx, - tls_psk_find_session_cb); - free(expected_psk_identity); - expected_psk_identity = NULL; - if (config->tls_psk_identity) { - if (autls_validate_psk_identity( - (const unsigned char *) - config->tls_psk_identity, - strlen(config->tls_psk_identity), - audit_msg) != 0) - goto err; - expected_psk_identity = - strdup(config->tls_psk_identity); - if (!expected_psk_identity) { - audit_msg(LOG_ERR, - "Out of memory for PSK identity"); - goto err; - } + } + + /* Step 2: Load expected identity for non-ACL mode */ + free(expected_psk_identity); + expected_psk_identity = NULL; + if (config->tls_psk_identity) { + if (autls_validate_psk_identity( + (const unsigned char *) + config->tls_psk_identity, + strlen(config->tls_psk_identity), + audit_msg) != 0) + goto err; + expected_psk_identity = + strdup(config->tls_psk_identity); + if (!expected_psk_identity) { + audit_msg(LOG_ERR, + "Out of memory for PSK identity"); + goto err; } + } - /* Load client ACL if configured */ - if (config->tls_allowed_clients) { - if (acl_table) { - autls_acl_free(acl_table); - acl_table = NULL; - } - if (autls_acl_load(config->tls_allowed_clients, - &acl_table, audit_msg) != 0) - goto err; - if (acl_table->enabled_count > 1) { - audit_msg(LOG_ERR, - "tls_allowed_clients has %d " - "enabled identities but " - "single-PSK mode allows at " - "most 1", - acl_table->enabled_count); - goto err; - } - if (expected_psk_identity) - audit_msg(LOG_NOTICE, - "tls_allowed_clients is " - "configured; tls_psk_identity " - "is ignored for authorization"); + /* Step 3: Load client ACL (independent of tls_psk_file) */ + if (config->tls_allowed_clients) { + if (acl_table) { + autls_acl_free(acl_table); + acl_table = NULL; + } + if (autls_acl_load(config->tls_allowed_clients, + &acl_table, audit_msg) != 0) + goto err; + if (acl_table->enabled_count > 1 && + !acl_table->has_per_identity_keys) { + audit_msg(LOG_ERR, + "tls_allowed_clients has %d " + "enabled identities but " + "single-PSK mode allows at " + "most 1", + acl_table->enabled_count); + goto err; } + if (acl_table->enabled_count == 0) + audit_msg(LOG_WARNING, + "tls_allowed_clients has no enabled " + "identities; all TLS connections " + "will be rejected"); + if (expected_psk_identity) + audit_msg(LOG_NOTICE, + "tls_allowed_clients is " + "configured; tls_psk_identity " + "is ignored for authorization"); + } - /* Register ex-data indices (once) */ + /* Step 4: Register PSK callback if any key material exists */ + if (server_psk_key || + (acl_table && acl_table->has_per_identity_keys)) { + SSL_CTX_set_psk_find_session_callback(tls_server_ctx, + tls_psk_find_session_cb); if (ssl_ex_idx_identity < 0) { ssl_ex_idx_identity = SSL_get_ex_new_index(0, NULL, @@ -2810,6 +2874,13 @@ static int init_tls_server_context(struct daemon_conf *config) goto err; } } + } else if (config->tls_psk_file || + config->tls_allowed_clients) { + audit_msg(LOG_ERR, + "No PSK key material available (need " + "tls_psk_file or per-identity keys in " + "tls_allowed_clients)"); + goto err; } return 0; @@ -3099,6 +3170,14 @@ static void reload_tls_client_acl(const struct daemon_conf *nconf, if (oconf->tls_allowed_clients == NULL) return; + /* Per-identity mode has no fallback */ + if (acl_table && acl_table->has_per_identity_keys) { + audit_msg(LOG_ERR, + "tls_allowed_clients removal ignored; " + "per-identity key mode has no fallback"); + return; + } + if (server_psk_key && expected_psk_identity == NULL) { audit_msg(LOG_ERR, "tls_allowed_clients removal ignored; " @@ -3128,7 +3207,20 @@ static void reload_tls_client_acl(const struct daemon_conf *nconf, return; } - if (server_psk_key && new_acl->enabled_count > 1) { + /* Reject mode transition (single-PSK <-> per-identity) */ + if (acl_table && + new_acl->has_per_identity_keys != + acl_table->has_per_identity_keys) { + audit_msg(LOG_ERR, + "ACL mode change (single-PSK <-> per-identity) " + "requires restart; keeping current ACL"); + autls_acl_free(new_acl); + free((void *)nconf->tls_allowed_clients); + return; + } + + if (new_acl->enabled_count > 1 && + !new_acl->has_per_identity_keys) { audit_msg(LOG_ERR, "Reloaded ACL has %d enabled identities but " "single-PSK mode allows at most 1; " @@ -3138,14 +3230,57 @@ static void reload_tls_client_acl(const struct daemon_conf *nconf, return; } + /* Reject single-PSK ACL with no global key */ + if (!new_acl->has_per_identity_keys && server_psk_key == NULL) { + audit_msg(LOG_ERR, + "Reloaded ACL has no per-identity keys and " + "no tls_psk_file is configured; " + "keeping current ACL"); + autls_acl_free(new_acl); + free((void *)nconf->tls_allowed_clients); + return; + } + old_enabled = acl_table ? acl_table->enabled_count : 0; autls_acl_free(acl_table); acl_table = new_acl; free((void *)oconf->tls_allowed_clients); oconf->tls_allowed_clients = nconf->tls_allowed_clients; + + if (new_acl->enabled_count == 0) + audit_msg(LOG_WARNING, + "Reloaded ACL has no enabled identities; " + "all new TLS connections will be rejected"); + audit_msg(LOG_NOTICE, "TLS client ACL reloaded (%d->%d enabled)", old_enabled, new_acl->enabled_count); + + /* Evict connected clients whose identity is now disabled */ + { + struct ev_loop *loop = ev_default_loop(EVFLAG_AUTO); + struct ev_tcp *ev, *next; + + for (ev = client_chain; ev; ev = next) { + next = ev->next; + if (ev->accepted_identity) { + int rc = autls_acl_check(acl_table, + (const unsigned char *) + ev->accepted_identity, + strlen(ev->accepted_identity)); + if (rc != 1) { + audit_msg(LOG_NOTICE, + "Evicting TLS client " + "'%.128s': identity " + "disabled or removed", + ev->accepted_identity); + ev_io_stop(loop, &ev->io); + release_client(ev); + free(ev); + } + } + } + } } #endif From 49b775b411deb2f74c91e24eed55ee15b97ec358 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 28 Jul 2026 07:25:53 -0300 Subject: [PATCH 3/6] auditd: allow tls_psk_file to be optional with per-identity keys Relax the configuration validation to allow tls_allowed_clients as an alternative to tls_psk_file. When per-identity keys are specified via key= entries in the ACL file, a global tls_psk_file is unnecessary. The actual validation that per-identity key material exists happens in init_tls_server_context() after ACL loading. A diagnostic notice is logged when tls_psk_file is absent to guide operators. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- src/auditd-config.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/auditd-config.c b/src/auditd-config.c index 4c3bff842..1f9589f04 100644 --- a/src/auditd-config.c +++ b/src/auditd-config.c @@ -2243,18 +2243,27 @@ static int sanity_check(struct daemon_conf *config) } #ifdef HAVE_TLS if (config->transport == T_TLS) { - if (config->tls_psk_file == NULL) { + if (!config->tls_psk_file && + !config->tls_allowed_clients) { audit_msg(LOG_ERR, - "transport=tls requires tls_psk_file"); + "tls_psk_file or tls_allowed_clients " + "is required for TLS transport"); return 1; } - if (!config->tls_psk_identity && + if (config->tls_psk_file && + !config->tls_psk_identity && !config->tls_allowed_clients) { audit_msg(LOG_ERR, "tls_psk_identity or tls_allowed_clients " "is required when tls_psk_file is set"); return 1; } + if (!config->tls_psk_file && + config->tls_allowed_clients) + audit_msg(LOG_NOTICE, + "tls_psk_file not set; " + "tls_allowed_clients must contain " + "per-identity key= entries"); #ifndef HAVE_SSL_GROUP_TO_NAME if (config->tls_require_pqc || config->tls_crypto_profile == TLS_PROFILE_PQC) { From 0b08758a33d5278992d8c5325650d7a640c293bd Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 28 Jul 2026 07:27:06 -0300 Subject: [PATCH 4/6] docs: document per-identity PSK mode in auditd.conf Document the key= column for per-identity key file paths, permission requirements, the distinction between single-PSK and per-identity mode, SIGHUP client eviction behavior, and the operational requirement to provision unique per-identity keys. Note that disabling an identity in single-PSK mode does not revoke a client that still possesses the shared key, and that switching between modes requires a daemon restart. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- docs/auditd.conf.5 | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/auditd.conf.5 b/docs/auditd.conf.5 index f56aec093..203aad160 100644 --- a/docs/auditd.conf.5 +++ b/docs/auditd.conf.5 @@ -370,21 +370,52 @@ requires post-quantum hybrid key exchange and rejects connections that negotiate .IR compatible . .TP .I tls_psk_file -Path to a file containing a hex-encoded pre-shared key for TLS-PSK authentication. The key must be at least 32 bytes (64 hex characters). Generate a key with: openssl rand \-hex 32. The file must be owned by root and mode 0400. +Path to a file containing a hex-encoded pre-shared key for TLS-PSK authentication. The key must be at least 32 bytes (64 hex characters). Generate a key with: openssl rand \-hex 32. The file must be owned by root and mode 0400. This option is not required when tls_allowed_clients contains per-identity key entries (see below). .TP .I tls_psk_identity The expected client PSK identity string. This option is required when tls_psk_file is set and tls_allowed_clients is not configured. The server rejects clients that present a different identity. The identity must contain only printable ASCII characters (no spaces or control characters) and must not exceed 255 bytes. .TP .I tls_allowed_clients -Path to a file listing authorized PSK client identities. The file format is one entry per line: identity, status (enabled or disabled), and optional notes separated by whitespace. Lines starting with # are comments. Example: -.nf +Path to a file listing authorized PSK client identities. The file format is one entry per line: identity, status (enabled or disabled), and optional notes separated by whitespace. Lines starting with # are comments. +.PP .RS +.B Single-PSK mode +(backward compatible): all clients share one key from tls_psk_file. At most one identity may be enabled. Disabling an identity does +.B not +revoke a client that still possesses the shared key; the PSK must be rotated. +.nf # identity status notes host-1234 enabled prod web host host-5678 disabled retired +.fi .RE +.PP +.RS +.B Per-identity key mode: +each identity has its own key file specified by a +.B key= +column with an absolute path. Multiple enabled identities are allowed. The +.B key= +prefix is recognized only when followed by +.B / +(absolute path). Each key file must meet the same requirements as tls_psk_file (mode 0400, root-owned, no symlinks, at least 32 bytes). When any entry has +.B key= +, all entries must have one. Per-identity keys must be cryptographically unique across entries; using the same key for two identities defeats the identity binding. +.nf +# identity status key file +host-prod enabled key=/etc/audit/psk/host-prod.key +host-retired disabled key=/etc/audit/psk/host-retired.key .fi -When this option is set, it is the sole authorization source for PSK connections; tls_psk_identity is ignored. The file must be owned by root, not group-writable, and not world-writable. In the current single-PSK model (one tls_psk_file), at most one identity may be enabled. A full per-identity PSK store is planned for a future release. +In per-identity key mode, tls_psk_file is optional. Clients must be provisioned with their own per-identity key file, not a shared key. +.RE +.PP +.RS +When this option is set, it is the sole authorization source for PSK connections; tls_psk_identity is ignored. The file must be owned by root, not group-writable, and not world-writable. Switching between single-PSK and per-identity mode requires a daemon restart. +.RE +.PP +.RS +On SIGHUP ACL reload, connected clients whose identity is disabled or absent in the new ACL are disconnected immediately. AUDIT_DAEMON_CLOSE and AUDIT_CRYPTO_KEY_USER audit records are emitted for each evicted client. +.RE .TP .I tls_cipher_suites Colon-separated list of TLS 1.3 cipher suites. Overrides the profile default. The default is "TLS_AES_128_GCM_SHA256:\:TLS_CHACHA20_POLY1305_SHA256:\:TLS_AES_256_GCM_SHA384". @@ -466,7 +497,7 @@ and .I verify_email . .SH NOTES -Changes to TLS configuration options (tls_psk_file, tls_psk_identity, tls_cipher_suites, tls_key_exchange, tls_require_pqc, tls_auth, tls_crypto_profile) require a full daemon restart to take effect. The tls_allowed_clients ACL file is reloaded on SIGHUP without restarting the daemon. +Changes to TLS configuration options (tls_psk_file, tls_psk_identity, tls_cipher_suites, tls_key_exchange, tls_require_pqc, tls_auth, tls_crypto_profile) require a full daemon restart to take effect. Switching the tls_allowed_clients ACL between single-PSK and per-identity key mode also requires a restart. The tls_allowed_clients ACL file is reloaded on SIGHUP without restarting the daemon; connected clients whose identity is disabled or removed by the reload are disconnected immediately. .PP In a CAPP environment, the audit trail is considered so important that access to system resources must be denied if an audit trail cannot be created. In this environment, it would be suggested that /var/log/audit be on its own partition. This is to ensure that space detection is accurate and that no other process comes along and consumes part of it. .PP From a4d2a3f43e08ed93af5990bfb222b85120718126 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 28 Jul 2026 07:29:04 -0300 Subject: [PATCH 5/6] tests: add per-identity PSK unit tests Add test_autls_acl_per_identity_keys() with 9 test cases covering the new per-identity key ACL functionality: 1. Per-identity key loading with key= paths 2. Mixed format (some key=, some not) rejected 3. Duplicate key file paths rejected 4. Missing key file rejected 5. Partial-load cleanup (ASAN-safe error path) 6. Multiple enabled identities accepted with per-identity keys 7. Backward compat (no key= columns, has_per_identity_keys=0) 8. Notes starting with key= (no /) treated as notes 9. Trailing tokens after key= path rejected Also exercises autls_acl_lookup() return values and verifies that per-identity keys are distinct across entries. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- audisp/plugins/remote/test-tls-helpers.c | 181 +++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/audisp/plugins/remote/test-tls-helpers.c b/audisp/plugins/remote/test-tls-helpers.c index 951877296..d2590c534 100644 --- a/audisp/plugins/remote/test-tls-helpers.c +++ b/audisp/plugins/remote/test-tls-helpers.c @@ -705,6 +705,186 @@ static void test_autls_authorize_psk_identity(void) autls_acl_free(t); } +/* Two distinct 32-byte hex keys for per-identity tests */ +#define KEY_A_HEX \ + "000102030405060708090a0b0c0d0e0f" \ + "101112131415161718191a1b1c1d1e1f\n" +#define KEY_B_HEX \ + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" \ + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef\n" + +/* + * write_key_file - write a PSK key file with mode 0400 + * + * Returns the full path via @path. + */ +static void write_key_file(char *path, size_t pathlen, + const char *name, const char *hex) +{ + snprintf(path, pathlen, "%s/%s", tmpdir, name); + write_file(path, hex); + chmod(path, 0400); +} + +static void test_autls_acl_per_identity_keys(void) +{ + struct autls_acl_table *t = NULL; + const struct autls_acl_entry *e; + char path[512], kpath_a[512], kpath_b[512]; + char acl_line[1024]; + + printf(" autls_acl_load (per-identity keys)...\n"); + + if (getuid() != 0) { + printf(" (skipped, not root)\n"); + return; + } + + write_key_file(kpath_a, sizeof(kpath_a), "key-a.psk", KEY_A_HEX); + write_key_file(kpath_b, sizeof(kpath_b), "key-b.psk", KEY_B_HEX); + + /* 1. Per-identity key loading succeeds */ + snprintf(path, sizeof(path), "%s/acl-pikey", tmpdir); + snprintf(acl_line, sizeof(acl_line), + "host-a enabled key=%s\nhost-b disabled key=%s\n", + kpath_a, kpath_b); + write_file(path, acl_line); + chmod(path, 0600); + + assert(autls_acl_load(path, &t, test_log) == 0); + assert(t != NULL); + assert(t->count == 2); + assert(t->enabled_count == 1); + assert(t->has_per_identity_keys == 1); + + e = autls_acl_lookup(t, (const unsigned char *)"host-a", 6); + assert(e != NULL); + assert(e->enabled == 1); + assert(e->psk_key != NULL); + assert(e->psk_key_len == 32); + assert(e->key_file != NULL); + + e = autls_acl_lookup(t, (const unsigned char *)"host-b", 6); + assert(e != NULL); + assert(e->enabled == 0); + assert(e->psk_key != NULL); + assert(e->psk_key_len == 32); + + /* Keys are distinct */ + e = autls_acl_lookup(t, (const unsigned char *)"host-a", 6); + { + const struct autls_acl_entry *e2 = + autls_acl_lookup(t, + (const unsigned char *)"host-b", 6); + assert(memcmp(e->psk_key, e2->psk_key, 32) != 0); + } + + /* Unknown identity returns NULL */ + assert(autls_acl_lookup(t, + (const unsigned char *)"host-c", 6) == NULL); + + autls_acl_free(t); + t = NULL; + unlink(path); + + /* 2. Mixed format rejected: one with key=, one without */ + snprintf(path, sizeof(path), "%s/acl-mixed", tmpdir); + snprintf(acl_line, sizeof(acl_line), + "host-a enabled key=%s\nhost-b disabled\n", kpath_a); + write_file(path, acl_line); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == -1); + assert(t == NULL); + unlink(path); + + /* 3. Duplicate key paths rejected */ + snprintf(path, sizeof(path), "%s/acl-dupkey", tmpdir); + snprintf(acl_line, sizeof(acl_line), + "host-a enabled key=%s\nhost-b disabled key=%s\n", + kpath_a, kpath_a); + write_file(path, acl_line); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == -1); + assert(t == NULL); + unlink(path); + + /* 4. Missing key file rejected */ + snprintf(path, sizeof(path), "%s/acl-nokey", tmpdir); + write_file(path, "host-a enabled key=/nonexistent/key.psk\n"); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == -1); + assert(t == NULL); + unlink(path); + + /* 5. Partial-load cleanup: 2nd entry has bad key */ + snprintf(path, sizeof(path), "%s/acl-partial", tmpdir); + snprintf(acl_line, sizeof(acl_line), + "host-a enabled key=%s\n" + "host-b disabled key=/nonexistent/key.psk\n", + kpath_a); + write_file(path, acl_line); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == -1); + assert(t == NULL); + unlink(path); + + /* 6. Multiple enabled with per-identity keys accepted */ + snprintf(path, sizeof(path), "%s/acl-multi", tmpdir); + snprintf(acl_line, sizeof(acl_line), + "host-a enabled key=%s\nhost-b enabled key=%s\n", + kpath_a, kpath_b); + write_file(path, acl_line); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == 0); + assert(t->enabled_count == 2); + assert(t->has_per_identity_keys == 1); + autls_acl_free(t); + t = NULL; + unlink(path); + + /* 7. Backward compat: no key= columns */ + snprintf(path, sizeof(path), "%s/acl-compat", tmpdir); + write_file(path, "host-a enabled notes here\n" + "host-b disabled retired\n"); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == 0); + assert(t->has_per_identity_keys == 0); + e = autls_acl_lookup(t, (const unsigned char *)"host-a", 6); + assert(e != NULL); + assert(e->psk_key == NULL); + assert(e->key_file == NULL); + autls_acl_free(t); + t = NULL; + unlink(path); + + /* 8. Notes starting with key= but no / are treated as notes */ + snprintf(path, sizeof(path), "%s/acl-notes", tmpdir); + write_file(path, "host-a enabled key=rotation-needed\n" + "host-b disabled key=decommissioned\n"); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == 0); + assert(t->has_per_identity_keys == 0); + e = autls_acl_lookup(t, (const unsigned char *)"host-a", 6); + assert(e != NULL); + assert(e->psk_key == NULL); + autls_acl_free(t); + t = NULL; + unlink(path); + + /* 9. Trailing tokens after key= rejected */ + snprintf(path, sizeof(path), "%s/acl-trailing", tmpdir); + snprintf(acl_line, sizeof(acl_line), + "host-a enabled key=%s extra-notes\n", kpath_a); + write_file(path, acl_line); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == -1); + assert(t == NULL); + unlink(path); + + unlink(kpath_a); + unlink(kpath_b); +} + int main(void) { char template[] = "/tmp/test-tls-XXXXXX"; @@ -731,6 +911,7 @@ int main(void) test_autls_acl_load(); test_autls_acl_check(); test_autls_authorize_psk_identity(); + test_autls_acl_per_identity_keys(); printf("All TLS helper tests passed.\n"); return 0; } From 62a1615ac205bb505f55bc654db02cd4492442c8 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Tue, 28 Jul 2026 07:56:48 -0300 Subject: [PATCH 6/6] tests: add TLS binder cross-identity regression test Add the essential regression test for the per-identity PSK fix: verify that cross-pairing an identity with a different identity's key fails at the TLS binder level, not just at the ACL label level. Uses in-memory BIO pairs with psk_ke mode (no certificate, no DHE) to exercise actual TLS 1.3 external-PSK handshakes through OpenSSL. The server callback selects per-identity keys via autls_acl_lookup, mirroring the production tls_psk_find_session_cb key selection logic. Five test cases: 1. Correct identity+key pairing succeeds 2. Cross-identity key pairing fails (the essential regression) 3. Disabled identity with own key is rejected by ACL 4. Unknown identity is rejected 5. Global key does not match per-identity entry Without this test, a regression in the PSK callback that falls back to a shared key would pass all unit tests while leaving the identity confusion vulnerability exploitable. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- audisp/plugins/remote/test-tls-helpers.c | 366 +++++++++++++++++++++++ 1 file changed, 366 insertions(+) diff --git a/audisp/plugins/remote/test-tls-helpers.c b/audisp/plugins/remote/test-tls-helpers.c index d2590c534..3a3374871 100644 --- a/audisp/plugins/remote/test-tls-helpers.c +++ b/audisp/plugins/remote/test-tls-helpers.c @@ -25,6 +25,9 @@ #include "autls.h" #ifdef HAVE_TLS +#include +#include +#include static char tmpdir[256]; @@ -712,6 +715,10 @@ static void test_autls_authorize_psk_identity(void) #define KEY_B_HEX \ "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" \ "e0e1e2e3e4e5e6e7e8e9eaebecedeeef\n" +/* A third key used as a fake global PSK for fallback tests */ +#define KEY_GLOBAL_HEX \ + "aabbccddeeff00112233445566778899" \ + "aabbccddeeff00112233445566778899\n" /* * write_key_file - write a PSK key file with mode 0400 @@ -885,6 +892,364 @@ static void test_autls_acl_per_identity_keys(void) unlink(kpath_b); } +/* + * TLS binder cross-identity regression test. + * + * Uses in-memory BIO pairs to exercise actual TLS 1.3 PSK handshakes + * with per-identity keys, verifying that cross-pairing an identity + * with the wrong key fails at the cryptographic (binder) level. + * + * This is the essential regression test from SPECS/fr-028-001: + * "the disabled identity's old key paired with the active label fails + * binder verification -- a label-only unit test will continue to pass + * while the vulnerability remains." + */ + +/* State for the BIO pair PSK test */ +static struct autls_acl_table *bio_test_acl = NULL; +static const unsigned char *bio_test_client_key = NULL; +static size_t bio_test_client_key_len = 0; +static const char *bio_test_client_identity = NULL; +static SSL_SESSION *bio_test_shared_session = NULL; + +/* + * build_psk_session - create an SSL_SESSION for external PSK + * @ssl: SSL connection (for cipher lookup) + * @key: PSK key bytes + * @key_len: PSK key length + * + * Returns a new SSL_SESSION, or NULL on error. + */ +/* + * find_first_tls13_cipher - return the first TLS 1.3 cipher + * @ssl: SSL connection for cipher list + * + * Returns the first configured TLS 1.3 cipher. Unlike + * autls_find_tls13_cipher which selects by hash, this returns + * whatever OpenSSL defaults to, ensuring client and server agree. + */ +static const SSL_CIPHER *find_first_tls13_cipher(SSL *ssl) +{ + STACK_OF(SSL_CIPHER) *ciphers = SSL_get_ciphers(ssl); + int i; + + for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) { + const SSL_CIPHER *c = sk_SSL_CIPHER_value(ciphers, i); + const char *ver = SSL_CIPHER_get_version(c); + + if (ver && strcmp(ver, "TLSv1.3") == 0) + return c; + } + return NULL; +} + +static SSL_SESSION *build_psk_session(SSL *ssl, const unsigned char *key, + size_t key_len) +{ + const SSL_CIPHER *cipher; + SSL_SESSION *s; + + cipher = find_first_tls13_cipher(ssl); + if (cipher == NULL) + return NULL; + + s = SSL_SESSION_new(); + if (s == NULL) + return NULL; + + if (!SSL_SESSION_set1_master_key(s, key, key_len) || + !SSL_SESSION_set_cipher(s, cipher) || + !SSL_SESSION_set_protocol_version(s, TLS1_3_VERSION)) { + SSL_SESSION_free(s); + return NULL; + } + return s; +} + +/* + * test_server_psk_cb - server PSK callback using per-identity keys + * + * Mirrors the production tls_psk_find_session_cb key selection logic: + * look up the identity in the ACL, select the per-identity key. + */ +static int test_server_psk_cb(SSL *ssl, const unsigned char *identity, + size_t identity_len, SSL_SESSION **sess) +{ + const struct autls_acl_entry *entry; + + if (bio_test_acl == NULL) + return 0; + + entry = autls_acl_lookup(bio_test_acl, identity, identity_len); + if (entry == NULL || !entry->enabled) + return 0; + if (entry->psk_key == NULL || entry->psk_key_len == 0) + return 0; + + { + const SSL_CIPHER *cipher = find_first_tls13_cipher(ssl); + SSL_SESSION *s; + + if (cipher == NULL) + return 0; + s = SSL_SESSION_new(); + if (s == NULL) + return 0; + if (!SSL_SESSION_set1_master_key(s, entry->psk_key, + entry->psk_key_len) || + !SSL_SESSION_set_cipher(s, cipher) || + !SSL_SESSION_set_protocol_version(s, + TLS1_3_VERSION)) { + SSL_SESSION_free(s); + return 0; + } + *sess = s; + } + return 1; +} + +/* + * test_client_psk_cb - client PSK callback presenting a specific + * identity and key pair. Uses a shared pre-built session. + */ +static int test_client_psk_cb(SSL *ssl, const EVP_MD *md, + const unsigned char **id, size_t *idlen, + SSL_SESSION **sess) +{ + (void)ssl; + (void)md; + + if (bio_test_client_key == NULL || bio_test_client_identity == NULL) + return 0; + + if (bio_test_shared_session == NULL) + return 0; + + *id = (const unsigned char *)bio_test_client_identity; + *idlen = strlen(bio_test_client_identity); + SSL_SESSION_up_ref(bio_test_shared_session); + *sess = bio_test_shared_session; + return 1; +} + +/* + * try_bio_pair_handshake - attempt a TLS 1.3 PSK handshake using shared + * memory BIOs (the same pattern OpenSSL's own test suite uses) + * + * Returns 1 if PSK handshake succeeds, 0 if it fails. + */ +static int try_bio_pair_handshake(void) +{ + SSL_CTX *server_ctx = NULL, *client_ctx = NULL; + SSL *server_ssl = NULL, *client_ssl = NULL; + BIO *s_to_c = NULL, *c_to_s = NULL; + int result = 0; + int i; + + server_ctx = SSL_CTX_new(TLS_server_method()); + client_ctx = SSL_CTX_new(TLS_client_method()); + if (!server_ctx || !client_ctx) + goto out; + + SSL_CTX_set_min_proto_version(server_ctx, TLS1_3_VERSION); + SSL_CTX_set_min_proto_version(client_ctx, TLS1_3_VERSION); + SSL_CTX_set_max_early_data(server_ctx, 0); + SSL_CTX_set_max_early_data(client_ctx, 0); + SSL_CTX_set_num_tickets(server_ctx, 0); + + /* + * No certificate. Use SSL_OP_ALLOW_NO_DHE_KEX for psk_ke mode + * (PSK without DHE key exchange). Without a certificate, the + * handshake can only succeed via PSK -- there is no cert fallback. + */ + SSL_CTX_set_options(server_ctx, SSL_OP_ALLOW_NO_DHE_KEX); + SSL_CTX_set_options(client_ctx, SSL_OP_ALLOW_NO_DHE_KEX); + + SSL_CTX_set_psk_find_session_callback(server_ctx, + test_server_psk_cb); + SSL_CTX_set_psk_use_session_callback(client_ctx, + test_client_psk_cb); + + server_ssl = SSL_new(server_ctx); + client_ssl = SSL_new(client_ctx); + if (!server_ssl || !client_ssl) + goto out; + + /* Shared memory BIOs (same pattern as OpenSSL test suite). + * s_to_c: server writes, client reads + * c_to_s: client writes, server reads */ + s_to_c = BIO_new(BIO_s_mem()); + c_to_s = BIO_new(BIO_s_mem()); + if (!s_to_c || !c_to_s) + goto out; + + /* Both SSL objects share these BIOs; bump refcounts */ + BIO_up_ref(s_to_c); + BIO_up_ref(c_to_s); + + SSL_set_bio(server_ssl, c_to_s, s_to_c); + SSL_set_bio(client_ssl, s_to_c, c_to_s); + s_to_c = c_to_s = NULL; /* owned by SSL now */ + + SSL_set_accept_state(server_ssl); + SSL_set_connect_state(client_ssl); + + /* Build the shared PSK session using the client's SSL for + * cipher lookup. The client callback will return this session + * directly; the server callback builds its own from the ACL. */ + if (bio_test_client_key && bio_test_client_key_len > 0) { + bio_test_shared_session = build_psk_session( + client_ssl, bio_test_client_key, + bio_test_client_key_len); + if (bio_test_shared_session == NULL) + goto out; + } + + /* Drive the handshake: alternate client and server */ + for (i = 0; i < 100; i++) { + int client_ret, server_ret; + int client_err, server_err; + + client_ret = SSL_do_handshake(client_ssl); + client_err = SSL_get_error(client_ssl, client_ret); + + server_ret = SSL_do_handshake(server_ssl); + server_err = SSL_get_error(server_ssl, server_ret); + + if (client_ret == 1 && server_ret == 1) { + result = 1; + break; + } + + if (client_err != SSL_ERROR_WANT_READ && + client_err != SSL_ERROR_WANT_WRITE && + client_ret != 1) + break; + if (server_err != SSL_ERROR_WANT_READ && + server_err != SSL_ERROR_WANT_WRITE && + server_ret != 1) + break; + } + +out: + SSL_SESSION_free(bio_test_shared_session); + bio_test_shared_session = NULL; + SSL_free(server_ssl); + SSL_free(client_ssl); + SSL_CTX_free(server_ctx); + SSL_CTX_free(client_ctx); + BIO_free(s_to_c); + BIO_free(c_to_s); + return result; +} + +static void test_tls_binder_cross_identity(void) +{ + struct autls_acl_table *t = NULL; + const struct autls_acl_entry *entry_a, *entry_b; + char path[512], kpath_a[512], kpath_b[512], kpath_g[512]; + char acl_line[1024]; + int ok; + + printf(" TLS binder cross-identity regression...\n"); + + if (getuid() != 0) { + printf(" (skipped, not root)\n"); + return; + } + + write_key_file(kpath_a, sizeof(kpath_a), "bio-key-a.psk", + KEY_A_HEX); + write_key_file(kpath_b, sizeof(kpath_b), "bio-key-b.psk", + KEY_B_HEX); + write_key_file(kpath_g, sizeof(kpath_g), "bio-key-g.psk", + KEY_GLOBAL_HEX); + + /* Load ACL with per-identity keys */ + snprintf(path, sizeof(path), "%s/acl-bio", tmpdir); + snprintf(acl_line, sizeof(acl_line), + "host-a enabled key=%s\nhost-b disabled key=%s\n", + kpath_a, kpath_b); + write_file(path, acl_line); + chmod(path, 0600); + assert(autls_acl_load(path, &t, test_log) == 0); + assert(t->has_per_identity_keys == 1); + + entry_a = autls_acl_lookup(t, + (const unsigned char *)"host-a", 6); + entry_b = autls_acl_lookup(t, + (const unsigned char *)"host-b", 6); + assert(entry_a && entry_b); + + bio_test_acl = t; + + /* Test 1: Correct pairing succeeds -- + * identity host-a with key-A should authenticate */ + bio_test_client_identity = "host-a"; + bio_test_client_key = entry_a->psk_key; + bio_test_client_key_len = entry_a->psk_key_len; + ok = try_bio_pair_handshake(); + assert(ok == 1); + printf(" 1. correct identity+key: PASS (authenticated)\n"); + + /* Test 2: Cross-identity pairing fails -- + * identity host-a with key-B should fail binder. + * THIS IS THE ESSENTIAL REGRESSION TEST. */ + bio_test_client_identity = "host-a"; + bio_test_client_key = entry_b->psk_key; + bio_test_client_key_len = entry_b->psk_key_len; + ok = try_bio_pair_handshake(); + assert(ok == 0); + printf(" 2. cross-identity key: PASS (rejected)\n"); + + /* Test 3: Disabled identity with own key rejected -- + * identity host-b (disabled) with key-B should be + * rejected by ACL before binder verification */ + bio_test_client_identity = "host-b"; + bio_test_client_key = entry_b->psk_key; + bio_test_client_key_len = entry_b->psk_key_len; + ok = try_bio_pair_handshake(); + assert(ok == 0); + printf(" 3. disabled identity+own key: PASS (rejected)\n"); + + /* Test 4: Unknown identity rejected */ + bio_test_client_identity = "host-unknown"; + bio_test_client_key = entry_a->psk_key; + bio_test_client_key_len = entry_a->psk_key_len; + ok = try_bio_pair_handshake(); + assert(ok == 0); + printf(" 4. unknown identity: PASS (rejected)\n"); + + /* Test 5: Global key does not match per-identity entry -- + * Load a "global" key different from both per-identity keys. + * identity host-a with global key should fail binder. */ + { + unsigned char *gkey = NULL; + size_t gkey_len = 0; + + assert(autls_load_psk(kpath_g, &gkey, &gkey_len, + test_log) == 0); + bio_test_client_identity = "host-a"; + bio_test_client_key = gkey; + bio_test_client_key_len = gkey_len; + ok = try_bio_pair_handshake(); + assert(ok == 0); + printf(" 5. global key vs per-identity: " + "PASS (rejected)\n"); + OPENSSL_cleanse(gkey, gkey_len); + OPENSSL_free(gkey); + } + + bio_test_acl = NULL; + bio_test_client_key = NULL; + bio_test_client_identity = NULL; + autls_acl_free(t); + unlink(path); + unlink(kpath_a); + unlink(kpath_b); + unlink(kpath_g); +} + int main(void) { char template[] = "/tmp/test-tls-XXXXXX"; @@ -912,6 +1277,7 @@ int main(void) test_autls_acl_check(); test_autls_authorize_psk_identity(); test_autls_acl_per_identity_keys(); + test_tls_binder_cross_identity(); printf("All TLS helper tests passed.\n"); return 0; }