ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
Crypto Module

🔐 Core cryptographic operations for ascii-chat More...

Files

file  crypto.c
 🔐 Core cryptography: encryption/decryption, key exchange, authentication, and session rekeying with BearSSL
 
file  agent.c
 GPG agent connection and communication implementation.
 
file  agent.h
 GPG agent connection and communication interface.
 
file  export.c
 GPG public key export implementation.
 
file  export.h
 GPG public key export interface.
 
file  gpg.h
 GPG operations - main header.
 
file  signing.c
 GPG signing operations implementation.
 
file  signing.h
 GPG message signing interface.
 
file  verification.c
 GPG signature verification implementation.
 
file  verification.h
 GPG signature verification interface.
 
file  known_hosts.c
 📜 SSH known_hosts file parser for host key verification and trust management
 
file  known_hosts.h
 Known hosts management for MITM attack prevention.
 
file  pem_utils.c
 📄 PEM format encoding/decoding utilities for certificates and keys (adapted from BearSSL)
 
file  pem_utils.h
 BearSSL PEM and trust anchor utilities adapted for in-memory data.
 

Data Structures

struct  crypto_context_t
 Cryptographic context structure. More...
 
struct  anchor_list
 Vector type for trust anchors. More...
 

Macros

#define SERVER_AUTH_RESPONSE_SIZE   AUTH_HMAC_SIZE
 Server authentication response size (32 bytes)
 
#define SSH_KEY_TYPE_LENGTH_SIZE   4
 
#define SSH_KEY_TYPE_STRING_SIZE   11
 
#define SSH_KEY_PUBLIC_KEY_LENGTH_SIZE   4
 
#define SSH_KEY_PUBLIC_KEY_SIZE   32
 
#define SSH_KEY_HEADER_SIZE    (SSH_KEY_TYPE_LENGTH_SIZE + SSH_KEY_TYPE_STRING_SIZE + SSH_KEY_PUBLIC_KEY_LENGTH_SIZE + SSH_KEY_PUBLIC_KEY_SIZE)
 
#define CRYPTO_KEY_SIZE   32
 Ed25519 key size in bytes.
 
#define CRYPTO_FINGERPRINT_SIZE   32
 Ed25519 key fingerprint size in bytes.
 
#define SSH_KEY_PERMISSIONS_MASK   (S_IRWXG | S_IRWXO)
 
#define SSH_KEY_RECOMMENDED_PERMISSIONS   0600
 
#define MAX_COMMENT_LEN   256
 
#define MAX_GPG_KEYGRIP_LEN   64
 

Typedefs

typedef struct crypto_context_t crypto_context_t
 Cryptographic context structure.
 

Enumerations

enum  crypto_result_t {
  CRYPTO_OK = 0 , CRYPTO_ERROR_INIT_FAILED = -1 , CRYPTO_ERROR_INVALID_PARAMS = -2 , CRYPTO_ERROR_MEMORY = -3 ,
  CRYPTO_ERROR_LIBSODIUM = -4 , CRYPTO_ERROR_KEY_GENERATION = -5 , CRYPTO_ERROR_PASSWORD_DERIVATION = -6 , CRYPTO_ERROR_ENCRYPTION = -7 ,
  CRYPTO_ERROR_DECRYPTION = -8 , CRYPTO_ERROR_INVALID_MAC = -9 , CRYPTO_ERROR_BUFFER_TOO_SMALL = -10 , CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE = -11 ,
  CRYPTO_ERROR_NONCE_EXHAUSTED = -12 , CRYPTO_ERROR_REKEY_IN_PROGRESS = -13 , CRYPTO_ERROR_REKEY_FAILED = -14 , CRYPTO_ERROR_REKEY_RATE_LIMITED = -15
}
 Cryptographic operation result codes. More...
 

Variables

br_x509_trust_anchor * anchor_list::buf
 
size_t anchor_list::ptr
 
size_t anchor_list::len
 

Core Initialization and Setup

crypto_result_t crypto_init (crypto_context_t *ctx)
 Initialize libsodium and crypto context.
 
crypto_result_t crypto_init_with_password (crypto_context_t *ctx, const char *password)
 Initialize with password-based encryption.
 
void crypto_cleanup (crypto_context_t *ctx)
 Cleanup crypto context with secure memory wiping.
 
crypto_result_t crypto_generate_keypair (crypto_context_t *ctx)
 Generate new X25519 key pair for key exchange.
 

Key Exchange Protocol

Automatic HTTPS-like key exchange using X25519 Diffie-Hellman. Both parties exchange ephemeral public keys and compute a shared secret.

crypto_result_t crypto_get_public_key (const crypto_context_t *ctx, uint8_t *public_key_out)
 Get public key for sending to peer (step 1 of handshake)
 
crypto_result_t crypto_set_peer_public_key (crypto_context_t *ctx, const uint8_t *peer_public_key)
 Set peer's public key and compute shared secret (step 2 of handshake)
 
bool crypto_is_ready (const crypto_context_t *ctx)
 Check if key exchange is complete and ready for encryption.
 

Password-Based Encryption

Optional additional encryption layer using password-derived keys. Uses Argon2id for memory-hard key derivation, providing resistance to offline brute-force attacks.

crypto_result_t crypto_validate_password (const char *password)
 Validate password length requirements.
 
crypto_result_t crypto_derive_password_key (crypto_context_t *ctx, const char *password)
 Derive key from password using Argon2id.
 
bool crypto_verify_password (const crypto_context_t *ctx, const char *password)
 Verify password matches stored salt/key.
 
crypto_result_t crypto_derive_password_encryption_key (const char *password, uint8_t encryption_key[32])
 Derive deterministic encryption key from password for handshake.
 

Encryption/Decryption Operations

Encrypt/decrypt data using XSalsa20-Poly1305 (via libsodium secretbox). Automatically handles nonce generation and MAC verification.

crypto_result_t crypto_encrypt (crypto_context_t *ctx, const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext_out, size_t ciphertext_out_size, size_t *ciphertext_len_out)
 Encrypt data using XSalsa20-Poly1305.
 
crypto_result_t crypto_decrypt (crypto_context_t *ctx, const uint8_t *ciphertext, size_t ciphertext_len, uint8_t *plaintext_out, size_t plaintext_out_size, size_t *plaintext_len_out)
 Decrypt data using XSalsa20-Poly1305.
 

Utility Functions

const char * crypto_result_to_string (crypto_result_t result)
 Convert crypto result to human-readable string.
 
void crypto_get_status (const crypto_context_t *ctx, char *status_buffer, size_t buffer_size)
 Get crypto context status information for debugging.
 
bool crypto_secure_compare (const uint8_t *lhs, const uint8_t *rhs, size_t len)
 Secure constant-time comparison of byte arrays.
 
crypto_result_t crypto_random_bytes (uint8_t *buffer, size_t len)
 Generate cryptographically secure random bytes.
 

Authentication and Handshake

HMAC-based authentication using HMAC-SHA256. Used for password authentication and challenge-response protocols.

crypto_result_t crypto_generate_nonce (uint8_t nonce[32])
 Generate random nonce for authentication.
 
crypto_result_t crypto_compute_hmac (crypto_context_t *ctx, const uint8_t key[32], const uint8_t data[32], uint8_t hmac[32])
 Compute HMAC-SHA256 for fixed 32-byte data.
 
crypto_result_t crypto_compute_hmac_ex (const crypto_context_t *ctx, const uint8_t key[32], const uint8_t *data, size_t data_len, uint8_t hmac[32])
 Compute HMAC-SHA256 for variable-length data.
 
bool crypto_verify_hmac (const uint8_t key[32], const uint8_t data[32], const uint8_t expected_hmac[32])
 Verify HMAC-SHA256 for fixed 32-byte data.
 
bool crypto_verify_hmac_ex (const uint8_t key[32], const uint8_t *data, size_t data_len, const uint8_t expected_hmac[32])
 Verify HMAC-SHA256 for variable-length data.
 

High-Level Authentication Helpers

Authentication helpers that bind password/key authentication to the DH key exchange, preventing man-in-the-middle attacks.

crypto_result_t crypto_compute_auth_response (const crypto_context_t *ctx, const uint8_t nonce[32], uint8_t hmac_out[32])
 Compute authentication response HMAC bound to DH shared_secret.
 
bool crypto_verify_auth_response (const crypto_context_t *ctx, const uint8_t nonce[32], const uint8_t expected_hmac[32])
 Verify authentication response HMAC bound to DH shared_secret.
 
crypto_result_t crypto_create_auth_challenge (const crypto_context_t *ctx, uint8_t *packet_out, size_t packet_size, size_t *packet_len_out)
 Create authentication challenge packet.
 
crypto_result_t crypto_process_auth_challenge (crypto_context_t *ctx, const uint8_t *packet, size_t packet_len)
 Process authentication challenge packet.
 
crypto_result_t crypto_process_auth_response (crypto_context_t *ctx, const uint8_t *packet, size_t packet_len)
 Process authentication response packet.
 

Network Integration Helpers

Packet creation and processing functions for network transmission. Handles packet formatting, encryption, and decryption automatically.

crypto_result_t crypto_create_public_key_packet (const crypto_context_t *ctx, uint8_t *packet_out, size_t packet_size, size_t *packet_len_out)
 Create public key packet for network transmission.
 
crypto_result_t crypto_process_public_key_packet (crypto_context_t *ctx, const uint8_t *packet, size_t packet_len)
 Process received public key packet from peer.
 
crypto_result_t crypto_create_encrypted_packet (crypto_context_t *ctx, const uint8_t *data, size_t data_len, uint8_t *packet_out, size_t packet_size, size_t *packet_len_out)
 Create encrypted data packet for network transmission.
 
crypto_result_t crypto_process_encrypted_packet (crypto_context_t *ctx, const uint8_t *packet, size_t packet_len, uint8_t *data_out, size_t data_size, size_t *data_len_out)
 Process received encrypted packet from peer.
 

Shared Cryptographic Operations

Low-level cryptographic operations used by both client and server for authentication and key exchange.

asciichat_error_t crypto_compute_password_hmac (crypto_context_t *ctx, const uint8_t *password_key, const uint8_t *nonce, const uint8_t *shared_secret, uint8_t *hmac_out)
 Compute password-based HMAC for authentication.
 
asciichat_error_t crypto_verify_peer_signature (const uint8_t *peer_public_key, const uint8_t *ephemeral_key, size_t ephemeral_key_size, const uint8_t *signature)
 Verify peer's signature on ephemeral key.
 
asciichat_error_t crypto_sign_ephemeral_key (const private_key_t *private_key, const uint8_t *ephemeral_key, size_t ephemeral_key_size, uint8_t *signature_out)
 Sign ephemeral key with private key.
 
void crypto_combine_auth_data (const uint8_t *hmac, const uint8_t *challenge_nonce, uint8_t *combined_out)
 Combine HMAC and challenge nonce for transmission.
 
void crypto_extract_auth_data (const uint8_t *combined_data, uint8_t *hmac_out, uint8_t *challenge_out)
 Extract HMAC and challenge nonce from combined data.
 

Session Rekeying Protocol

Periodic key rotation to limit exposure if keys are compromised. Rekeys after time threshold (default: 1 hour) OR packet count threshold (default: 1 million), whichever comes first.

Note
Test environment detection: If CRITERION_TEST or TESTING environment variable is set, rekey thresholds are reduced to 30 seconds / 1000 packets for faster testing.
Rekeying flow:
  1. Initiator calls crypto_rekey_init() and sends REKEY_REQUEST
  2. Responder processes request, calls crypto_rekey_process_request(), sends REKEY_RESPONSE
  3. Initiator processes response, calls crypto_rekey_process_response(), sends REKEY_COMPLETE
  4. Responder verifies REKEY_COMPLETE decrypts with new key, calls crypto_rekey_commit()
  5. Initiator calls crypto_rekey_commit() after receiving confirmation
Old keys remain active until REKEY_COMPLETE is verified, ensuring no service interruption.
bool crypto_should_rekey (const crypto_context_t *ctx)
 Check if rekeying should be triggered based on time or packet count thresholds.
 
crypto_result_t crypto_rekey_init (crypto_context_t *ctx)
 Initiate rekeying by generating new ephemeral keys.
 
crypto_result_t crypto_rekey_process_request (crypto_context_t *ctx, const uint8_t *peer_new_public_key)
 Process REKEY_REQUEST from peer (responder side)
 
crypto_result_t crypto_rekey_process_response (crypto_context_t *ctx, const uint8_t *peer_new_public_key)
 Process REKEY_RESPONSE from peer (initiator side)
 
crypto_result_t crypto_rekey_commit (crypto_context_t *ctx)
 Commit to new keys after successful REKEY_COMPLETE.
 
void crypto_rekey_abort (crypto_context_t *ctx)
 Abort rekeying and fallback to old keys.
 
void crypto_get_rekey_status (const crypto_context_t *ctx, char *status_buffer, size_t buffer_size)
 Get the current rekeying state for debugging/logging.
 
#define REKEY_MIN_INTERVAL   3
 Minimum time interval between rekey requests (3 seconds for testing, 60 for production)
 
#define REKEY_DEFAULT_TIME_THRESHOLD   3600
 Default rekey time threshold (1 hour in seconds)
 
#define REKEY_DEFAULT_PACKET_THRESHOLD   1000000
 Default rekey packet threshold (1 million packets)
 
#define REKEY_TEST_TIME_THRESHOLD   30
 Test mode rekey time threshold (30 seconds)
 
#define REKEY_TEST_PACKET_THRESHOLD   1000
 Test mode rekey packet threshold (1000 packets)
 
#define REKEY_MAX_FAILURE_COUNT   10
 Maximum consecutive rekey failures before giving up.
 
#define REKEY_MIN_REQUEST_INTERVAL   60
 Minimum interval between rekey requests (60 seconds, DDoS protection)
 

GPG Agent Connection Management

int gpg_agent_connect (void)
 Connect to gpg-agent.
 
void gpg_agent_disconnect (int sock)
 Disconnect from gpg-agent.
 
bool gpg_agent_is_available (void)
 Check if GPG agent is available.
 

GPG Agent Signing Operations

int gpg_agent_sign (int sock, const char *keygrip, const uint8_t *message, size_t message_len, uint8_t *signature_out, size_t *signature_len_out)
 Sign a message using GPG agent.
 

GPG Key Export

int gpg_get_public_key (const char *key_id, uint8_t *public_key_out, char *keygrip_out)
 Get public key from GPG keyring by key ID.
 

GPG Signing Operations

int gpg_sign_with_key (const char *key_id, const uint8_t *message, size_t message_len, uint8_t *signature_out, size_t *signature_len_out)
 Sign a message using GPG key and return OpenPGP signature.
 
int gpg_sign_detached_ed25519 (const char *key_id, const uint8_t *message, size_t message_len, uint8_t signature_out[64])
 Sign message with GPG and extract raw Ed25519 signature.
 

GPG Signature Verification

int gpg_verify_detached_ed25519 (const char *key_id, const uint8_t *message, size_t message_len, const uint8_t signature[64])
 Verify Ed25519 signature using GPG binary.
 
int gpg_verify_signature (const uint8_t *public_key, const uint8_t *message, size_t message_len, const uint8_t *signature)
 Verify Ed25519 signature using libgcrypt (no GPG binary required)
 
int gpg_verify_signature_with_binary (const uint8_t *signature, size_t signature_len, const uint8_t *message, size_t message_len, const char *expected_key_id)
 Verify OpenPGP signature using GPG binary.
 

Known Hosts Management

const char * get_known_hosts_path (void)
 Get the path to the known_hosts file.
 
asciichat_error_t add_known_host (const char *server_ip, uint16_t port, const uint8_t server_key[32])
 Add server to known_hosts.
 
asciichat_error_t remove_known_host (const char *server_ip, uint16_t port)
 Remove server from known_hosts.
 

Known Hosts Verification

asciichat_error_t check_known_host (const char *server_ip, uint16_t port, const uint8_t server_key[32])
 Check if server key is in known_hosts.
 
asciichat_error_t check_known_host_no_identity (const char *server_ip, uint16_t port)
 Check known_hosts for servers without identity key (no-identity entries)
 

User Interaction

bool display_mitm_warning (const char *server_ip, uint16_t port, const uint8_t expected_key[32], const uint8_t received_key[32])
 Display MITM warning with key comparison and prompt user for confirmation.
 
bool prompt_unknown_host (const char *server_ip, uint16_t port, const uint8_t server_key[32])
 Interactive prompt for unknown host - returns true if user wants to add, false to abort.
 
bool prompt_unknown_host_no_identity (const char *server_ip, uint16_t port)
 Interactive prompt for unknown host without identity key - returns true if user wants to continue, false to abort.
 

Key Fingerprinting

void compute_key_fingerprint (const uint8_t key[32], char fingerprint[65])
 Compute SHA256 fingerprint of Ed25519 key for display.
 

Cleanup

void known_hosts_cleanup (void)
 Cleanup function to free cached known_hosts path.
 

Trust Anchor Management

size_t read_trust_anchors_from_memory (anchor_list *dst, const unsigned char *pem_data, size_t pem_len)
 Read trust anchors from PEM-encoded data in memory.
 
void free_ta_contents (br_x509_trust_anchor *ta)
 Free the contents of a trust anchor.
 
#define ANCHOR_LIST_INIT   {NULL, 0, 0}
 Initializer for anchor_list.
 

SSH Agent Detection

bool ssh_agent_is_available (void)
 Check if ssh-agent is running and available.
 

SSH Agent Key Management

asciichat_error_t ssh_agent_add_key (const private_key_t *private_key, const char *key_path)
 Add a private key to ssh-agent.
 
bool ssh_agent_has_key (const public_key_t *public_key)
 Check if a public key is already in ssh-agent.
 
asciichat_error_t ssh_agent_get_key (const public_key_t *public_key, private_key_t *key_out)
 Retrieve a private key from ssh-agent by matching public key.
 
asciichat_error_t ssh_agent_sign (const public_key_t *public_key, const uint8_t *message, size_t message_len, uint8_t signature[64])
 Sign data using SSH agent with the specified public key.
 

Password Requirements

#define MIN_PASSWORD_LENGTH   8
 Minimum password length (8 characters)
 
#define MAX_PASSWORD_LENGTH   256
 Maximum password length (256 characters)
 

Algorithm-Specific Key Sizes

#define X25519_KEY_SIZE   32
 X25519 key size in bytes.
 
#define ED25519_PUBLIC_KEY_SIZE   32
 Ed25519 public key size in bytes.
 
#define ED25519_PRIVATE_KEY_SIZE   64
 Ed25519 private key size (seed + public) in bytes.
 
#define ED25519_SIGNATURE_SIZE   64
 Ed25519 signature size in bytes.
 
#define XSALSA20_NONCE_SIZE   24
 XSalsa20 nonce size in bytes.
 
#define POLY1305_MAC_SIZE   16
 Poly1305 MAC size in bytes.
 
#define HMAC_SHA256_SIZE   32
 HMAC-SHA256 output size in bytes.
 
#define ARGON2ID_SALT_SIZE   32
 Argon2id salt size in bytes.
 
#define SECRETBOX_KEY_SIZE   32
 Secretbox key size in bytes.
 
#define AES256_KEY_SIZE   32
 AES-256 key size in bytes.
 
#define AES_IV_SIZE   16
 AES initialization vector (IV) size in bytes.
 
#define AES256_DERIVED_SIZE   (AES256_KEY_SIZE + AES_IV_SIZE)
 AES-256 key + IV derived size in bytes (for bcrypt_pbkdf)
 
#define SESSION_ID_SIZE   16
 Session ID size in bytes.
 

Abstracted Cryptographic Constants

These constants abstract the underlying algorithms to allow future changes.

#define CRYPTO_PUBLIC_KEY_SIZE   X25519_KEY_SIZE
 Public key size (X25519)
 
#define CRYPTO_PRIVATE_KEY_SIZE   X25519_KEY_SIZE
 Private key size (X25519)
 
#define CRYPTO_SHARED_KEY_SIZE   X25519_KEY_SIZE
 Shared key size (X25519)
 
#define CRYPTO_ED25519_PUBLIC_KEY_SIZE   ED25519_PUBLIC_KEY_SIZE
 Ed25519 public key size.
 
#define CRYPTO_ED25519_PRIVATE_KEY_SIZE   ED25519_PRIVATE_KEY_SIZE
 Ed25519 private key size.
 
#define CRYPTO_ED25519_SIGNATURE_SIZE   ED25519_SIGNATURE_SIZE
 Ed25519 signature size.
 
#define CRYPTO_NONCE_SIZE   XSALSA20_NONCE_SIZE
 Nonce size (XSalsa20)
 
#define CRYPTO_SALT_SIZE   ARGON2ID_SALT_SIZE
 Salt size (Argon2id)
 
#define CRYPTO_ENCRYPTION_KEY_SIZE   SECRETBOX_KEY_SIZE
 Encryption key size (XSalsa20-Poly1305)
 
#define CRYPTO_MAC_SIZE   POLY1305_MAC_SIZE
 MAC size (Poly1305)
 
#define CRYPTO_HMAC_SIZE   HMAC_SHA256_SIZE
 HMAC size (HMAC-SHA256)
 

Authentication Packet Sizes

#define AUTH_HMAC_SIZE   32
 HMAC size in authentication packets (32 bytes)
 
#define AUTH_CHALLENGE_SIZE   32
 Challenge nonce size (32 bytes)
 
#define AUTH_COMBINED_SIZE   (AUTH_HMAC_SIZE + AUTH_CHALLENGE_SIZE)
 Combined authentication data size (HMAC + challenge, 64 bytes)
 
#define AUTH_SIGNATURE_SIZE   64
 Ed25519 signature size (64 bytes)
 
#define AUTH_SIGNATURE_COMBINED_SIZE   (AUTH_SIGNATURE_SIZE + AUTH_CHALLENGE_SIZE)
 Combined signature + challenge size (96 bytes)
 

Authentication Challenge Packet Structure

#define AUTH_CHALLENGE_FLAGS_SIZE   1
 Authentication flags size (1 byte)
 
#define AUTH_CHALLENGE_PACKET_SIZE   (AUTH_CHALLENGE_FLAGS_SIZE + AUTH_CHALLENGE_SIZE)
 Complete authentication challenge packet size (1 + 32 = 33 bytes)
 

Authentication Response Packet Sizes

#define AUTH_RESPONSE_PASSWORD_SIZE   (AUTH_HMAC_SIZE + AUTH_CHALLENGE_SIZE)
 Password-based authentication response size (HMAC + challenge, 64 bytes)
 
#define AUTH_RESPONSE_SIGNATURE_SIZE   (AUTH_SIGNATURE_SIZE + AUTH_CHALLENGE_SIZE)
 Signature-based authentication response size (signature + challenge, 96 bytes)
 

Packet Size Limits

#define MAX_AUTH_FAILED_PACKET_SIZE   256
 Maximum AUTH_FAILED packet size (256 bytes)
 
#define MAX_ENCRYPTED_PACKET_SIZE   65536
 Maximum encrypted packet size (64KB)
 

Buffer Sizes

#define HEX_STRING_SIZE_32   (32 * 2 + 1)
 Hex string size for 32-byte values (64 hex chars + null terminator)
 
#define HEX_STRING_SIZE_64   (64 * 2 + 1)
 Hex string size for 64-byte values (128 hex chars + null terminator)
 
#define PASSWORD_BUFFER_SIZE   256
 Password input buffer size (256 bytes)
 
#define ZERO_KEY_SIZE   X25519_KEY_SIZE
 Zero key array size (32 bytes, used for no-identity entries)
 

Encryption Size Limits

#define CRYPTO_MAX_PLAINTEXT_SIZE   ((size_t)1024 * 1024)
 Maximum plaintext size (1MB)
 
#define CRYPTO_MAX_CIPHERTEXT_SIZE   (CRYPTO_MAX_PLAINTEXT_SIZE + CRYPTO_MAC_SIZE)
 Maximum ciphertext size (plaintext + MAC, ~1MB + 16 bytes)
 

Hex String Size Constants

#define CRYPTO_HEX_KEY_SIZE   64
 Hex string size for 32-byte key (64 hex characters)
 
#define CRYPTO_HEX_KEY_SIZE_NULL   65
 Hex string size for 32-byte key with null terminator (65 bytes)
 
#define CRYPTO_HEX_KEY64_SIZE   128
 Hex string size for 64-byte key (128 hex characters)
 
#define CRYPTO_HEX_KEY64_SIZE_NULL   129
 Hex string size for 64-byte key with null terminator (129 bytes)
 

Cryptographic String Literals

#define SSH_ED25519_KEY_TYPE   "ssh-ed25519"
 SSH Ed25519 key type string ("ssh-ed25519")
 
#define X25519_KEY_TYPE   "x25519"
 X25519 key type string ("x25519")
 
#define NO_IDENTITY_MARKER   "no-identity"
 No-identity entry marker ("no-identity")
 

Detailed Description

🔐 Core cryptographic operations for ascii-chat

This header provides the core cryptographic operations for secure communication in ascii-chat, including key exchange, encryption/decryption, authentication, and session rekeying.

The interface provides:

Note
Key Exchange: Uses ephemeral X25519 keys for perfect forward secrecy. Each connection generates new keys automatically.
Encryption: XSalsa20-Poly1305 provides authenticated encryption with automatic MAC verification. Nonces are generated as session_id || counter to prevent replay attacks.
Password Authentication: Optional password-based encryption using Argon2id for memory-hard key derivation. Passwords are bound to DH shared secrets to prevent MITM attacks.
Rekeying: Automatic periodic key rotation after time threshold (1 hour) OR packet count threshold (1 million), whichever comes first. Test mode uses reduced thresholds (30 seconds / 1000 packets).
Test Environment: Automatically detects test environment via CRITERION_TEST or TESTING environment variables and adjusts thresholds accordingly.
Byte Order: Client must convert network byte order to host byte order for crypto parameters. Server uses host byte order directly.
Key Exchange Formats:
  • Simple format: Only ephemeral public key (when server has no identity key)
  • Authenticated format: Ephemeral key + identity key + signature (when server has identity key)
Warning
Always use crypto_secure_compare() for comparing sensitive data (keys, MACs, HMACs). Do NOT use regular memcmp() as it is vulnerable to timing attacks.
Nonce counter starts at 1 (0 is reserved for testing). Returns CRYPTO_ERROR_NONCE_EXHAUSTED if counter reaches 0 or UINT64_MAX (extremely unlikely, but triggers rekeying).
Password salt is deterministic ("ascii-chat-password-salt-v1") for consistent key derivation across client/server. Only use for session encryption, not long-term storage.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides GPG agent (gpg-agent) integration for signing operations with GPG keys. Allows private keys to stay in GPG agent without being loaded into application memory.

Note
GPG agent protocol: Implements Assuan protocol for communicating with gpg-agent. Keys stay in agent and are never loaded into application memory.
Platform support:
  • Unix: Uses GPG_AGENT_INFO or connects to standard socket (~/.gnupg/S.gpg-agent)
  • Windows: Connects to standard named pipe (Gpg4win installs gpg-agent as service)
Key format: Only Ed25519 GPG keys are supported. RSA/ECDSA GPG keys are NOT supported.
Keygrip: GPG uses keygrips (40-char hex strings) to identify keys in the agent. Keygrips are computed from public key material and are stable identifiers.
Agent detection: Checks for agent socket/pipe existence and accessibility. On Unix, verifies socket is accessible. On Windows, checks for named pipe.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides functions for exporting public keys from GPG keyring. Supports retrieving Ed25519 public keys and keygrips for use in authentication and signing operations.

Note
Key export: Uses gpg --export to extract public key from local keyring. Parses OpenPGP packet format to extract Ed25519 public key material.
Keygrip extraction: Optionally extracts keygrip for use with GPG agent. Keygrip is a stable 40-char hex identifier computed from public key.
Key ID formats: Supports short (8-char), long (16-char), and full (40-char) key IDs. Accepts key IDs with or without "0x" prefix.
Ed25519 only: Only Ed25519 GPG keys are supported. RSA/ECDSA keys will cause export to fail.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides the complete GPG interface by including all submodule headers. Users can include this single header to access all GPG functionality.

Submodules:

This header provides GPG signing operations for creating detached signatures. Supports both OpenPGP-formatted signatures and raw Ed25519 signatures extracted from GPG output.

Note
Signing method: Uses gpg --detach-sign to create detached signatures. Signatures are separate from message data (not inline signatures).
Key requirements: Only Ed25519 GPG keys are supported. RSA/ECDSA keys will cause signing operations to fail.
Output formats:
GPG binary: Requires gpg binary in PATH for all operations. Returns error if GPG is not installed or not accessible.
Key passphrase: If key is encrypted, GPG may prompt for passphrase. Use ssh-agent or gpg-agent for password-free signing, or set $ASCII_CHAT_KEY_PASSWORD environment variable.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides GPG signature verification operations supporting both GPG binary-based verification and direct cryptographic verification via libgcrypt. Handles both raw Ed25519 signatures and OpenPGP-formatted signatures.

Note
Verification methods:
  • GPG binary: Uses gpg --verify for full OpenPGP packet verification
  • libgcrypt: Direct Ed25519 signature verification without GPG binary
Key requirements: Only Ed25519 signatures are supported. RSA/ECDSA signatures will cause verification to fail.
Signature formats:
GPG binary dependency: Functions using GPG binary require gpg in PATH. libgcrypt-based verification works without GPG binary installed.
Key trust: GPG binary verification checks key trust and validity. libgcrypt verification only checks cryptographic signature validity.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides known hosts management functionality similar to SSH's known_hosts. Tracks server identity keys to detect man-in-the-middle attacks and key changes.

Known hosts file format:

Note
File format examples:
  • IPv4: 192.0.2.1:8080 x25519 1234abcd... ascii-chat
  • IPv6: [2001:db8::1]:8080 x25519 1234abcd... ascii-chat
  • No-identity: 192.0.2.1:8080 no-identity
No-identity servers: Servers without identity keys use "no-identity" entries. Cannot verify key (ephemeral keys change each connection) but can track server identity.
Key comparison: Uses constant-time comparison (sodium_memcmp) to prevent timing attacks.
MITM detection: Detects key mismatches by comparing received key with stored key. If key doesn't match, displays warning and prompts user for confirmation.
Multiple entries: Can have multiple entries for same IP:port (e.g., key rotation). Function searches all entries and uses first matching key.
Zero key handling: Special case for no-identity servers with zero keys. Zero key matches zero key (secure no-identity connection previously accepted).
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This file contains BearSSL tools utilities adapted to work with in-memory PEM data instead of files. Original code from BearSSL tools (ISC license).

These utilities are used for loading system CA certificates for TLS validation.

Note
BearSSL adaptation: Original BearSSL tools read PEM data from files. This version works with in-memory PEM data for better flexibility.
Trust anchors: Trust anchors are root CA certificates used for TLS validation. Loaded from system CA certificate store and used to validate server certificates.
Memory management: Trust anchors contain dynamically allocated memory. Must call free_ta_contents() for each anchor and free anchor_list.buf.
Author
Thomas Pornin (original BearSSL tools)
Zachary Fogg me@zf.nosp@m.o.gg (adaptation for ascii-chat)
Date
October 2025

This header provides SSH agent integration for signing operations. Allows keys to stay in SSH agent (not loaded into memory) for better security.

Note
SSH agent: Uses SSH agent protocol to communicate with ssh-agent. Keys stay in agent and are never loaded into application memory.
Platform support:
  • Unix: Uses SSH_AUTH_SOCK environment variable (Unix domain socket)
  • Windows: Uses SSH_AUTH_SOCK environment variable (named pipe)
Key format: Only Ed25519 keys are supported. RSA/ECDSA keys are NOT supported.
Agent detection: Checks SSH_AUTH_SOCK environment variable. On Unix, also verifies socket is accessible. On Windows, only checks environment variable (named pipes handled differently).
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Cryptography README

This page provides comprehensive documentation on ascii-chat's cryptographic implementation, including threat models, algorithms, protocols, security considerations, and known vulnerabilities.

Overview

ascii-chat implements end-to-end encryption by default using modern cryptographic primitives from libsodium. All data packets (headers and payloads) are encrypted after the initial handshake, protecting against eavesdropping and tampering.

Key Features

  • Encrypted by default - No configuration required
  • Modern crypto - X25519, XSalsa20-Poly1305, Argon2id
  • SSH/GPG key integration - Use existing Ed25519 keys
  • SSH agent support - Auto-adds keys, eliminates password prompts
  • GPG agent support - Use gpg-agent for signing, no passphrase prompts
  • GitHub/GitLab integration - Fetch public keys automatically
  • Password protection - Optional shared password authentication
  • Client whitelisting - Server-side access control
  • Known hosts - SSH-style server verification
  • Forward secrecy - Ephemeral key exchange per connection
  • Dynamic algorithm negotiation - Future-proof crypto with post-quantum support

Non-Goals

Not a replacement for TLS/HTTPS - Different trust model ❌ Not anonymous - Focus is encryption, not anonymity ❌ Not quantum-resistant - Uses elliptic curve cryptography (X25519) - Post-quantum support planned via dynamic algorithm negotiation

Philosophy & Threat Model

The MITM Problem

ascii-chat faces a fundamental cryptographic challenge: there is no pre-existing trust infrastructure like the Certificate Authority (CA) system used by HTTPS. This creates a security tradeoff:

Without verification:

  • ✅ Privacy: Encrypted against passive eavesdropping (ISP, WiFi admin, etc.)
  • ❌ Security: Vulnerable to active Man-in-the-Middle (MITM) attacks

With verification:

  • ✅ Privacy: Encrypted against passive eavesdropping
  • ✅ Security: Protected against MITM attacks via key pinning

Default Behavior: Privacy Without Trust

By default, ascii-chat provides privacy but not authentication:

# Server (ephemeral key generated)
./ascii-chat server
# Client (ephemeral key generated, no verification)
./ascii-chat client

Why this default?

  1. No certificate infrastructure - Unlike HTTPS, there's no global CA system
  2. Ease of use - Works immediately without configuration
  3. Better than nothing - Protects against passive attacks (most common threat)
  4. User choice - Advanced users can add verification with --server-key

This is similar to Bluetooth pairing or Signal safety numbers - the first connection is vulnerable, but subsequent connections can be verified.

Trust Models Supported

Mode Trust Mechanism MITM Protection Use Case
Default None (ephemeral DH) Quick sessions, low-threat environments
Password Shared secret Friends exchanging password out-of-band
SSH Keys Key pinning Tech users with existing SSH keys
GPG Keys Key pinning + gpg-agent GPG users, no passphrase prompts
GitHub/GitLab Social proof + keys Verify identity via public profiles
Known Hosts First-use trust ⚠️ Like SSH - detect key changes
Whitelist Pre-approved keys Private servers, access control

Comparison to Other Protocols

Protocol Default Encryption Trust Model Verification Difficulty
HTTPS ✅ Always CA system Automatic (OS trust store)
SSH ✅ Always Known hosts Manual (first connection prompts)
Signal ✅ Always Safety numbers Manual (QR code scanning)
ascii-chat ✅ Always Ephemeral DH Optional (–server-key flag)
Zoom ✅ Sometimes Central server None (trust Zoom)

Cryptographic Primitives

ascii-chat uses libsodium, a modern, portable, easy-to-use crypto library based on NaCl.

X25519 (Key Exchange)

  • Algorithm: Elliptic Curve Diffie-Hellman (ECDH) on Curve25519
  • Key Size: 32 bytes (256 bits)
  • Purpose: Establish shared secret between client and server
  • Properties: Forward secrecy, constant-time operations
  • Function: crypto_box_beforenm()

Why X25519?

  • Fast: ~40,000 operations/second on modern CPUs
  • Secure: No known practical attacks, constant-time implementation
  • Small: 32-byte keys, 32-byte shared secrets
  • Standard: RFC 7748, widely used (TLS 1.3, SSH, Signal)

XSalsa20-Poly1305 (Encryption)

  • Algorithm: Stream cipher (XSalsa20) + MAC (Poly1305)
  • Key Size: 32 bytes (256 bits)
  • Nonce Size: 24 bytes (192 bits)
  • MAC Size: 16 bytes (128 bits)
  • Purpose: Encrypt packet data with authenticated encryption
  • Function: crypto_secretbox_easy()

Why XSalsa20-Poly1305?

  • AEAD: Authenticated Encryption with Associated Data (prevents tampering)
  • Fast: ~700 MB/s encryption on modern CPUs
  • Nonce-misuse resistant: Large 192-bit nonce makes collisions astronomically unlikely
  • Proven: Used in libsodium, NaCl, and many production systems

Encryption formula:

ciphertext = XSalsa20(key, nonce, plaintext) + Poly1305(key, ciphertext)

Argon2id (Password Hashing)

  • Algorithm: Argon2id (hybrid Argon2i + Argon2d)
  • Purpose: Derive encryption keys from passwords
  • Function: crypto_pwhash()
  • Parameters:
    • Memory: 64 MB (interactive limit)
    • Operations: 2 (OPSLIMIT_INTERACTIVE)
    • Parallelism: 1
  • Password Requirements:
    • Minimum: 8 characters
    • Maximum: 256 characters
    • Validation: Enforced on both client and server

Why Argon2id?

  • Memory-hard: Resistant to GPU/ASIC brute-force attacks
  • Modern: Winner of Password Hashing Competition (2015)
  • Hybrid: Combines data-dependent (Argon2d) and data-independent (Argon2i) modes
  • Tunable: Can increase difficulty as hardware improves

Ed25519 (Signatures - SSH Keys)

  • Algorithm: EdDSA signatures on Edwards curve (Curve25519)
  • Key Size: 32 bytes public key, 64 bytes private key (seed + public)
  • Signature Size: 64 bytes
  • Purpose: Authenticate clients with SSH keys
  • Functions: crypto_sign_detached(), crypto_sign_verify_detached()

Ed25519 to X25519 Conversion:

// Convert Ed25519 public key to X25519 for DH
crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk);
// Convert Ed25519 private key to X25519 for DH
crypto_sign_ed25519_sk_to_curve25519(x25519_sk, ed25519_sk);

This allows using existing SSH Ed25519 keys for both signing (authentication) and key exchange (encryption).

bcrypt_pbkdf (SSH Key Decryption)

  • Algorithm: bcrypt-based password-based key derivation function (OpenBSD)
  • Purpose: Decrypt encrypted SSH Ed25519 private keys (OpenSSH format)
  • Library: libsodium-bcrypt-pbkdf (GitHub: openssh/openssh-portable)
  • Integration: cmake/LibsodiumBcryptPbkdf.cmake

Supported OpenSSH Key Encryption:

  • Cipher: aes256-ctr, aes256-cbc (OpenSSH defaults for Ed25519 keys)
  • KDF: bcrypt (bcrypt_pbkdf with configurable rounds, typically 16-24)
  • Key Format: openssh-key-v1 (OpenSSH private key format)

Native Decryption Process:

// 1. Parse OpenSSH key file format (openssh-key-v1)
// 2. Extract KDF parameters (salt, rounds)
// 3. Derive decryption key + IV using bcrypt_pbkdf:
uint8_t derived[48]; // 32-byte key + 16-byte IV
sodium_bcrypt_pbkdf(password, strlen(password), salt, salt_len,
derived, sizeof(derived), rounds);
const uint8_t *key = derived; // First 32 bytes
const uint8_t *derived_iv = derived + 32; // Last 16 bytes
// 4. Decrypt private key blob using BearSSL AES-256-CTR:
br_aes_ct_ctr_keys aes_ctx;
br_aes_ct_ctr_init(&aes_ctx, key, 32); // 32-byte key
// Extract initial counter from last 4 bytes of IV (big-endian)
uint32_t initial_counter = ((uint32_t)derived_iv[12] << 24) |
((uint32_t)derived_iv[13] << 16) |
((uint32_t)derived_iv[14] << 8) |
((uint32_t)derived_iv[15]);
// Decrypt in-place (BearSSL reads first 12 bytes of derived_iv as nonce)
memcpy(decrypted, encrypted_blob, blob_len);
br_aes_ct_ctr_run(&aes_ctx, derived_iv, initial_counter, decrypted, blob_len);
// 5. Parse decrypted Ed25519 seed (32 bytes) + public key (32 bytes)
unsigned int uint32_t
Definition common.h:58
unsigned char uint8_t
Definition common.h:56

Why bcrypt_pbkdf?

  • Memory-hard: Resistant to GPU/FPGA brute-force attacks
  • OpenSSH standard: Same algorithm as ssh-keygen uses for encryption
  • Battle-tested: Used by OpenSSH since 2013 (OpenSSH 6.5+)
  • Configurable: Adjustable rounds parameter (higher = slower = more secure)

The PBKDF Security Tradeoff:

PBKDF creates an asymmetric cost between legitimate users and attackers:

  • Legitimate user: Pays the cost once per session (~200ms to load SSH key)
  • Attacker: Pays the cost for every password guess (billions of attempts)
Without PBKDF (direct SHA256):
User experience: Instant key loading
Attacker speed: 60 billion passwords/second (GPU)
Weak password: Cracked in 0.016 seconds ⚠️
With bcrypt_pbkdf (rounds=16):
User experience: 200ms delay (barely noticeable)
Attacker speed: ~5 passwords/second (memory-hard slowdown)
Weak password: Cracked in 6.3 years ✅
Strong password: Effectively uncrackable (10^20+ years)

Keyspace vs. Compute Time:

A common observation: "Attackers get a smaller keyspace (passwords) instead of full AES-256 keyspace (2^256), but they sacrifice compute time for it."

This is precisely correct. The tradeoff works as follows:

Attack Strategy Keyspace Size Keys/Second Time to Exhaust
Try random AES keys 2^256 (~10^77) 10^9 3.67 × 10^60 years (IMPOSSIBLE)
Try passwords (no PBKDF) ~10^12 common 60 × 10^9 16 minutes ⚠️
Try passwords (bcrypt r=16) ~10^12 common 5 6,300 years ✅

Key insight: The attacker chooses a drastically smaller keyspace (password space instead of full AES keyspace) to make the problem tractable, but PBKDF makes that small keyspace exponentially more expensive to search by adding computational cost to each attempt.

The result: Legitimate users pay ~200ms once, attackers pay years of compute time trying password guesses. This asymmetry is what makes password-based encryption practical and secure.

Tunable security (rounds parameter):

rounds=10: ~50ms delay, attacker: 50,000 guesses/sec (weak)
rounds=16: ~200ms delay, attacker: 5,000 guesses/sec (OpenSSH default)
rounds=20: ~800ms delay, attacker: 1,250 guesses/sec (stronger)
rounds=24: ~3.2sec delay, attacker: 312 guesses/sec (very strong)

OpenSSH chose rounds=16 as optimal: barely noticeable for users, devastating for attackers.

Why attackers can't skip PBKDF:

  • The encrypted file contains salt + rounds (attacker knows the parameters)
  • The AES key was derived using bcrypt_pbkdf (not a random key)
  • Attacker must replicate the exact derivation to get the correct key
  • Trying random 256-bit keys is hopeless (2^256 keyspace, would take 10^60 years)
  • Only viable attack: guess passwords and run them through bcrypt_pbkdf

Salt prevents rainbow tables:

  • Each encrypted key file has unique random salt (16 bytes)
  • Same password + different salt = completely different derived key
  • Attacker cannot pre-compute password→key mappings
  • Must derive keys fresh for each target file

Implementation verification:

  • Byte-for-byte validation against ssh-keygen decryption output
  • Supports same key formats as OpenSSH (aes256-ctr, aes256-cbc)
  • No external tool dependencies (fully native C implementation)

See also:

  • lib/crypto/keys/ssh_keys.c - Native decryption implementation
  • cmake/LibsodiumBcryptPbkdf.cmake - Library integration
  • deps/bearssl/ - BearSSL minimal TLS library (AES-CTR/CBC)

Randomness Source

CSPRNG: randombytes_buf() (libsodium)

  • Implementation: Platform-specific secure random
    • Linux/BSD: /dev/urandom
    • Windows: CryptGenRandom() / BCryptGenRandom()
    • macOS: arc4random_buf()
  • Properties: Cryptographically secure, non-blocking

Protocol Architecture

Packet Structure

All packets (encrypted and unencrypted) share a common header:

typedef struct {
uint32_t magic; // 0xDEADBEEF (packet validation)
uint16_t type; // packet_type_t (see below)
uint32_t length; // payload length in bytes
uint32_t crc32; // CRC32 checksum of payload
uint32_t client_id; // source client ID (0 = server)
} __attribute__((packed)) packet_header_t; // 18 bytes
unsigned short uint16_t
Definition common.h:57
RGB pixel structure.
Definition video/image.h:80
Network packet header structure.
Definition packet.h:490

Note: All multi-byte fields are in network byte order (big-endian).

Packet Types

Protocol Negotiation Packets (Always Unencrypted):

PACKET_TYPE_PROTOCOL_VERSION = 1 // Client → Server: Protocol version, compression support, encryption enabled
@ PACKET_TYPE_PROTOCOL_VERSION
Protocol version and capabilities negotiation.
Definition packet.h:283

Crypto Handshake Packets (Always Unencrypted):

PACKET_TYPE_CRYPTO_CAPABILITIES = 14 // Client → Server: Supported crypto algorithms
PACKET_TYPE_CRYPTO_PARAMETERS = 15 // Server → Client: Chosen algorithms + data sizes
PACKET_TYPE_CRYPTO_KEY_EXCHANGE_INIT = 16 // Server → Client: DH public key
PACKET_TYPE_CRYPTO_KEY_EXCHANGE_RESP = 17 // Client → Server: DH public key
PACKET_TYPE_CRYPTO_AUTH_CHALLENGE = 18 // Server → Client: Challenge nonce
PACKET_TYPE_CRYPTO_AUTH_RESPONSE = 19 // Client → Server: HMAC response
PACKET_TYPE_CRYPTO_AUTH_FAILED = 20 // Server → Client: Failure
PACKET_TYPE_CRYPTO_SERVER_AUTH_RESP = 21 // Server → Client: HMAC proof
PACKET_TYPE_CRYPTO_HANDSHAKE_COMPLETE = 22 // Server → Client: Success
PACKET_TYPE_CRYPTO_NO_ENCRYPTION = 23 // Client → Server: Opt-out
@ PACKET_TYPE_CRYPTO_AUTH_RESPONSE
Client -> Server: {HMAC[32]} (UNENCRYPTED)
Definition packet.h:323
@ PACKET_TYPE_CRYPTO_HANDSHAKE_COMPLETE
Server -> Client: "encryption ready" (UNENCRYPTED)
Definition packet.h:329
@ PACKET_TYPE_CRYPTO_KEY_EXCHANGE_INIT
Server -> Client: {server_pubkey[32]} (UNENCRYPTED)
Definition packet.h:317
@ PACKET_TYPE_CRYPTO_KEY_EXCHANGE_RESP
Client -> Server: {client_pubkey[32]} (UNENCRYPTED)
Definition packet.h:319
@ PACKET_TYPE_CRYPTO_AUTH_FAILED
Server -> Client: "authentication failed" (UNENCRYPTED)
Definition packet.h:325
@ PACKET_TYPE_CRYPTO_SERVER_AUTH_RESP
Server -> Client: {HMAC[32]} server proves knowledge (UNENCRYPTED)
Definition packet.h:327
@ PACKET_TYPE_CRYPTO_NO_ENCRYPTION
Client -> Server: "I want to proceed without encryption" (UNENCRYPTED)
Definition packet.h:331
@ PACKET_TYPE_CRYPTO_AUTH_CHALLENGE
Server -> Client: {nonce[32]} (UNENCRYPTED)
Definition packet.h:321
@ PACKET_TYPE_CRYPTO_PARAMETERS
Server -> Client: Chosen algorithms + data sizes (UNENCRYPTED)
Definition packet.h:315
@ PACKET_TYPE_CRYPTO_CAPABILITIES
Client -> Server: Supported crypto algorithms (UNENCRYPTED)
Definition packet.h:313

Encrypted Packets (After Handshake):

PACKET_TYPE_ENCRYPTED = 24 // Wrapper for all post-handshake packets
@ PACKET_TYPE_ENCRYPTED
Encrypted packet (after handshake completion)
Definition packet.h:333

All application packets (video, audio, control) are wrapped in PACKET_TYPE_ENCRYPTED after successful handshake.

Dynamic Algorithm Negotiation: The CRYPTO_CAPABILITIES and CRYPTO_PARAMETERS packets enable future-proof algorithm selection:

  • Client Capabilities: Declares supported algorithms (X25519, Ed25519, XSalsa20-Poly1305)
  • Server Parameters: Selects algorithms and provides key sizes for dynamic handshake
  • Future-Proof: Designed to support post-quantum algorithms (Kyber, Dilithium) when available

Why unencrypted? These packets establish the encryption keys - they cannot be encrypted with keys that don't exist yet. This is standard for all key exchange protocols (TLS, SSH, etc.).

Wire Format: Encrypted vs Unencrypted

Unencrypted Packet (Handshake):

[Header: 18 bytes][Payload: N bytes]
| |
magic=0xDEADBEEF type-specific data
type=14-21
length=N
crc32=checksum
client_id=0

Encrypted Packet (Post-Handshake):

[Outer Header: 18 bytes][Encrypted Blob]
| |
magic=0xDEADBEEF [Nonce: 24 bytes][Inner Header + Payload: N bytes][MAC: 16 bytes]
type=20 | | |
length=24+N+16 Random nonce Real packet data Poly1305 tag
crc32=0
client_id=0

Decryption process:

  1. Verify outer header (magic, type)
  2. Extract nonce (first 24 bytes of encrypted blob)
  3. Decrypt remaining blob with XSalsa20-Poly1305
  4. Parse inner header from plaintext
  5. Extract real payload

Result: An attacker sees:

  • Outer packet magic (needed for framing)
  • Fact that packet is encrypted
  • Total encrypted size (nonce + ciphertext + MAC)

Hidden from attacker:

  • Real packet type (video, audio, control?)
  • Real payload size
  • Client ID
  • All payload data

Handshake Protocol

Sequence Diagram

Client Server
| |
|------ TCP Connect ------------------->|
| |
|<----- PROTOCOL_VERSION ----------------|
|------ PROTOCOL_VERSION --------------->|
| [version, encryption support] |
| |
|------ CRYPTO_CAPABILITIES ------------>|
| [supported algorithms] |
| [preferred algorithms] |
| |
|<----- CRYPTO_PARAMETERS ---------------|
| [selected algorithms + sizes] |
| |
| | Generate ephemeral keypair
| | (or use --key loaded SSH key)
| |
|<----- CRYPTO_KEY_EXCHANGE_INIT --------|
| [32-byte X25519 public key] |
| [32-byte Ed25519 identity key] | (if using SSH key)
| [64-byte Ed25519 signature] | (signature of X25519 key)
| |
| Verify signature (if present) |
| Check against --server-key (if set) |
| Check known_hosts (if exists) |
| Compute DH shared secret |
| |
|------ CRYPTO_KEY_EXCHANGE_RESP ------->|
| [32-byte X25519 public key] |
| [32-byte Ed25519 identity key] | (if using SSH key)
| [64-byte Ed25519 signature] | (signature of X25519 key)
| |
| | Verify signature (if present)
| | Check whitelist (if enabled)
| | Compute DH shared secret
| |
|<----- CRYPTO_AUTH_CHALLENGE -----------|
| [32-byte random nonce] |
| [1-byte flags] | (password required? key required?)
| |
| Compute HMAC(shared_secret, nonce) |
| If password: HMAC(password_key, nonce) |
| |
|------ CRYPTO_AUTH_RESPONSE ----------->|
| [32-byte HMAC] |
| |
| | Verify HMAC
| | Check password (if required)
| | Check client key whitelist (if enabled)
| |
|<----- CRYPTO_HANDSHAKE_COMPLETE -------|
| |
| ✅ Encryption active | ✅ Encryption active
| |
|<===== ENCRYPTED PACKETS ==============>|
| All future packets encrypted |

Handshake Phases Explained

Phase 0: Dynamic Algorithm Negotiation

**Client Capabilities (CRYPTO_CAPABILITIES):**

typedef struct {
uint16_t supported_kex_algorithms; // Bitmask of supported KEX algorithms
uint16_t supported_auth_algorithms; // Bitmask of supported auth algorithms
uint16_t supported_cipher_algorithms; // Bitmask of supported cipher algorithms
uint8_t requires_verification; // Boolean: requires server identity verification
uint8_t preferred_kex; // Preferred KEX algorithm
uint8_t preferred_auth; // Preferred auth algorithm
uint8_t preferred_cipher; // Preferred cipher algorithm
Crypto capabilities packet structure (Packet Type 14)
Definition packet.h:844

**Server Parameters (CRYPTO_PARAMETERS):**

typedef struct {
uint8_t selected_kex; // Which KEX algorithm (KEX_ALGO_*)
uint8_t selected_auth; // Which auth algorithm (AUTH_ALGO_*)
uint8_t selected_cipher; // Which cipher algorithm (CIPHER_ALGO_*)
uint8_t verification_enabled; // Boolean: server requires verification
uint16_t kex_public_key_size; // e.g., 32 for X25519, 1568 for Kyber1024
uint16_t auth_public_key_size; // e.g., 32 for Ed25519, 1952 for Dilithium3
uint16_t signature_size; // e.g., 64 for Ed25519, 3309 for Dilithium3
uint16_t shared_secret_size; // e.g., 32 for X25519
uint8_t nonce_size; // e.g., 24 for XSalsa20 nonce
uint8_t mac_size; // e.g., 16 for Poly1305 MAC
uint8_t hmac_size; // e.g., 32 for HMAC-SHA256
Crypto parameters packet structure (Packet Type 15)
Definition packet.h:873

Current Algorithm Constants:

#define KEX_ALGO_X25519 0x01
#define AUTH_ALGO_ED25519 0x01
#define AUTH_ALGO_NONE 0x00
#define CIPHER_ALGO_XSALSA20_POLY1305 0x01

Future-Proofing Design:

  • Algorithm negotiation enables post-quantum migration
  • Hybrid mode support (classical + post-quantum)
  • Backward compatibility with older clients
  • Dynamic key sizes for different algorithms

Phase 1: Key Exchange Init (Server → Client)

Server sends:

typedef struct {
uint8_t ephemeral_public_key[32]; // X25519 DH public key (always)
uint8_t identity_public_key[32]; // Ed25519 identity (if --key used)
uint8_t signature[64]; // Ed25519 signature (if --key used)
} key_exchange_init_packet_t;

Packet size:

  • 32 bytes: Ephemeral mode (default)
  • 128 bytes: Authenticated mode (--key SSH key)

Client verifies:

  1. If --server-key provided: Verify identity key matches expected key → ABORT if mismatch
  2. If signature present: Verify signature is valid for ephemeral_public_key using identity_public_key
  3. Check known_hosts: If server:port in ~/.ascii-chat/known_hosts, verify identity key matches
  4. First connection: Prompt user to save to known_hosts

Security note: The signature binds the ephemeral key to the long-term identity key, preventing an attacker from replacing the DH key while keeping the identity key.

Phase 2: Key Exchange Response (Client → Server)

Client sends:

typedef struct {
uint8_t ephemeral_public_key[32]; // X25519 DH public key (always)
uint8_t identity_public_key[32]; // Ed25519 identity (if --key used)
uint8_t signature[64]; // Ed25519 signature (if --key used)
} key_exchange_response_packet_t;

Server verifies:

  1. If --client-keys provided: Check if identity_public_key is in whitelist → REJECT if not found
  2. If signature present: Verify signature is valid
  3. Compute shared secret: shared_secret = X25519(server_private, client_public)

At this point both sides have:

  • ✅ Ephemeral DH shared secret (32 bytes)
  • ✅ Peer's identity public key (if authenticated mode)

Phase 3: Authentication Challenge (Server → Client)

Server sends:

typedef struct {
uint8_t nonce[32]; // Random challenge nonce
uint8_t flags; // AUTH_REQUIRE_PASSWORD | AUTH_REQUIRE_CLIENT_KEY
} auth_challenge_packet_t;

Flags:

  • AUTH_REQUIRE_PASSWORD (0x01): Server has --password, client must prove knowledge
  • AUTH_REQUIRE_CLIENT_KEY (0x02): Server has --client-keys, client must be whitelisted

Client prepares response:

  1. If password set: HMAC(password_key, nonce || shared_secret) using Argon2-derived key
  2. If no password: HMAC(shared_secret, nonce || shared_secret) using DH shared secret
  3. If client has SSH key: Also sign nonce with Ed25519 identity key

Phase 4: Authentication Response (Client → Server)

Client sends:

typedef struct {
uint8_t hmac[32]; // HMAC of challenge nonce || shared_secret
} auth_response_packet_t;

Server verifies:

  1. Recompute HMAC using same key (password_key or shared_secret) and same data (nonce || shared_secret)
  2. Constant-time compare with received HMAC → REJECT if mismatch
  3. If whitelist enabled: Verify client's identity key is in --client-keys

Phase 5: Server Authentication Response (Server → Client)

Server sends:

typedef struct {
uint8_t hmac[32]; // HMAC of client_nonce || shared_secret
} server_auth_response_packet_t;

Client verifies:

  1. Recompute HMAC using shared_secret and client_nonce
  2. Constant-time compare with received HMAC → ABORT if mismatch

This provides mutual authentication - both sides prove knowledge of the shared secret.

Phase 6: Handshake Complete or Failed

Success:

PACKET_TYPE_HANDSHAKE_COMPLETE // Empty packet

Failure:

PACKET_TYPE_AUTH_FAILED // Empty packet

After HANDSHAKE_COMPLETE, both sides:

  • ✅ Enable packet encryption using XSalsa20-Poly1305
  • ✅ Use shared secret as encryption key
  • ✅ Increment nonce counter for each packet

Keys Module

Design Principle: Separation of Identity and Encryption

Core architecture decision: SSH keys are ONLY used for authentication (identity proof), NEVER for encryption.

Why this matters:

  1. Forward Secrecy is Critical
    • If your SSH key is compromised today, an attacker should NOT be able to decrypt conversations from last week
    • Ephemeral keys provide this guarantee: each session has unique encryption keys that are destroyed after use
    • Using long-term keys for encryption breaks forward secrecy (recorded traffic can be decrypted later)
  2. Consistency Across Environments
    • SSH agent availability varies by system (Unix-only, requires configuration)
    • Users should get identical security regardless of environment
  3. Matches SSH Protocol Design
    • SSH itself uses this exact model: long-term host/user keys for identity, ephemeral DH keys for encryption
    • Proven design with 30+ years of security analysis
    • No need to reinvent the wheel

SSH Agent Integration

ascii-chat supports SSH agent for encrypted private keys, allowing password-free authentication when your SSH key is already loaded in the agent.

How it works:

When you provide an encrypted SSH key via --key, ascii-chat automatically checks if that specific key is available in the SSH agent:

# 1. Start SSH agent (if not already running)
eval "$(ssh-agent -s)"
# 2. Add your encrypted key to the agent (prompts for password ONCE)
ssh-add ~/.ssh/id_ed25519
Enter passphrase for /Users/you/.ssh/id_ed25519: [password]
Identity added: /Users/you/.ssh/id_ed25519
# 3. Start server or client using the key - uses ssh-agent; NO password prompt!
ascii-chat server --key ~/.ssh/id_ed25519
# or
ascii-chat client --key ~/.ssh/id_ed25519
INFO: Using SSH agent for this key (agent signing + ephemeral encryption)
INFO: SSH agent mode: Will use agent for identity signing, ephemeral X25519 for encryption
# There was no password prompt because the key was decrypted in the ssh-agent already.

If the key is not in the ssh-agent and ssh-agent is running, ascii-chat will prompt for the password and decrypt the key and store it in the ssh-agent securely for future use. This is done via ssh-agent's official protocol and is safe for production use - this is the intended usage of ssh-agent.

GitHub/GitLab Key Fetching

Fetch SSH keys from public profiles:

# Server: Use your GitHub SSH key
ascii-chat server --key github:zfogg
# Client: Verify server using GitHub profile
ascii-chat client --server-key github:zfogg

How it works:

  1. HTTPS fetch: GET https://github.com/zfogg.keys (using BearSSL)
  2. Parse response: Extract ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... lines
  3. Filter: Only accept Ed25519 keys (RSA/ECDSA rejected)
  4. Use first key: If multiple Ed25519 keys, use the first one

Security properties:

  • ✅ TLS-protected fetch (BearSSL verifies GitHub's certificate)
  • ✅ Public keys are not secret (safe to fetch over network)
  • ✅ Social proof: Attacker must compromise GitHub account
  • ⚠️ Trust GitHub's key infrastructure

GitLab support: Same mechanism, https://gitlab.com/username.keys

SSH Ed25519 Keys

ascii-chat can use existing SSH Ed25519 keys for authentication (identity proof via signatures):

# Server: Use SSH private key for identity
ascii-chat server --key ~/.ssh/id_ed25519
# Client: Verify server's SSH public key
ascii-chat client --server-key ~/.ssh/server_id_ed25519.pub

Key file formats supported:

  • OpenSSH native format (BEGIN OPENSSH PRIVATE KEY)
  • OpenSSH public key format (ssh-ed25519 AAAAC3...)
  • Encrypted private keys (prompts for passphrase)

Ed25519 to X25519 conversion:

ascii-chat can convert Ed25519 keys to X25519 format for compatibility, but does NOT use the converted key for encryption:

// Public key: Ed25519 (signing) → X25519 (for compatibility only)
crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk);
// Private key: Ed25519 (signing) → X25519 (NEVER used for encryption)
crypto_sign_ed25519_sk_to_curve25519(x25519_sk, ed25519_sk);

Why this works: Both Ed25519 and X25519 use the same underlying curve (Curve25519). Ed25519 uses the Edwards form for signing, X25519 uses the Montgomery form for DH. libsodium provides safe conversion functions.

Security architecture:

  • SSH keys used for: Identity authentication (Ed25519 signatures only)
  • Ephemeral keys used for: Encryption (X25519 Diffie-Hellman)
  • Result: Forward secrecy + strong authentication

The SSH key proves identity through signatures. The ephemeral keys provide encryption with forward secrecy. The signature binds them together cryptographically, preventing MITM attacks while maintaining forward secrecy.

How it works:

Handshake Protocol:
1. Server generates ephemeral X25519 keypair (random, per-connection)
2. Server signs ephemeral public key with long-term Ed25519 key
3. Server sends: [ephemeral_pk][identity_pk][signature]
4. Client verifies signature (proves server has identity key)
5. Client uses ephemeral_pk for encryption (forward secrecy)

The signature cryptographically binds the ephemeral encryption key to the long-term identity key. This provides:

  • ✅ Strong authentication (server must possess identity private key)
  • ✅ Forward secrecy (ephemeral keys are destroyed after session)
  • ✅ MITM protection (signature prevents key substitution attacks)

Alternative considered and rejected:

Using Ed25519→X25519 conversion for encryption would be simpler code-wise, but:

  • ❌ Breaks forward secrecy (recorded traffic can be decrypted if key is compromised later)
  • ❌ Inconsistent security (SSH agent mode would still need ephemeral keys)
  • ❌ Deviates from SSH protocol best practices

Result: All modes use ephemeral encryption keys. SSH keys are authentication-only.

SSH Agent Details

Key detection algorithm:

// lib/crypto/keys.c:823-844
// When parsing an encrypted private key:
if (is_encrypted) {
// Extract the public key from the encrypted key file
uint8_t embedded_public_key[32];
// Check if THIS SPECIFIC key is in SSH agent (not just any Ed25519 key)
bool ssh_agent_has_key = ssh_agent_has_specific_key(embedded_public_key);
// Mode 1: SSH agent mode
key_out->type = KEY_TYPE_ED25519;
key_out->use_ssh_agent = true;
memcpy(key_out->public_key, embedded_public_key, 32);
// Use agent for identity signing, ephemeral X25519 for encryption
return 0;
} else {
// Mode 2: Password prompt (agent doesn't have this key)
// Prompts for passphrase and decrypts key
}
}
bool ssh_agent_has_key(const public_key_t *public_key)
Check if a public key is already in ssh-agent.
Definition ssh_agent.c:90
@ KEY_TYPE_ED25519
Definition key_types.h:52

SSH agent signing protocol:

When use_ssh_agent = true, all Ed25519 signatures are delegated to the SSH agent:

// lib/crypto/keys.c:1318-1494
int ed25519_sign_message(const private_key_t *key, const uint8_t *message,
size_t message_len, uint8_t signature[64]) {
if (key->use_ssh_agent) {
// 1. Connect to SSH agent Unix socket ($SSH_AUTH_SOCK)
int agent_fd = connect_to_agent();
// 2. Build SSH_AGENTC_SIGN_REQUEST (type 13)
// [pubkey_blob][data_to_sign][flags]
uint8_t request[...];
send(agent_fd, request);
// 3. Receive SSH_AGENT_SIGN_RESPONSE (type 14)
// [signature_blob: "ssh-ed25519" + 64-byte signature]
uint8_t response[...];
recv(agent_fd, response);
// 4. Extract 64-byte Ed25519 signature
memcpy(signature, response + offset, 64);
return 0;
} else {
// Use in-memory Ed25519 key to sign
crypto_sign_detached(signature, NULL, message, message_len, key->key.ed25519);
return 0;
}
}
bool use_ssh_agent
Definition key_types.h:97
uint8_t ed25519[64]
Definition key_types.h:94
asciichat_error_t ed25519_sign_message(const private_key_t *key, const uint8_t *message, size_t message_len, uint8_t signature[64])
Sign a message with Ed25519 (uses SSH agent if available, otherwise in-memory key)
Definition ssh_keys.c:1067
union private_key_t::@1 key
Private key structure (for server –ssh-key)
Definition key_types.h:91

Security architecture:

ascii-chat uses a separation of concerns design where SSH keys are ONLY used for authentication, never encryption:

Component SSH Agent Mode In-Memory Mode
Identity signing SSH agent (Ed25519) In-memory Ed25519
Encryption keys Ephemeral X25519 Ephemeral X25519
Private key storage None (agent-only) Decrypted in memory
Password required No (once in agent) Yes (every restart)
Forward secrecy ✅ YES ✅ YES

Why always ephemeral encryption?

Both modes use ephemeral X25519 keys for encryption to provide forward secrecy:

  1. Identity authentication: SSH key proves identity via Ed25519 signature
  2. Encryption: Ephemeral X25519 keys generated fresh per connection
  3. Cryptographic binding: Signature covers ephemeral key, proving possession of both
  4. Forward secrecy: If SSH key is compromised later, past sessions remain secure

This matches SSH protocol design: long-term keys for identity, ephemeral keys for encryption.

Handshake signature binding:

// Server proves: "I possess identity_Ed25519 AND I'm using ephemeral_X25519"
Server sends: [ephemeral_X25519:32][identity_Ed25519:32][signature:64]
where: signature = sign(identity_private_key, ephemeral_X25519)

The signature cryptographically binds the ephemeral encryption key to the long-term identity key, preventing MITM attacks while maintaining forward secrecy.

Fallback behavior:

# If key is encrypted but NOT in SSH agent:
ascii-chat server --key ~/.ssh/id_ed25519
Encrypted private key detected (cipher: aes256-ctr)
Key not in SSH agent, will prompt for password
Enter passphrase for /Users/you/.ssh/id_ed25519: [password]
Successfully decrypted key, parsing...
# If SSH agent isn't running:
ascii-chat server --key ~/.ssh/id_ed25519
ssh_agent_has_specific_key: SSH_AUTH_SOCK not set
Key not in SSH agent, will prompt for password

Environment variable:

SSH agent communication requires the SSH_AUTH_SOCK environment variable:

# Check if SSH agent is running
echo $SSH_AUTH_SOCK
/tmp/ssh-XXXXXX/agent.12345
# If not set, start agent:
eval "$(ssh-agent -s)"

Security benefits:

  • Password once per session - Add key to agent once, use many times
  • No plaintext passwords - Agent handles passphrase, applications never see it
  • Process isolation - Private key never leaves agent process
  • Forward secrecy - Ephemeral X25519 keys per connection
  • Transparent fallback - Works with or without agent

Limitations:

  • Unix-only - SSH agent uses Unix domain sockets (not available on Windows)
  • Signing only - Cannot use agent for X25519 DH operations
  • Session-scoped - Keys removed from agent on logout/reboot

Code locations:

Security guarantee: Both SSH agent mode and in-memory mode provide identical forward secrecy - ephemeral X25519 keys are used for encryption in all cases. The only difference is where signatures come from (agent vs in-memory).

Automatic SSH Agent Key Addition

Problem: Users with encrypted SSH keys had to manually add keys to ssh-agent, or enter their password repeatedly.

Solution: ascii-chat now automatically adds decrypted keys to ssh-agent after successful password entry, eliminating future password prompts.

How it works:

# First run with encrypted key (password required)
ascii-chat server --key ~/.ssh/id_ed25519
Encrypted private key detected (cipher: aes256-ctr)
Key not in SSH agent, will prompt for password
Enter passphrase for /Users/you/.ssh/id_ed25519: [enter password]
Successfully decrypted key, parsing...
[SSH Agent] Adding key to ssh-agent to avoid future password prompts...
[SSH Agent] ✓ Key successfully added to ssh-agent
INFO: Key added to ssh-agent - password won't be required again this session
# Second run (NO password prompt!)
ascii-chat server --key ~/.ssh/id_ed25519
INFO: Using SSH agent for this key (agent signing + ephemeral encryption)
# Server starts immediately - no password needed!

What happens automatically:

  1. User enters password - Decrypts SSH key successfully
  2. Key parsed - Validates Ed25519 format
  3. Check ssh-agent - Verifies $SSH_AUTH_SOCK is set
  4. Auto-add to agent - Uses the agent's pipe to add the key to the agent (Windows named pipe or Unix domain socket)
  5. Future runs - No password prompt (uses agent)

Implementation details:

The auto-add feature uses the SSH agent protocol directly via the pipe.h platform abstraction:

// lib/crypto/keys/ssh_keys.c:765-774
// After successfully decrypting the key from file:
// 1. Check if ssh-agent is available
log_debug("ssh-agent not available, skipping auto-add");
return ASCIICHAT_OK; // Non-fatal: key is already decrypted
}
// 2. Check if key is already in agent (to avoid duplicate adds)
public_key_t pub_key = {0};
memcpy(pub_key.key, decrypted_key + 32, 32); // Extract public key
if (ssh_agent_has_key(&pub_key)) {
log_info("Key already in ssh-agent - skipping auto-add");
return ASCIICHAT_OK;
}
// 3. Add decrypted key to agent using SSH agent protocol
log_info("Attempting to add decrypted key to ssh-agent");
asciichat_error_t agent_result = ssh_agent_add_key(key_out, key_path);
if (agent_result == ASCIICHAT_OK) {
log_info("Successfully added key to ssh-agent - password will not be required on next run");
} else {
// Non-fatal: key is already decrypted and loaded
log_warn("Failed to add key to ssh-agent (non-fatal): %s", asciichat_error_string(agent_result));
}
bool ssh_agent_is_available(void)
Check if ssh-agent is running and available.
Definition ssh_agent.c:52
asciichat_error_t ssh_agent_add_key(const private_key_t *private_key, const char *key_path)
Add a private key to ssh-agent.
Definition ssh_agent.c:185
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ASCIICHAT_OK
Definition error_codes.h:48
uint8_t key[32]
Definition key_types.h:71
key_type_t type
Definition key_types.h:70
#define log_warn(...)
Log a WARN message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
Public key structure.
Definition key_types.h:69

Platform abstraction:

The implementation uses lib/platform/pipe.h for cross-platform agent communication:

  • POSIX (Linux/macOS): Uses Unix domain sockets via SSH_AUTH_SOCK environment variable
    • Path format: /tmp/ssh-XXXXXX/agent.XXXXXX (Unix socket)
    • Implementation: socket(AF_UNIX, SOCK_STREAM) + connect()
  • Windows: Uses named pipes via SSH_AUTH_SOCK environment variable or default path
    • Path format: \\.\pipe\openssh-ssh-agent (named pipe)
    • Implementation: CreateFileA() with GENERIC_READ | GENERIC_WRITE

Agent communication flow:

// lib/crypto/ssh_agent.c: ssh_agent_open_pipe()
// 1. Get agent path from environment or use default
const char *auth_sock = SAFE_GETENV("SSH_AUTH_SOCK");
#ifdef _WIN32
// Windows: Use named pipe path (default or from SSH_AUTH_SOCK)
const char *pipe_path = (auth_sock && strlen(auth_sock) > 0)
? auth_sock
: "\\\\.\\pipe\\openssh-ssh-agent";
return platform_pipe_connect(pipe_path); // CreateFileA() to named pipe
#else
// POSIX: Use Unix domain socket from SSH_AUTH_SOCK
if (!auth_sock || strlen(auth_sock) == 0) {
}
return platform_pipe_connect(auth_sock); // socket() + connect() to Unix socket
#endif
// 2. Send SSH agent protocol messages via platform_pipe_read/platform_pipe_write
// - SSH2_AGENTC_REQUEST_IDENTITIES (11) - List keys
// - SSH2_AGENTC_ADD_IDENTITY (17) - Add key
// - SSH_AGENT_SUCCESS (6) - Success response
// 3. Close connection when done
#define SAFE_GETENV(name)
Definition common.h:378
pipe_t platform_pipe_connect(const char *path)
Connect to an agent via named pipe (Windows) or Unix socket (POSIX)
int platform_pipe_close(pipe_t pipe)
Close a pipe connection.
#define INVALID_PIPE_VALUE
Invalid pipe value (POSIX: -1)
Definition pipe.h:42

Security considerations:

  • Direct protocol communication - No shell scripts or temporary files
  • Memory zeroed - Private key material cleared with sodium_memzero() after use
  • Agent session-scoped - Key removed from agent on logout/reboot
  • Idempotent operation - Adding same key multiple times is safe (agent deduplicates)
  • Non-fatal failures - Agent add failures don't prevent key decryption/use

Platform support:

Platform Supported Notes
Linux ✅ YES Requires SSH agent running (check $SSH_AUTH_SOCK)
macOS ✅ YES Built-in with macOS (ssh-agent auto-starts)
Windows ✅ YES Uses Windows named pipes (requires OpenSSH for Windows)

Code locations:

  • lib/crypto/keys/ssh_keys.c:242 - Check if key already in agent (before decryption)
  • lib/crypto/keys/ssh_keys.c:767 - Auto-add implementation after successful decryption
  • lib/crypto/ssh_agent.h - SSH agent interface declarations
  • lib/crypto/ssh_agent.c - SSH agent protocol implementation
  • lib/platform/pipe.h - Cross-platform pipe/agent socket abstraction
  • lib/platform/posix/pipe.c - POSIX Unix domain socket implementation
  • lib/platform/windows/pipe.c - Windows named pipe implementation

GPG Key Support

ascii-chat supports GPG Ed25519 keys via gpg-agent integration:

# Server with GPG key (short key ID - 8 hex chars)
ascii-chat server --key gpg:ABCD1234
# Server with GPG key (long key ID - 16 hex chars)
ascii-chat server --key gpg:1234567890ABCDEF
# Server with GPG key (full fingerprint - 40 hex chars)
ascii-chat server --key gpg:1234567890ABCDEF1234567890ABCDEF12345678
# Client verifying server GPG key
ascii-chat client --server-key gpg:1234567890ABCDEF1234567890ABCDEF12345678

Requirements:

  • gpg binary must be in PATH
  • GPG key must be Ed25519 (Curve 25519) type
  • gpg-agent must be running for signing operations

Key ID formats:

  • Short key ID: 8 hex characters (last 8 chars of fingerprint)
  • Long key ID: 16 hex characters (last 16 chars of fingerprint)
  • Full fingerprint: 40 hex characters (complete fingerprint)
  • Optional 0x prefix is accepted for all formats

How it works:

  1. Extract public key: gpg --list-keys + keygrip lookup
  2. Get Ed25519 key: Query gpg-agent via READKEY command (Assuan protocol)
  3. Sign challenges: gpg-agent signs via PKSIGN command (no passphrase prompt needed)
  4. Verify signatures: gpg --verify for deterministic Ed25519 signature verification

Graceful fallback:

ERROR: GPG key requested but 'gpg' command not found
Install GPG:
Ubuntu/Debian: apt-get install gnupg
macOS: brew install gnupg
Arch: pacman -S gnupg
Or use SSH keys: --key ~/.ssh/id_ed25519

Code locations:

  • lib/crypto/gpg.h - GPG interface declarations
  • lib/crypto/gpg.c - GPG protocol implementation and gpg-agent communication
  • lib/crypto/keys/gpg_keys.c - GPG key parsing and Ed25519 extraction
  • lib/crypto/keys/keys.c:167 - GPG key loading (keygrip stored for agent signing)

Known Hosts (IP-Based TOFU)

File location: ~/.ascii-chat/known_hosts

Format:

# ascii-chat Known Hosts
# Format: IP:port x25519 <hex-key> [comment]
# IPv4 example:
192.168.1.100:27224 x25519 a1b2c3d4e5f6... homeserver
10.0.0.50:8080 x25519 1234567890ab... office-server
# IPv6 example (bracket notation):
[2001:db8::1]:27224 x25519 fedcba098765... ipv6-server
[::1]:27224 x25519 abcdef123456... localhost-ipv6

Security Design: IP Binding (Not Hostnames)

ascii-chat binds server keys to resolved IP addresses, not DNS hostnames, for critical security reasons:

Why IP addresses?

  1. DNS Hijacking Prevention:
    • DNS responses can be spoofed or hijacked
    • Attacker points example.com → malicious server IP
    • With hostname binding: Attacker's server key accepted as example.com's key ❌
    • With IP binding: Client connects to attacker's IP, which won't match trusted IP ✅
  2. Cryptographic Binding:
    • TCP connection is to a specific IP address, not a hostname
    • Hostname is resolved once via getaddrinfo(), then IP is used
    • Binding key to IP matches actual network connection
  3. IPv6 Support:
    • Dual-stack servers accept IPv4 (192.0.2.1) and IPv6 (2001:db8::1)
    • Each IP:port combination gets its own key binding
    • Bracket notation [::1]:8080 clearly distinguishes IPv6

Example attack scenario (hostname binding):

1. User connects to example.com:27224
2. DNS resolves to 203.0.113.50 (attacker-controlled)
3. Attacker's server presents key_A
4. Client saves: "example.com:27224 → key_A"
5. Later, DNS changes to 198.51.100.25 (legitimate server)
6. Legitimate server presents key_B
7. Client sees hostname match, ACCEPTS key_B
8. No MITM detection! ❌

With IP binding (current implementation):

1. User connects to example.com:27224
2. DNS resolves to 203.0.113.50
3. Attacker's server presents key_A
4. Client saves: "203.0.113.50:27224 → key_A"
5. Later, example.com resolves to 198.51.100.25
6. Different IP! No key stored, prompts user
7. User realizes IP changed, investigates
8. MITM detected! ✅

Behavior:

  1. First connection: If server IP:port not in known_hosts, prompt user:
    The authenticity of host '192.168.1.100:27224' can't be established.
    Ed25519 key fingerprint is: SHA256:abc123...
    Are you sure you want to continue connecting (yes/no)? yes
  2. IPv6 first connection:
    The authenticity of host '[2001:db8::1]:27224' can't be established.
    Ed25519 key fingerprint is: SHA256:def456...
    Are you sure you want to continue connecting (yes/no)? yes
  3. Subsequent connections: Verify server key matches stored key → ABORT if mismatch
  4. Key change detected:
    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
    Connection ABORTED for security.
    To remove old key: sed -i '/192.168.1.100:27224 /d' ~/.ascii-chat/known_hosts

Trade-offs:

Aspect IP Binding (Current) Hostname Binding (SSH-style)
DNS Hijacking ✅ Protected ❌ Vulnerable
Server IP Change ⚠️ Prompts user (manual verification) ✅ Transparent
Dynamic DNS ⚠️ New prompt per IP ✅ Works seamlessly
Multi-homed Servers ⚠️ Separate entry per IP ✅ Single entry
Security Model Paranoid (explicit trust) Convenience (implicit trust)

Recommendation: The security benefit of IP binding outweighs the inconvenience of re-verification when server IPs change. For production servers, IP addresses should be stable (static IPs or fixed cloud instances).

Security model: "Trust on first use" (TOFU) with IP binding - assumes first connection to specific IP:port is legitimate, detects any changes in either IP or key thereafter.

Authentication Modes

Mode 1: Default (Ephemeral DH Only)

Server:

ascii-chat server

Client:

ascii-chat client

Security:

  • ✅ Privacy: All packets encrypted
  • ✅ Forward secrecy: New keys per connection
  • ❌ MITM vulnerable: No identity verification

Use case: Quick sessions, low-threat environments

Mode 2: Password Authentication

Server:

ascii-chat server --password mySecretPass123

Client:

ascii-chat client --password mySecretPass123

Security:

  • ✅ Privacy: All packets encrypted
  • ✅ MITM protection: Attacker must know password
  • ✅ Forward secrecy: DH keys still ephemeral
  • ⚠️ Password strength: Security depends on password quality

Mode 3: SSH/GPG Key Pinning

Server with SSH key:

ascii-chat server --key ~/.ssh/id_ed25519

Server with GPG key:

ascii-chat server --key gpg:897607FA43DC66F612710AF97FE90A79F2E80ED3

Client (verify using GitHub SSH key):

ascii-chat client --server-key github:zfogg

Client (verify using GitHub GPG key):

ascii-chat client --server-key github:zfogg.gpg

Client (verify using GitLab GPG key):

ascii-chat client --server-key gitlab:username.gpg

Client (verify using local GPG key):

ascii-chat client --server-key gpg:897607FA43DC66F612710AF97FE90A79F2E80ED3

Security:

  • ✅ Privacy: All packets encrypted
  • ✅ MITM protection: Cryptographically verified identity
  • ✅ Forward secrecy: DH keys still ephemeral
  • ✅ No shared secret: Public keys can be shared openly
  • ✅ GPG agent integration: No passphrase prompts during signing

Verification flow:

  1. Server sends Ed25519 identity key + signature
  2. Client fetches expected key (from GitHub, GPG keyring, or file)
  3. Client verifies signature: ed25519_verify(signature, ephemeral_key, identity_key)
  4. Client proceeds only if signature valid

Mode 4: Client Whitelisting

Server:

ascii-chat server --client-keys ~/.ssh/authorized_keys

Client:

# Client displays their public key on startup
ascii-chat client
# Output:
# Client public key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFoo... alice@laptop
# Share this key with the server operator to be whitelisted

Server's authorized_keys format:

# ascii-chat Authorized Keys
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFoo... alice@laptop
ssh-ed25519 AAAAB3NzaC1yc2EAAAADAQABAAABAQC... bob@desktop
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBar... carol@phone

Security:

  • ✅ Access control: Only pre-approved clients connect
  • ✅ Audit trail: Server logs all connection attempts with client keys
  • ✅ Revocable: Remove key from file to revoke access

GitHub/GitLab GPG Key Fetching:

Both --server-key (client verifies server) and --client-keys (server whitelists clients) support fetching GPG public keys from GitHub/GitLab:

# Client verifies server using GitHub GPG keys
ascii-chat client --server-key github:zfogg.gpg
# Server whitelists clients using GitHub GPG keys
ascii-chat server --key gpg:MYKEYID --client-keys github:zfogg.gpg
# GitLab works the same way
ascii-chat client --server-key gitlab:username.gpg
ascii-chat server --key gpg:MYKEYID --client-keys gitlab:username.gpg
# Full example: Client authenticates with GPG key, verifies server via GitHub
ascii-chat client --key gpg:897607FA43DC66F612710AF97FE90A79F2E80ED3 \
--server-key github:serveruser.gpg

How it works:

  1. Server fetches GPG armored public key block from https://github.com/username.gpg
  2. Imports all GPG keys using gpg --import (GitHub users often have multiple keys)
  3. Extracts full 40-character Ed25519 fingerprints using gpg --list-keys
  4. Adds all valid Ed25519 keys to client whitelist
  5. Client authenticates by signing challenge with their GPG key via gpg-agent

Why this is useful:

  • Social proof: Verify client identity via their public GitHub/GitLab profile
  • Zero configuration: No need to manually exchange public keys
  • Multi-key support: Automatically whitelists all user's Ed25519 GPG keys
  • No secrets: Only public keys are fetched; private keys stay in gpg-agent

Mode 5: Defense in Depth (All Features)

Server with SSH key:

ascii-chat server \
--key ~/.ssh/id_ed25519 \
--password myPass123 \
--client-keys ~/.ssh/authorized_keys

Server with GPG key:

ascii-chat server \
--key gpg:897607FA43DC66F612710AF97FE90A79F2E80ED3 \
--password myPass123 \
--client-keys ~/.ssh/authorized_keys

Client:

ascii-chat client \
--password myPass123 \
--server-key github:zfogg

Security:

  • ✅ Password authentication (both sides verify password)
  • ✅ SSH key pinning (client verifies server identity)
  • ✅ Client whitelist (server only accepts known clients)
  • ✅ Forward secrecy (ephemeral DH keys)
  • ✅ Defense in depth (multiple layers)

Mode 6: Opt-Out (No Encryption)

Server:

ascii-chat server --no-encrypt

Client:

ascii-chat client --no-encrypt

Security:

  • ❌ No protection whatsoever
  • ⚠️ All packets sent in plaintext

Use case: Debugging, packet inspection with tcpdump

WARNING: Only use in trusted networks or for development!

Password Derivation

Password validation:

// Password validation
if (strlen(password) < MIN_PASSWORD_LENGTH || strlen(password) > MAX_PASSWORD_LENGTH) {
log_error("Password length invalid (must be %d-%d characters)",
return -1;
}
// Argon2id parameters
uint8_t salt[32]; // Random, generated once
uint8_t key[32]; // Derived key
randombytes_buf(salt, sizeof(salt));
crypto_pwhash(
key, sizeof(key),
password, strlen(password),
salt,
crypto_pwhash_OPSLIMIT_INTERACTIVE, // 2 iterations
crypto_pwhash_MEMLIMIT_INTERACTIVE, // 64 MB
crypto_pwhash_ALG_ARGON2ID13
);
#define MIN_PASSWORD_LENGTH
Minimum password length (8 characters)
#define MAX_PASSWORD_LENGTH
Maximum password length (256 characters)
#define log_error(...)
Log an ERROR message.

HMAC challenge/response:

// Server generates challenge
uint8_t nonce[32];
randombytes_buf(nonce, sizeof(nonce));
// Client computes response (binds to shared_secret!)
uint8_t combined_data[64]; // nonce || shared_secret
memcpy(combined_data, nonce, 32);
memcpy(combined_data + 32, shared_secret, 32);
uint8_t hmac[32];
crypto_compute_hmac_ex(password_key, combined_data, 64, hmac);
// Server verifies (also binds to shared_secret)
crypto_verify_hmac_ex(password_key, combined_data, 64, hmac);
crypto_result_t crypto_compute_hmac_ex(const crypto_context_t *ctx, const uint8_t key[32], const uint8_t *data, size_t data_len, uint8_t hmac[32])
Compute HMAC-SHA256 for variable-length data.
bool crypto_verify_hmac_ex(const uint8_t key[32], const uint8_t *data, size_t data_len, const uint8_t expected_hmac[32])
Verify HMAC-SHA256 for variable-length data.

Critical security note: The HMAC binds both the nonce AND the DH shared_secret, preventing MITM attacks even if the attacker knows the password. See Issue 1: Password Mode MITM Vulnerability ⚠️ CRITICAL for details.

Packet Encryption

Encryption Process

For each packet after handshake:

// 1. Construct inner packet (real type, real payload)
packet_header_t inner_header = {
.magic = htonl(PACKET_MAGIC),
.type = htons(PACKET_TYPE_ASCII_FRAME), // Real type
.length = htonl(payload_len),
.crc32 = htonl(crc32(payload, payload_len)),
.client_id = htonl(client_id)
};
// 2. Combine inner header + payload
uint8_t plaintext[sizeof(inner_header) + payload_len];
memcpy(plaintext, &inner_header, sizeof(inner_header));
memcpy(plaintext + sizeof(inner_header), payload, payload_len);
// 3. Generate nonce (counter-based, never reused)
uint8_t nonce[24];
memcpy(nonce, ctx->session_id, 16); // Session ID (unique per connection)
*(uint64_t*)(nonce + 16) = htole64(ctx->nonce_counter++); // Increment counter
// 4. Encrypt with XSalsa20-Poly1305
uint8_t ciphertext[crypto_secretbox_MACBYTES + sizeof(plaintext)];
crypto_secretbox_easy(
ciphertext,
plaintext, sizeof(plaintext),
nonce,
shared_secret // 32-byte key from DH
);
// 5. Build encrypted packet: [nonce][ciphertext][MAC]
uint8_t encrypted_blob[24 + sizeof(ciphertext)];
memcpy(encrypted_blob, nonce, 24);
memcpy(encrypted_blob + 24, ciphertext, sizeof(ciphertext));
// 6. Send outer packet (type=ENCRYPTED)
packet_header_t outer_header = {
.magic = htonl(PACKET_MAGIC),
.type = htons(PACKET_TYPE_ENCRYPTED),
.length = htonl(sizeof(encrypted_blob)),
.crc32 = 0, // Not used for encrypted packets
.client_id = 0
};
send_packet(socket, &outer_header, encrypted_blob);
unsigned long long uint64_t
Definition common.h:59
uint32_t magic
Magic number (PACKET_MAGIC) for packet validation.
Definition packet.h:492
int send_packet(socket_t sockfd, packet_type_t type, const void *data, size_t len)
Send a basic packet without encryption.
Definition packet.c:754
#define PACKET_MAGIC
Packet magic number (0xDEADBEEF)
Definition packet.h:251
@ PACKET_TYPE_ASCII_FRAME
Complete ASCII frame with all metadata.
Definition packet.h:286

Wire format:

[Outer Header: 18 bytes]
magic: 0xDEADBEEF
type: 20 (ENCRYPTED)
length: 24 + N + 16
[Encrypted Blob: 24 + N + 16 bytes]
[Nonce: 24 bytes] ← Session ID (16) + Counter (8)
[Ciphertext: N bytes] ← XSalsa20(plaintext)
[MAC: 16 bytes] ← Poly1305(ciphertext)

Decryption Process

// 1. Receive outer packet
packet_header_t outer_header;
recv(socket, &outer_header, sizeof(outer_header));
// Verify outer header
if (ntohl(outer_header.magic) != PACKET_MAGIC) return -1;
if (ntohs(outer_header.type) != PACKET_TYPE_ENCRYPTED) return -1;
// 2. Read encrypted blob
uint32_t blob_len = ntohl(outer_header.length);
uint8_t encrypted_blob[blob_len];
recv(socket, encrypted_blob, blob_len);
// 3. Extract nonce and ciphertext
uint8_t nonce[24];
memcpy(nonce, encrypted_blob, 24);
uint8_t *ciphertext = encrypted_blob + 24;
size_t ciphertext_len = blob_len - 24;
// 4. Decrypt with XSalsa20-Poly1305
uint8_t plaintext[ciphertext_len - crypto_secretbox_MACBYTES];
if (crypto_secretbox_open_easy(
plaintext,
ciphertext, ciphertext_len,
nonce,
shared_secret) != 0) {
// MAC verification failed - packet was tampered with!
return -1;
}
// 5. Parse inner header
packet_header_t *inner_header = (packet_header_t*)plaintext;
uint16_t real_type = ntohs(inner_header->type);
uint32_t payload_len = ntohl(inner_header->length);
// 6. Extract payload
uint8_t *payload = plaintext + sizeof(packet_header_t);
// 7. Process packet based on real type
handle_packet(real_type, payload, payload_len);
uint32_t length
Payload data length in bytes (0 for header-only packets)
Definition packet.h:496
uint16_t type
Packet type (packet_type_t enumeration)
Definition packet.h:494

Nonce Management

Critical security requirement: Never reuse a nonce with the same key!

Implementation:

// Nonce format (24 bytes for XSalsa20-Poly1305):
// Bytes 0-15: Session ID (random, unique per connection/rekey)
// Bytes 16-23: Counter (increments per packet, little-endian uint64_t)
// Generate nonce for each packet
uint8_t nonce[24];
memcpy(nonce, ctx->session_id, 16); // Session ID prevents cross-session replay
*(uint64_t*)(nonce + 16) = htole64(ctx->nonce_counter++); // Counter prevents within-session replay

Why this is safe:

  • 24-byte nonce = 192 bits
  • 16-byte session ID = unique per connection/rekey
  • 8-byte counter = 2^64 = 18 quintillion packets per session
  • At 60 FPS video, counter lasts 9.7 trillion years
  • Session ID changes on rekey, providing additional safety

Replay protection:

  • Each side maintains their own send counter
  • Session ID prevents cross-session replay attacks
  • Received packets are not checked for sequence (UDP-like behavior)
  • Poly1305 MAC prevents tampering
  • Application-level sequence numbers in packet headers (not crypto-related)

Rekeying integration:

  • On rekey, session_id regenerated (16 random bytes)
  • Counter reset to 1
  • Prevents nonce reuse even if counters wrap (extremely unlikely)

Session Rekeying Protocol

Overview

ascii-chat implements automatic session rekeying to provide forward secrecy within long-lived connections. After the initial handshake establishes encryption, the system periodically performs new Diffie-Hellman key exchanges to rotate encryption keys.

Security benefits:

  • Intra-session forward secrecy - Compromise at time T doesn't decrypt packets before time T
  • Reduced cryptanalytic surface - Less ciphertext encrypted with same key
  • Industry best practice - Aligns with TLS 1.3 (rekeys at 2^24 records)
  • Protection against long-term key exposure - Limits damage from key compromise

Rekeying Triggers

Rekeying occurs automatically when either threshold is reached (whichever comes first):

Trigger Type Default Threshold Notes
Time-based 3600 seconds (1 hour) Typical video chat session length
Traffic-based 1,000,000 packets ~4.6 hours at 60 FPS video

Configuration options:

# Custom thresholds
./ascii-chat server --rekey-time 1800 --rekey-packets 500000
# Default thresholds (1 hour OR 1M packets)
./ascii-chat server

Test mode (for development): For testing, use low thresholds to trigger frequent rekeying:

# Rekey every 30 seconds OR 1000 packets
./ascii-chat server --rekey-time 30 --rekey-packets 1000

Rekeying Protocol Flow

The rekeying protocol uses a 3-packet handshake similar to the initial key exchange:

Client (Initiator) Server (Responder)
| |
| Trigger: Time/packet threshold |
| |
|-- CRYPTO_REKEY_REQUEST --------> |
| [new_ephemeral_pk: 32 bytes] |
| |
| Generate new ephemeral X25519 keypair |
| Compute shared_secret_new = DH(my_sk, their_pk) |
| |
|<-- CRYPTO_REKEY_RESPONSE ------> |
| [new_ephemeral_pk: 32 bytes] |
| |
| Compute shared_secret_new |
| |
|-- CRYPTO_REKEY_COMPLETE --> | (encrypted with NEW key)
| |
| ✅ Switch to new key |
| ✅ Reset nonce_counter = 1 |
| ✅ Generate new session_id |
| ✅ Wipe old shared_secret |
uint8_t session_id[16]

New Packet Types:

PACKET_TYPE_CRYPTO_REKEY_REQUEST = 25 // Initiator sends new ephemeral public key
PACKET_TYPE_CRYPTO_REKEY_RESPONSE = 26 // Responder sends new ephemeral public key
PACKET_TYPE_CRYPTO_REKEY_COMPLETE = 27 // Initiator confirms (encrypted with new key)
@ PACKET_TYPE_CRYPTO_REKEY_COMPLETE
Initiator -> Responder: Empty (encrypted with NEW key, but still handshake)
Definition packet.h:340
@ PACKET_TYPE_CRYPTO_REKEY_REQUEST
Initiator -> Responder: {new_ephemeral_pk[32]} (UNENCRYPTED during rekey)
Definition packet.h:336
@ PACKET_TYPE_CRYPTO_REKEY_RESPONSE
Responder -> Initiator: {new_ephemeral_pk[32]} (UNENCRYPTED during rekey)
Definition packet.h:338

Packet Structures:

REKEY_REQUEST:

typedef struct {
uint8_t new_ephemeral_public_key[32]; // Fresh X25519 public key
} rekey_request_packet_t;

REKEY_RESPONSE:

typedef struct {
uint8_t new_ephemeral_public_key[32]; // Fresh X25519 public key
} rekey_response_packet_t;

REKEY_COMPLETE:

// Empty packet (confirmation only)
// CRITICAL: Must be encrypted with NEW shared secret

Key Transition Logic

Before REKEY_COMPLETE:

  • All packets encrypted with old shared_secret
  • Keep old key in memory for decryption
  • Keep new key (temp_shared_key) ready for transition

REKEY_COMPLETE packet:

  • First packet encrypted with new key
  • Proves both sides successfully computed same shared secret
  • If decryption fails → abort rekey, keep old key

After REKEY_COMPLETE:

  • Copy temp_shared_key → shared_key
  • Reset nonce_counter = 1
  • Generate new random session_id (16 bytes)
  • Securely wipe old shared_secret and temp keys
  • Update rekey_last_time and reset rekey_packet_count

Security Features

Anti-Denial-of-Service:

// Rate limiting
#define REKEY_MIN_INTERVAL 60 // Minimum 60 seconds between rekeys
// Reject if rekey already in progress
if (ctx->rekey_in_progress) {
log_warn("Rekey already in progress, ignoring new request");
return -1;
}
// Exponential backoff on repeated failures
if (rekey_failure_count > 3) {
backoff_seconds = 60 * (1 << rekey_failure_count); // 60s, 120s, 240s, ...
}

Race Condition Handling: If both sides simultaneously initiate rekeying:

  • Lower client_id wins (becomes initiator)
  • Higher client_id becomes responder
  • Server has client_id=0, so server always wins ties
if (received_rekey_request && ctx->rekey_in_progress) {
// Both sides initiated simultaneously
if (my_client_id < peer_client_id) {
// I win - continue as initiator, ignore their request
log_debug("Rekey tie-break: I win (lower client_id)");
} else {
// They win - abort my rekey, become responder
log_debug("Rekey tie-break: They win, switching to responder");
handle_rekey_request(peer_request);
}
}
void crypto_rekey_abort(crypto_context_t *ctx)
Abort rekeying and fallback to old keys.

Key Confirmation: The REKEY_COMPLETE packet must decrypt successfully:

// Attempt to decrypt REKEY_COMPLETE with new key
if (crypto_decrypt_with_key(packet, temp_shared_key, plaintext) != 0) {
log_error("REKEY_COMPLETE decryption failed - aborting rekey");
crypto_rekey_abort(ctx); // Keep old key
return -1;
}
// Success - both sides have same key
crypto_rekey_commit(ctx); // Switch to new key
crypto_result_t crypto_rekey_commit(crypto_context_t *ctx)
Commit to new keys after successful REKEY_COMPLETE.

Failure Handling:

Rekey can fail for several reasons:

  1. Timeout - Peer doesn't respond within 10 seconds
  2. Bad keys - DH computation yields different secrets
  3. Decryption failure - REKEY_COMPLETE doesn't decrypt with new key
  4. Network error - Connection lost during rekey

Failure recovery:

log_warn("Aborting rekey, keeping old encryption key");
// Wipe temporary keys
sodium_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
sodium_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
sodium_memzero(ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
// Reset state
ctx->rekey_in_progress = false;
ctx->has_temp_key = false;
// Increment failure counter for backoff
// Continue using old key - no disruption to connection
}
Cryptographic context structure.
uint8_t temp_public_key[32]
uint8_t temp_private_key[32]
uint8_t temp_shared_key[32]

Important: Rekey failure does NOT disconnect the session. The connection continues with the old key.

Performance impact:

  • Rekey adds ~10ms latency (3 packets, DH computation)
  • Occurs once per hour (or less frequently)
  • Negligible impact on user experience

Security Considerations

Cryptographic Strengths

Modern primitives: X25519, XSalsa20-Poly1305, Argon2id (current best practices) ✅ Forward secrecy (connection-level): Ephemeral DH keys per connection ✅ Forward secrecy (session-level): Automatic rekeying within long-lived sessions ✅ Authenticated encryption: XSalsa20-Poly1305 AEAD prevents tampering ✅ Memory-hard passwords: Argon2id resistant to GPU brute-force ✅ Constant-time crypto: libsodium uses constant-time implementations (timing attack resistant) ✅ Large nonce space: 192-bit nonces make collision astronomically unlikely ✅ Intra-session forward secrecy: Rekeying limits exposure from key compromise

Potential Weaknesses

⚠️ Default MITM vulnerability:

  • Threat: Attacker intercepts initial handshake, performs two separate DH exchanges
  • Mitigation: Use --server-key or known_hosts verification
  • Acceptable because: User is warned, optional verification available

⚠️ Password quality:

  • Threat: Weak passwords vulnerable to offline dictionary attacks
  • Mitigation: Argon2id makes attacks expensive, but cannot prevent weak passwords
  • Best practice: Use long, random passwords (20+ characters)

⚠️ No quantum resistance:

  • Threat: Large quantum computers could break X25519 in the future
  • Timeline: Not a practical threat as of 2025
  • Future: Post-quantum algorithms (Kyber, Dilithium) may be added later

Known Vulnerabilities

Summary

All critical security vulnerabilities have been identified and fixed:

Issue Severity Status
Password Mode MITM 🔴 Critical FIXED - HMACs bound to DH shared_secret
No Mutual Auth 🔴 High FIXED - Bidirectional challenge-response
Replay Across Sessions 🟡 Medium FIXED - Session IDs implemented
No Whitelist Revocation 🟢 Low 🟢 Won't Fix - Out of scope, manual restart acceptable
TOFU Weakness 🟢 Info 🟢 Acceptable - Inherent to TOFU model
Code Duplication 🟢 Low FIXED - Shared authentication functions
Timing Attack on Keys 🔴 Critical FIXED - Constant-time sodium_memcmp()

Security Status:All security vulnerabilities have been addressed

CVE-None: Default MITM Vulnerability (By Design)

Severity: Medium (mitigated by user choice)

Description: By default, ascii-chat does not verify server identity. An attacker who controls the network can intercept the initial handshake and perform a man-in-the-middle attack.

Attack scenario:

  1. Client attempts to connect to server at 192.168.1.100:27224
  2. Attacker intercepts connection, poses as server to client
  3. Attacker poses as client to real server
  4. Attacker decrypts client packets, re-encrypts with server's key
  5. Result: Attacker sees all traffic

Why this is not a critical bug:

  1. Informed choice: This is the default because there's no global CA system
  2. User can verify: --server-key, --password, or known_hosts provide protection
  3. Active attack required: Attacker must control network (harder than passive eavesdropping)
  4. Similar to SSH: First SSH connection has same vulnerability (known_hosts helps thereafter)

Mitigation:

# Server: Use SSH key for identity
ascii-chat server --key ~/.ssh/id_ed25519
# Client: Verify server (pick one)
ascii-chat client --server-key github:zfogg # Fetch from GitHub
ascii-chat client --server-key ~/.ssh/server.pub # Manual verification
ascii-chat client # Will prompt to save to known_hosts

Potential Bug: Nonce Counter Overflow

Severity: Low (practically impossible)

Description: If a single connection sends more than 2^64 packets, the nonce counter wraps to zero, potentially reusing nonces.

Attack scenario:

  1. Keep connection alive for years
  2. Send packets at maximum rate (10,000/sec) for ~58 million years
  3. Nonce counter wraps to zero
  4. Nonces start repeating

Likelihood: Astronomically low (would require 58 million years of continuous packets)

Impact: Nonce reuse could allow an attacker to recover plaintext of two packets with the same nonce

Mitigation: Reconnect periodically (every 24 hours is more than sufficient)

Potential Bug: SSH Key Signature Bypass

Severity: High (if implementation bug exists)

Description: If signature verification is skipped when --server-key is set, attacker could send any identity key.

Attack scenario:

  1. Client connects with --server-key github:zfogg
  2. Attacker sends their own Ed25519 key in handshake
  3. BUG: Client doesn't verify signature of ephemeral key
  4. Client accepts attacker's identity key as valid

Current status: ✅ Not vulnerable (signature verification is mandatory)

Critical Security Review (Third-Party Analysis)

Note: This section documents security issues identified during independent review of the cryptographic implementation. These represent real vulnerabilities that should be addressed before production use.

Issue 1: Password Mode MITM Vulnerability ⚠️ <strong>CRITICAL</strong>

Severity: Critical (actively exploitable)

Description: Password mode was vulnerable to MITM attacks despite using encryption. An attacker could intercept the key exchange and derive the password-based key themselves because the password HMAC wasn't bound to the DH shared_secret.

Previous attack scenario:

Client Attacker Server
| | |
| --------KEY_EXCHANGE_INIT---------> | |
| | ------------KEY_EXCHANGE_INIT--------------> |
| | <--------------server_pubkey---------------- |
| <-------attacker_pubkey------------ | |
| | |
| Computes: HMAC(password_key, nonce) | |
| | Attacker can compute same HMAC with password |
| | Even though DH secrets differ! |

Root cause:

  • Password HMAC was computed as: HMAC(password_key, nonce)
  • This didn't bind the password to the DH exchange
  • Attacker who knows the password can MITM the DH exchange and still pass authentication

Status: 🟢 FIXED - Password HMACs now bound to DH shared_secret

Implementation: All password HMAC computations now bind to the DH shared_secret to prevent MITM:

// Client and server now both compute (for AUTH_RESPONSE):
uint8_t combined_data[64]; // nonce || shared_secret
memcpy(combined_data, nonce, 32);
memcpy(combined_data + 32, shared_secret, 32);
uint8_t hmac[32];
crypto_compute_hmac_ex(password_key, combined_data, 64, hmac);
// Server verifies (for SERVER_AUTH_RESPONSE):
uint8_t combined_data[64]; // client_nonce || shared_secret
memcpy(combined_data, client_challenge_nonce, 32);
memcpy(combined_data + 32, shared_secret, 32);
uint8_t server_hmac[32];
crypto_compute_hmac_ex(password_key, combined_data, 64, server_hmac);

Why this fixes the MITM vulnerability:

  • Client computes: HMAC(password_key, nonce || DH_secret_A)
  • Attacker with different DH secret computes: HMAC(password_key, nonce || DH_secret_B)
  • Server expects: HMAC(password_key, nonce || DH_secret_A)
  • Attacker's HMAC doesn't match → authentication fails

Code locations:

  • lib/crypto/crypto.h:168-172 - Added crypto_compute_hmac_ex() and crypto_verify_hmac_ex() for variable-length HMAC
  • lib/crypto/crypto.c:610-648 - Implemented extended HMAC functions
  • lib/crypto/handshake.c:569-582 - Client AUTH_RESPONSE binds to shared_secret
  • lib/crypto/handshake.c:644-657 - Client optional AUTH_RESPONSE binds to shared_secret
  • lib/crypto/handshake.c:706-719 - Client no-server-requirement AUTH_RESPONSE binds to shared_secret
  • lib/crypto/handshake.c:829-841 - Client SERVER_AUTH_RESPONSE verification binds to shared_secret
  • lib/crypto/handshake.c:890-902 - Server AUTH_RESPONSE verification binds to shared_secret
  • lib/crypto/handshake.c:962-974 - Server SERVER_AUTH_RESPONSE computation binds to shared_secret

Impact: Critical vulnerability fixed - Password mode now provides true MITM protection, not just passive eavesdropping protection

Issue 2: No Mutual Authentication in Default Mode ⚠️ <strong>HIGH</strong>

Severity: High (design flaw)

Description: The challenge-response protocol only authenticated the client to the server, not the other way around. Server never proved it has the shared secret.

Previous handshake flow:

Server Client
|----KEY_EXCHANGE_INIT--------->|
|<---KEY_EXCHANGE_RESPONSE------|
| |
| Derives: shared_secret | Derives: shared_secret
| |
|----AUTH_CHALLENGE: nonce----->|
|<---AUTH_RESPONSE: HMAC--------|
| |
| Verifies HMAC ✓ | Hopes server has key (no verification)
|----HANDSHAKE_COMPLETE-------->|

Problem: Client never received proof that server has the correct shared secret. A MITM attacker could:

  1. Intercept client's DH pubkey
  2. Generate their own server DH pubkey
  3. Send AUTH_CHALLENGE to client
  4. Client responds with HMAC (client is now authenticated)
  5. Attacker sends HANDSHAKE_COMPLETE (without ever proving they have the shared secret)

Status: 🟢 FIXED - Mutual authentication implemented

Implementation: The protocol now includes bidirectional challenge-response:

// New handshake flow with mutual authentication:
// 1. Server → Client: AUTH_CHALLENGE (server_nonce)
// 2. Client generates client_nonce
// 3. Client → Server: AUTH_RESPONSE (HMAC + client_nonce)
// - Password mode: HMAC(32) + client_nonce(32) = 64 bytes
// - Ed25519 mode: signature(64) + client_nonce(32) = 96 bytes
// 4. Server verifies client's HMAC/signature
// 5. Server computes HMAC(shared_secret, client_nonce)
// 6. Server → Client: AUTH_CONFIRM (server's HMAC of client_nonce)
// 7. Client verifies server's HMAC
// Both sides now authenticated

New packet type:

PACKET_TYPE_SERVER_AUTH_RESPONSE = 22 // Server → Client: HMAC(32 bytes)

Code locations:

  • lib/network.h:97 - Added PACKET_TYPE_SERVER_AUTH_RESPONSE
  • lib/crypto/handshake.c:593 - Client sends challenge nonce
  • lib/crypto/handshake.c:946 - Server sends AUTH_CONFIRM
  • lib/crypto/handshake.c:834 - Client verifies server's HMAC

Impact: High - Previously allowed MITM without server authentication, now both sides prove knowledge of shared secret

Issue 3: Replay Vulnerability Across Sessions ⚠️ <strong>MEDIUM</strong>

Severity: Medium (limited exploitation window)

Description: Nonce counter resets to 0 on each connection. An attacker who records packets from Session 1 can replay them into Session 2 if the same shared secret is used.

Attack scenario:

Session 1:
Client sends: encrypt(nonce=0, "start stream")
Client sends: encrypt(nonce=1, "video frame 1")
Attacker records these packets
Session 2 (client reconnects):
Nonce counter resets to 0
Attacker replays: encrypt(nonce=0, "start stream") ← Accepted!
Attacker replays: encrypt(nonce=1, "video frame 1") ← Accepted!

Root cause:

// crypto_context_init() in lib/crypto/crypto.c
ctx->send_nonce_counter = 1; // Resets to 1 every connection!

Status: 🟢 FIXED - Session IDs added to nonce generation

Implementation:

// Each connection gets a unique random session ID
randombytes_buf(ctx->session_id, 16);
// Nonce format: [session_id (16 bytes)][counter (8 bytes)]
SAFE_MEMCPY(nonce_out, 16, ctx->session_id, 16);
uint64_t counter = ctx->nonce_counter++;
SAFE_MEMCPY(nonce_out + 16, 8, &counter, 8);
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:388

This ensures nonces are unique across sessions even if counters reset.

Impact: Medium - Attacker can replay old packets into new sessions, but needs network position and recorded traffic

Issue 4: Whitelist Has No Revocation Mechanism ⚠️ <strong>LOW</strong>

Severity: Informational (not a security issue)

Description: If a client's SSH key is compromised, the server operator must manually edit the whitelist file and restart the server to revoke access.

Why hot-reload is not needed:

ascii-chat's security model does not require hot revocation:

  1. Server makes no assumptions about key compromise - The server's job is to enforce the whitelist at connection time, not to detect or respond to compromise
  2. Manual restart is acceptable - If an operator learns a client key is compromised, a simple server restart (2 seconds) is perfectly adequate
  3. Symmetric responsibility - Just as clients can choose not to connect to servers with compromised keys, operators can restart servers to remove compromised client keys
  4. Operator responsibility - Key management and compromise response are the operator's concern, not the server's

Revocation workflow (current and sufficient):

# 1. Remove compromised key from whitelist
vim ~/.ascii-chat/authorized_clients.txt
# 2. Restart server (takes ~2 seconds)
killall ascii-chat server
./ascii-chat server --client-keys ~/.ascii-chat/authorized_clients.txt
# Result: Compromised key can no longer connect

Why this is acceptable:

  • ✅ Simple and predictable
  • ✅ No additional complexity or attack surface
  • ✅ Matches SSH's authorized_keys model (also requires service restart for revocation)
  • ✅ If a key is compromised, a 2-second restart is not a meaningful security delay
  • ✅ Zero-downtime reload is overkill for a video chat application

Status: 🟢 Won't Fix - Out of Scope - Manual restart is the intended design

Impact: None - This is not a security issue, just an operational characteristic

Issue 5: Known Hosts TOFU Weakness ⚠️ <strong>INFORMATIONAL</strong>

Severity: Informational (inherent to TOFU model)

Description: Trust On First Use (TOFU) means the first connection is always vulnerable. If an attacker MITM's the first connection, their key is saved as "trusted".

This is the same vulnerability as:

  • SSH on first connection
  • HTTPS certificate pinning on first connection
  • Signal safety numbers on first message

Attack scenario:

User's first connection to server:
1. Attacker intercepts first connection
2. Attacker presents their own Ed25519 key
3. Client saves attacker's key to known_hosts
4. Future connections verify against attacker's key ✓
5. Real server's key is never seen

Why this is acceptable:

  1. Industry standard: SSH uses the same model
  2. User can verify: Out-of-band key fingerprint verification (QR code, voice call, etc.)
  3. Detectable: Key change triggers warning
  4. Better than nothing: Prevents MITM on all future connections

Status: 🟢 Acceptable by design (future verification server will improve this)

Impact: Informational - This is an accepted trade-off in the TOFU model

Issue 6: Code Duplication in Handshake Implementation ⚠️ <strong>TECHNICAL DEBT</strong>

Severity: Low (code quality issue, not a security vulnerability)

Description: The cryptographic protocol is symmetric - both client and server perform identical crypto operations - yet the code duplicates logic between crypto_client_handshake() and crypto_server_handshake().

Problem:

// lib/crypto/handshake.c has two nearly identical functions:
crypto_client_handshake() // Lines 416+
crypto_server_handshake() // Lines 772+
// Both compute identical crypto operations:
// - HMAC(password_key, nonce || shared_secret)
// - crypto_verify_hmac_ex(password_key, combined_data, 64, hmac)
// - Ed25519 signature generation/verification
// - DH key derivation

Status: 🟢 FIXED - Shared authentication functions implemented

Implementation: The password HMAC duplication has been eliminated by extracting shared functions:

// lib/crypto/crypto.c - New shared authentication helpers
const uint8_t nonce[32],
uint8_t hmac_out[32]);
const uint8_t nonce[32],
const uint8_t expected_hmac[32]);
crypto_result_t crypto_compute_auth_response(const crypto_context_t *ctx, const uint8_t nonce[32], uint8_t hmac_out[32])
Compute authentication response HMAC bound to DH shared_secret.
crypto_result_t
Cryptographic operation result codes.
bool crypto_verify_auth_response(const crypto_context_t *ctx, const uint8_t nonce[32], const uint8_t expected_hmac[32])
Verify authentication response HMAC bound to DH shared_secret.

Code locations:

  • lib/crypto/crypto.h:182-206 - Function declarations with documentation
  • lib/crypto/crypto.c:655-689 - Implementation of shared functions
  • lib/crypto/handshake.c:572 - Client AUTH_RESPONSE (3 locations) now use shared function
  • lib/crypto/handshake.c:815 - Client SERVER_AUTH_RESPONSE verification uses shared function
  • lib/crypto/handshake.c:862 - Server AUTH_RESPONSE verification uses shared function
  • lib/crypto/handshake.c:923 - Server SERVER_AUTH_RESPONSE computation uses shared function

Results:

  • ✅ ~35 lines of duplicated code eliminated
  • ✅ Single source of truth for HMAC computation and verification
  • ✅ Bug fixes now apply to all code paths automatically
  • ✅ Shared functions can be unit tested independently

Impact: Low - This is technical debt, not a security issue

Issue 7: Timing Attack on Public Key Comparisons ⚠️ <strong>CRITICAL</strong>

Severity: Critical (side-channel information leakage)

Description: Public key comparisons used variable-time memcmp() instead of constant-time comparison, allowing timing attacks that could leak information about cryptographic keys through side-channel analysis.

Vulnerability: The standard C library function memcmp() returns early on the first byte difference. This creates timing differences that can be measured by an attacker to learn information about the expected key.

// ❌ WRONG: memcmp() is NOT constant-time
if (memcmp(server_key, expected_key.key, 32) == 0) {
return 1; // Match
}

Attack scenario:

Attacker tries different server identity keys:
Attempt 1: Key starts with 0x00... → memcmp() fails on byte 0 → 10ns
Attempt 2: Key starts with 0xAB... → memcmp() fails on byte 0 → 10ns
Attempt 3: Key starts with 0xFF... → memcmp() succeeds on byte 0, fails on byte 1 → 11ns ✓
Attacker learns: First byte of expected key is 0xFF
Repeat for each byte to recover full 32-byte key

Root cause: Three locations used memcmp() for cryptographic key comparisons:

  1. lib/crypto/handshake.c:194 - Server identity verification during client key exchange
  2. lib/crypto/handshake.c:363 - Client whitelist verification during server auth challenge
  3. lib/crypto/known_hosts.c:69 - Known hosts verification (client-side server verification)

Why this is critical:

  • Timing attacks are practical - Measurable over network with enough samples
  • Leaks information about expected keys - Helps attacker forge identity
  • Affects authentication bypass - Compromise of identity verification
  • Applicable to all authentication modes - SSH keys, whitelists, known_hosts

Status: 🟢 FIXED - All comparisons now use constant-time sodium_memcmp()

Implementation: All cryptographic key comparisons now use libsodium's constant-time comparison:

// ✅ CORRECT: sodium_memcmp() is constant-time
// Compare server's IDENTITY key with expected key (constant-time to prevent timing attacks)
if (sodium_memcmp(server_identity_key, expected_key.key, 32) != 0) {
log_error("Server identity key mismatch - potential MITM attack!");
return -1;
}

Code locations:

  • lib/crypto/handshake.c:194 - ✅ Fixed: Server identity verification
  • lib/crypto/handshake.c:363 - ✅ Fixed: Client whitelist verification
  • lib/crypto/known_hosts.c:69 - ✅ Fixed: Known hosts verification

How sodium_memcmp() prevents timing attacks:

// From libsodium source (simplified):
int sodium_memcmp(const void *b1, const void *b2, size_t len) {
const unsigned char *c1 = b1;
const unsigned char *c2 = b2;
unsigned char d = 0;
// Always compares ALL bytes, never returns early
for (size_t i = 0; i < len; i++) {
d |= c1[i] ^ c2[i];
}
return (1 & ((d - 1) >> 8)) - 1; // Constant-time final comparison
}

Benefits of constant-time comparison:

  • No timing variation - Takes same time regardless of where keys differ
  • Cryptographically sound - Standard practice for key comparison
  • libsodium guarantee - Maintained by crypto experts
  • Zero performance cost - 32-byte comparison is <100ns either way

**Impact:** Critical vulnerability fixed - All cryptographic key comparisons now resistant to timing attacks

Future Enhancements

Post-Quantum Cryptography

**Current:** X25519 (not quantum-resistant)

**Future:** Hybrid key exchange with post-quantum algorithm

**Candidates:**

  • **Kyber:** NIST-selected post-quantum KEM (key encapsulation)
  • **Dilithium:** NIST-selected post-quantum signatures
  • **X25519-Kyber:** Hybrid combining classical + post-quantum
  • **Ed25519-Dilithium:** Hybrid signature algorithms

The dynamic algorithm negotiation system is designed to support post-quantum migration when libsodium adds support.

Verification Server (Planned - See Issue #82)

**Problem:** No global certificate authority like HTTPS

**Proposed solution:** Optional verification server for key registry

**Architecture:**

┌─────────┐
│ Client │────┐
└─────────┘ │
├─→ [Verification Server] ←─── Stores public keys
┌─────────┐ │ - Users register keys
│ Server │────┘ - Provides key lookup
└─────────┘ - Issues signed certificates

**Benefits:**

  • ✅ Verified identity without pre-sharing keys
  • ✅ Revocation support (server marks keys as invalid)
  • ✅ Discovery service integration (session strings + verification)

**Trust model:** Similar to Signal's key server - optional, transparency log, user can verify out-of-band

**Status:** RFC in progress (see issue #82 for detailed spec)

Post-Quantum Cryptography Details

**Current:** X25519 (not quantum-resistant)

**Future:** Hybrid key exchange with post-quantum algorithm

**Candidates:**

  • **Kyber:** NIST-selected post-quantum KEM (key encapsulation)
  • **Dilithium:** NIST-selected post-quantum signatures
  • **X25519-Kyber:** Hybrid combining classical + post-quantum
  • **Ed25519-Dilithium:** Hybrid signature algorithms

**Implementation Strategy:** The dynamic algorithm negotiation system is designed to support post-quantum migration:

  1. **Phase 1**: Add post-quantum algorithms to capabilities
    #define KEX_ALGO_KYBER1024 0x02
    #define AUTH_ALGO_DILITHIUM3 0x02
    #define KEX_ALGO_X25519_KYBER_HYBRID 0x03
    #define AUTH_ALGO_ED25519_DILITHIUM_HYBRID 0x03
  2. **Phase 2**: Prefer hybrid mode when both sides support it
    • Client capabilities include both classical and post-quantum
    • Server selects hybrid algorithms when available
    • Fallback to classical-only for older clients
  3. **Phase 3**: Gradual migration
    • New clients prefer post-quantum
    • Legacy clients continue working
    • Server can enforce post-quantum for sensitive applications

**Timeline:** Wait for libsodium to add post-quantum support (currently in development)

**Backward compatibility:** Hybrid mode allows gradual migration without breaking existing connections

Session Rekeying (IMPLEMENTED ✅)

**Status:** ✅ **IMPLEMENTED** - See Session Rekeying Protocol section above

**Implementation:**

  • Automatic rekeying based on time (1 hour) or traffic (1M packets)
  • Full DH re-exchange with new ephemeral keys
  • Intra-session forward secrecy

**Future consideration:** Double Ratchet (Signal-style per-message rekeying)

  • **Benefit:** Forward secrecy per message
  • **Cost:** Massive complexity, state synchronization issues
  • **Decision:** Current session rekeying is sufficient for video chat use case

Certificate Transparency Log

**Inspired by:** Let's Encrypt Certificate Transparency

**Idea:** Public append-only log of all server public keys

**Benefits:**

  • Detect rogue keys (someone impersonating your server)
  • Audit trail of key changes
  • Social accountability (anyone can verify log integrity)

**Implementation:**

  • Merkle tree of Ed25519 public keys
  • Signed by verification server
  • Clients can request inclusion proofs

**Status:** Research phase

Appendix: Cryptography Bugs to Watch For

Bug Class 1: Nonce Reuse

**Danger:** Reusing a nonce with the same key breaks XSalsa20-Poly1305 security

**How it happens:**

// ❌ WRONG: Same nonce for multiple packets
uint8_t nonce[24] = {0}; // Never changes!
for (int i = 0; i < 100; i++) {
crypto_secretbox_easy(ciphertext, plaintext, nonce, key); // Same nonce!
}

**Fix:**

// ✅ CORRECT: Increment nonce counter
uint64_t counter = 1;
for (int i = 0; i < 100; i++) {
uint8_t nonce[24] = {0};
*(uint64_t*)nonce = htole64(counter++); // Unique nonce per packet
crypto_secretbox_easy(ciphertext, plaintext, nonce, key);
}

**Test:** Verify nonce increments in packet capture

Bug Class 2: Timing Attacks in Verification

**Danger:** Variable-time comparison leaks information

**How it happens:**

// ❌ WRONG: memcmp() returns early on first mismatch
if (memcmp(expected_hmac, received_hmac, 32) == 0) {
// Attacker can measure timing differences to guess HMAC
}

**Fix:**

// ✅ CORRECT: Constant-time comparison
if (crypto_verify_32(expected_hmac, received_hmac) == 0) {
// Takes same time regardless of mismatch location
}

**Test:** Run verification 10,000 times with random HMACs, verify timing is consistent

Bug Class 3: Signature Bypass

**Danger:** Skipping signature verification allows impersonation

**How it happens:**

// ❌ WRONG: Assuming presence of signature field means it's valid
if (packet_len == 128) {
// Has signature field, assume it's correct
identity_key = packet->identity_key;
}

Fix:

// ✅ CORRECT: Always verify signature if present
if (packet_len == 128) {
if (ed25519_verify_signature(identity_key, ephemeral_key, signature) != 0) {
return -1; // REJECT invalid signature
}
}
asciichat_error_t ed25519_verify_signature(const uint8_t public_key[32], const uint8_t *message, size_t message_len, const uint8_t signature[64], const char *gpg_key_id)
Verify an Ed25519 signature.
Definition ssh_keys.c:1163

Test: Send handshake with random signature bytes → should be rejected

Bug Class 4: Replay Attacks

Danger: Old packets can be re-sent by attacker

How it happens:

// ❌ WRONG: No replay protection
decrypt_packet(ciphertext, plaintext);
process_packet(plaintext); // Attacker can replay old encrypted packets

Fix:

// ✅ CORRECT: Check sequence numbers or use nonces
if (packet->sequence_number <= last_seen_sequence) {
return -1; // REJECT replayed packet
}

Note: ascii-chat uses nonce-based encryption which provides implicit replay resistance (same nonce won't decrypt to same plaintext due to MAC)

Bug Class 5: Key Confusion

Danger: Using wrong key for operation

How it happens:

// ❌ WRONG: Using password key when DH key should be used
if (password_set) {
crypto_secretbox_easy(ciphertext, plaintext, nonce, password_key);
} else {
crypto_secretbox_easy(ciphertext, plaintext, nonce, shared_secret);
}
// Attacker can cause decrypt with wrong key by manipulating password flag

Fix:

// ✅ CORRECT: Consistent key selection logic
uint8_t *encryption_key = (key_exchange_complete && shared_secret_valid)
? shared_secret
: password_key;
crypto_secretbox_easy(ciphertext, plaintext, nonce, encryption_key);

Test: Verify encrypted packets with password can't be decrypted with DH key and vice versa

References

Document Version: 2.3 Last Updated: October 2025 (Session rekeying protocol implemented) Maintainer: ascii-chat Development Team License: Same as ascii-chat project (see LICENSE)

See also
topic_crypto "Cryptography Module" for module API documentation
topic_keys "Keys Module" for detailed key handling
topic_handshake "Handshake Protocol" for Handshake Module details

Macro Definition Documentation

◆ AES256_DERIVED_SIZE

#define AES256_DERIVED_SIZE   (AES256_KEY_SIZE + AES_IV_SIZE)

#include <crypto.h>

AES-256 key + IV derived size in bytes (for bcrypt_pbkdf)

Definition at line 112 of file lib/crypto/crypto.h.

◆ AES256_KEY_SIZE

#define AES256_KEY_SIZE   32

#include <crypto.h>

AES-256 key size in bytes.

Definition at line 108 of file lib/crypto/crypto.h.

◆ AES_IV_SIZE

#define AES_IV_SIZE   16

#include <crypto.h>

AES initialization vector (IV) size in bytes.

Definition at line 110 of file lib/crypto/crypto.h.

◆ ANCHOR_LIST_INIT

#define ANCHOR_LIST_INIT   {NULL, 0, 0}

#include <pem_utils.h>

Initializer for anchor_list.

Macro for initializing an empty anchor_list structure. Sets all fields to zero/NULL.

Definition at line 93 of file pem_utils.h.

◆ ARGON2ID_SALT_SIZE

#define ARGON2ID_SALT_SIZE   32

#include <crypto.h>

Argon2id salt size in bytes.

Definition at line 104 of file lib/crypto/crypto.h.

◆ AUTH_CHALLENGE_FLAGS_SIZE

#define AUTH_CHALLENGE_FLAGS_SIZE   1

#include <crypto.h>

Authentication flags size (1 byte)

Definition at line 174 of file lib/crypto/crypto.h.

◆ AUTH_CHALLENGE_PACKET_SIZE

#define AUTH_CHALLENGE_PACKET_SIZE   (AUTH_CHALLENGE_FLAGS_SIZE + AUTH_CHALLENGE_SIZE)

#include <crypto.h>

Complete authentication challenge packet size (1 + 32 = 33 bytes)

Definition at line 176 of file lib/crypto/crypto.h.

◆ AUTH_CHALLENGE_SIZE

#define AUTH_CHALLENGE_SIZE   32

#include <crypto.h>

Challenge nonce size (32 bytes)

Definition at line 158 of file lib/crypto/crypto.h.

◆ AUTH_COMBINED_SIZE

#define AUTH_COMBINED_SIZE   (AUTH_HMAC_SIZE + AUTH_CHALLENGE_SIZE)

#include <crypto.h>

Combined authentication data size (HMAC + challenge, 64 bytes)

Definition at line 160 of file lib/crypto/crypto.h.

◆ AUTH_HMAC_SIZE

#define AUTH_HMAC_SIZE   32

#include <crypto.h>

HMAC size in authentication packets (32 bytes)

Definition at line 156 of file lib/crypto/crypto.h.

◆ AUTH_RESPONSE_PASSWORD_SIZE

#define AUTH_RESPONSE_PASSWORD_SIZE   (AUTH_HMAC_SIZE + AUTH_CHALLENGE_SIZE)

#include <crypto.h>

Password-based authentication response size (HMAC + challenge, 64 bytes)

Definition at line 186 of file lib/crypto/crypto.h.

◆ AUTH_RESPONSE_SIGNATURE_SIZE

#define AUTH_RESPONSE_SIGNATURE_SIZE   (AUTH_SIGNATURE_SIZE + AUTH_CHALLENGE_SIZE)

#include <crypto.h>

Signature-based authentication response size (signature + challenge, 96 bytes)

Definition at line 188 of file lib/crypto/crypto.h.

◆ AUTH_SIGNATURE_COMBINED_SIZE

#define AUTH_SIGNATURE_COMBINED_SIZE   (AUTH_SIGNATURE_SIZE + AUTH_CHALLENGE_SIZE)

#include <crypto.h>

Combined signature + challenge size (96 bytes)

Definition at line 164 of file lib/crypto/crypto.h.

◆ AUTH_SIGNATURE_SIZE

#define AUTH_SIGNATURE_SIZE   64

#include <crypto.h>

Ed25519 signature size (64 bytes)

Definition at line 162 of file lib/crypto/crypto.h.

◆ CRYPTO_ED25519_PRIVATE_KEY_SIZE

#define CRYPTO_ED25519_PRIVATE_KEY_SIZE   ED25519_PRIVATE_KEY_SIZE

#include <crypto.h>

Ed25519 private key size.

Definition at line 134 of file lib/crypto/crypto.h.

◆ CRYPTO_ED25519_PUBLIC_KEY_SIZE

#define CRYPTO_ED25519_PUBLIC_KEY_SIZE   ED25519_PUBLIC_KEY_SIZE

#include <crypto.h>

Ed25519 public key size.

Definition at line 132 of file lib/crypto/crypto.h.

◆ CRYPTO_ED25519_SIGNATURE_SIZE

#define CRYPTO_ED25519_SIGNATURE_SIZE   ED25519_SIGNATURE_SIZE

#include <crypto.h>

Ed25519 signature size.

Definition at line 136 of file lib/crypto/crypto.h.

◆ CRYPTO_ENCRYPTION_KEY_SIZE

#define CRYPTO_ENCRYPTION_KEY_SIZE   SECRETBOX_KEY_SIZE

#include <crypto.h>

Encryption key size (XSalsa20-Poly1305)

Definition at line 142 of file lib/crypto/crypto.h.

◆ CRYPTO_FINGERPRINT_SIZE

#define CRYPTO_FINGERPRINT_SIZE   32

#include <crypto.h>

Ed25519 key fingerprint size in bytes.

SHA-256 hash of the key, resulting in 32 bytes.

Definition at line 380 of file lib/crypto/crypto.h.

◆ CRYPTO_HEX_KEY64_SIZE

#define CRYPTO_HEX_KEY64_SIZE   128

#include <crypto.h>

Hex string size for 64-byte key (128 hex characters)

Definition at line 396 of file lib/crypto/crypto.h.

◆ CRYPTO_HEX_KEY64_SIZE_NULL

#define CRYPTO_HEX_KEY64_SIZE_NULL   129

#include <crypto.h>

Hex string size for 64-byte key with null terminator (129 bytes)

Definition at line 398 of file lib/crypto/crypto.h.

◆ CRYPTO_HEX_KEY_SIZE

#define CRYPTO_HEX_KEY_SIZE   64

#include <crypto.h>

Hex string size for 32-byte key (64 hex characters)

Definition at line 392 of file lib/crypto/crypto.h.

◆ CRYPTO_HEX_KEY_SIZE_NULL

#define CRYPTO_HEX_KEY_SIZE_NULL   65

#include <crypto.h>

Hex string size for 32-byte key with null terminator (65 bytes)

Definition at line 394 of file lib/crypto/crypto.h.

◆ CRYPTO_HMAC_SIZE

#define CRYPTO_HMAC_SIZE   HMAC_SHA256_SIZE

#include <crypto.h>

HMAC size (HMAC-SHA256)

Definition at line 146 of file lib/crypto/crypto.h.

◆ CRYPTO_KEY_SIZE

#define CRYPTO_KEY_SIZE   32

#include <crypto.h>

Ed25519 key size in bytes.

Ed25519 public and private keys are 32 bytes each.

Definition at line 371 of file lib/crypto/crypto.h.

◆ CRYPTO_MAC_SIZE

#define CRYPTO_MAC_SIZE   POLY1305_MAC_SIZE

#include <crypto.h>

MAC size (Poly1305)

Definition at line 144 of file lib/crypto/crypto.h.

◆ CRYPTO_MAX_CIPHERTEXT_SIZE

#define CRYPTO_MAX_CIPHERTEXT_SIZE   (CRYPTO_MAX_PLAINTEXT_SIZE + CRYPTO_MAC_SIZE)

#include <crypto.h>

Maximum ciphertext size (plaintext + MAC, ~1MB + 16 bytes)

Definition at line 231 of file lib/crypto/crypto.h.

◆ CRYPTO_MAX_PLAINTEXT_SIZE

#define CRYPTO_MAX_PLAINTEXT_SIZE   ((size_t)1024 * 1024)

#include <crypto.h>

Maximum plaintext size (1MB)

Definition at line 229 of file lib/crypto/crypto.h.

◆ CRYPTO_NONCE_SIZE

#define CRYPTO_NONCE_SIZE   XSALSA20_NONCE_SIZE

#include <crypto.h>

Nonce size (XSalsa20)

Definition at line 138 of file lib/crypto/crypto.h.

◆ CRYPTO_PRIVATE_KEY_SIZE

#define CRYPTO_PRIVATE_KEY_SIZE   X25519_KEY_SIZE

#include <crypto.h>

Private key size (X25519)

Definition at line 128 of file lib/crypto/crypto.h.

◆ CRYPTO_PUBLIC_KEY_SIZE

#define CRYPTO_PUBLIC_KEY_SIZE   X25519_KEY_SIZE

#include <crypto.h>

Public key size (X25519)

Definition at line 126 of file lib/crypto/crypto.h.

◆ CRYPTO_SALT_SIZE

#define CRYPTO_SALT_SIZE   ARGON2ID_SALT_SIZE

#include <crypto.h>

Salt size (Argon2id)

Definition at line 140 of file lib/crypto/crypto.h.

◆ CRYPTO_SHARED_KEY_SIZE

#define CRYPTO_SHARED_KEY_SIZE   X25519_KEY_SIZE

#include <crypto.h>

Shared key size (X25519)

Definition at line 130 of file lib/crypto/crypto.h.

◆ ED25519_PRIVATE_KEY_SIZE

#define ED25519_PRIVATE_KEY_SIZE   64

#include <crypto.h>

Ed25519 private key size (seed + public) in bytes.

Definition at line 94 of file lib/crypto/crypto.h.

◆ ED25519_PUBLIC_KEY_SIZE

#define ED25519_PUBLIC_KEY_SIZE   32

#include <crypto.h>

Ed25519 public key size in bytes.

Definition at line 92 of file lib/crypto/crypto.h.

◆ ED25519_SIGNATURE_SIZE

#define ED25519_SIGNATURE_SIZE   64

#include <crypto.h>

Ed25519 signature size in bytes.

Definition at line 96 of file lib/crypto/crypto.h.

◆ HEX_STRING_SIZE_32

#define HEX_STRING_SIZE_32   (32 * 2 + 1)

#include <crypto.h>

Hex string size for 32-byte values (64 hex chars + null terminator)

Definition at line 213 of file lib/crypto/crypto.h.

◆ HEX_STRING_SIZE_64

#define HEX_STRING_SIZE_64   (64 * 2 + 1)

#include <crypto.h>

Hex string size for 64-byte values (128 hex chars + null terminator)

Definition at line 215 of file lib/crypto/crypto.h.

◆ HMAC_SHA256_SIZE

#define HMAC_SHA256_SIZE   32

#include <crypto.h>

HMAC-SHA256 output size in bytes.

Definition at line 102 of file lib/crypto/crypto.h.

◆ MAX_AUTH_FAILED_PACKET_SIZE

#define MAX_AUTH_FAILED_PACKET_SIZE   256

#include <crypto.h>

Maximum AUTH_FAILED packet size (256 bytes)

Definition at line 201 of file lib/crypto/crypto.h.

◆ MAX_COMMENT_LEN

#define MAX_COMMENT_LEN   256

#include <crypto.h>

Definition at line 433 of file lib/crypto/crypto.h.

◆ MAX_ENCRYPTED_PACKET_SIZE

#define MAX_ENCRYPTED_PACKET_SIZE   65536

#include <crypto.h>

Maximum encrypted packet size (64KB)

Definition at line 203 of file lib/crypto/crypto.h.

◆ MAX_GPG_KEYGRIP_LEN

#define MAX_GPG_KEYGRIP_LEN   64

#include <crypto.h>

Definition at line 434 of file lib/crypto/crypto.h.

◆ MAX_PASSWORD_LENGTH

#define MAX_PASSWORD_LENGTH   256

#include <crypto.h>

Maximum password length (256 characters)

Definition at line 80 of file lib/crypto/crypto.h.

◆ MIN_PASSWORD_LENGTH

#define MIN_PASSWORD_LENGTH   8

#include <crypto.h>

Minimum password length (8 characters)

Definition at line 78 of file lib/crypto/crypto.h.

◆ NO_IDENTITY_MARKER

#define NO_IDENTITY_MARKER   "no-identity"

#include <crypto.h>

No-identity entry marker ("no-identity")

Definition at line 416 of file lib/crypto/crypto.h.

◆ PASSWORD_BUFFER_SIZE

#define PASSWORD_BUFFER_SIZE   256

#include <crypto.h>

Password input buffer size (256 bytes)

Definition at line 217 of file lib/crypto/crypto.h.

◆ POLY1305_MAC_SIZE

#define POLY1305_MAC_SIZE   16

#include <crypto.h>

Poly1305 MAC size in bytes.

Definition at line 100 of file lib/crypto/crypto.h.

◆ REKEY_DEFAULT_PACKET_THRESHOLD

#define REKEY_DEFAULT_PACKET_THRESHOLD   1000000

#include <crypto.h>

Default rekey packet threshold (1 million packets)

Definition at line 1242 of file lib/crypto/crypto.h.

◆ REKEY_DEFAULT_TIME_THRESHOLD

#define REKEY_DEFAULT_TIME_THRESHOLD   3600

#include <crypto.h>

Default rekey time threshold (1 hour in seconds)

Definition at line 1240 of file lib/crypto/crypto.h.

◆ REKEY_MAX_FAILURE_COUNT

#define REKEY_MAX_FAILURE_COUNT   10

#include <crypto.h>

Maximum consecutive rekey failures before giving up.

Definition at line 1248 of file lib/crypto/crypto.h.

◆ REKEY_MIN_INTERVAL

#define REKEY_MIN_INTERVAL   3

#include <crypto.h>

Minimum time interval between rekey requests (3 seconds for testing, 60 for production)

Definition at line 1238 of file lib/crypto/crypto.h.

◆ REKEY_MIN_REQUEST_INTERVAL

#define REKEY_MIN_REQUEST_INTERVAL   60

#include <crypto.h>

Minimum interval between rekey requests (60 seconds, DDoS protection)

Definition at line 1250 of file lib/crypto/crypto.h.

◆ REKEY_TEST_PACKET_THRESHOLD

#define REKEY_TEST_PACKET_THRESHOLD   1000

#include <crypto.h>

Test mode rekey packet threshold (1000 packets)

Definition at line 1246 of file lib/crypto/crypto.h.

◆ REKEY_TEST_TIME_THRESHOLD

#define REKEY_TEST_TIME_THRESHOLD   30

#include <crypto.h>

Test mode rekey time threshold (30 seconds)

Definition at line 1244 of file lib/crypto/crypto.h.

◆ SECRETBOX_KEY_SIZE

#define SECRETBOX_KEY_SIZE   32

#include <crypto.h>

Secretbox key size in bytes.

Definition at line 106 of file lib/crypto/crypto.h.

◆ SERVER_AUTH_RESPONSE_SIZE

#define SERVER_AUTH_RESPONSE_SIZE   AUTH_HMAC_SIZE

#include <crypto.h>

Server authentication response size (32 bytes)

Definition at line 193 of file lib/crypto/crypto.h.

◆ SESSION_ID_SIZE

#define SESSION_ID_SIZE   16

#include <crypto.h>

Session ID size in bytes.

Definition at line 114 of file lib/crypto/crypto.h.

◆ SSH_ED25519_KEY_TYPE

#define SSH_ED25519_KEY_TYPE   "ssh-ed25519"

#include <crypto.h>

SSH Ed25519 key type string ("ssh-ed25519")

Definition at line 412 of file lib/crypto/crypto.h.

◆ SSH_KEY_HEADER_SIZE

#include <crypto.h>

Definition at line 357 of file lib/crypto/crypto.h.

◆ SSH_KEY_PERMISSIONS_MASK

#define SSH_KEY_PERMISSIONS_MASK   (S_IRWXG | S_IRWXO)

#include <crypto.h>

Definition at line 425 of file lib/crypto/crypto.h.

◆ SSH_KEY_PUBLIC_KEY_LENGTH_SIZE

#define SSH_KEY_PUBLIC_KEY_LENGTH_SIZE   4

#include <crypto.h>

Definition at line 355 of file lib/crypto/crypto.h.

◆ SSH_KEY_PUBLIC_KEY_SIZE

#define SSH_KEY_PUBLIC_KEY_SIZE   32

#include <crypto.h>

Definition at line 356 of file lib/crypto/crypto.h.

◆ SSH_KEY_RECOMMENDED_PERMISSIONS

#define SSH_KEY_RECOMMENDED_PERMISSIONS   0600

#include <crypto.h>

Definition at line 426 of file lib/crypto/crypto.h.

◆ SSH_KEY_TYPE_LENGTH_SIZE

#define SSH_KEY_TYPE_LENGTH_SIZE   4

#include <crypto.h>

Definition at line 353 of file lib/crypto/crypto.h.

◆ SSH_KEY_TYPE_STRING_SIZE

#define SSH_KEY_TYPE_STRING_SIZE   11

#include <crypto.h>

Definition at line 354 of file lib/crypto/crypto.h.

◆ X25519_KEY_SIZE

#define X25519_KEY_SIZE   32

#include <crypto.h>

X25519 key size in bytes.

Definition at line 90 of file lib/crypto/crypto.h.

◆ X25519_KEY_TYPE

#define X25519_KEY_TYPE   "x25519"

#include <crypto.h>

X25519 key type string ("x25519")

Definition at line 414 of file lib/crypto/crypto.h.

◆ XSALSA20_NONCE_SIZE

#define XSALSA20_NONCE_SIZE   24

#include <crypto.h>

XSalsa20 nonce size in bytes.

Definition at line 98 of file lib/crypto/crypto.h.

◆ ZERO_KEY_SIZE

#define ZERO_KEY_SIZE   X25519_KEY_SIZE

#include <crypto.h>

Zero key array size (32 bytes, used for no-identity entries)

Definition at line 219 of file lib/crypto/crypto.h.

Typedef Documentation

◆ crypto_context_t

#include <crypto.h>

Cryptographic context structure.

Manages all cryptographic state for a single connection, including key exchange, encryption/decryption, authentication, and session rekeying.

Note
Nonce generation: Nonces are constructed as session_id || counter where session_id is 16 bytes and counter fills the remaining bytes. This prevents both within-session and cross-session replay attacks.
Session ID: Generated once per connection and remains constant. Used to prevent cross-session replay attacks.
Nonce counter: Starts at 1 (0 is reserved for testing) and increments for each encryption operation. Prevents nonce reuse within a session.
Byte order: Client must convert network byte order to host byte order for crypto parameters. Server uses host byte order directly.
Key exchange formats:
  • Simple format: Only ephemeral public key (when server has no identity key)
  • Authenticated format: Ephemeral key + identity key + signature (when server has identity key)

Enumeration Type Documentation

◆ crypto_result_t

#include <crypto.h>

Cryptographic operation result codes.

Enumerator
CRYPTO_OK 

Operation succeeded

CRYPTO_ERROR_INIT_FAILED 

Initialization failed

CRYPTO_ERROR_INVALID_PARAMS 

Invalid parameters provided

CRYPTO_ERROR_MEMORY 

Memory allocation failed

CRYPTO_ERROR_LIBSODIUM 

libsodium operation failed

CRYPTO_ERROR_KEY_GENERATION 

Key generation failed

CRYPTO_ERROR_PASSWORD_DERIVATION 

Password-based key derivation failed

CRYPTO_ERROR_ENCRYPTION 

Encryption operation failed

CRYPTO_ERROR_DECRYPTION 

Decryption operation failed

CRYPTO_ERROR_INVALID_MAC 

MAC verification failed (possible tampering)

CRYPTO_ERROR_BUFFER_TOO_SMALL 

Output buffer too small

CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE 

Key exchange not complete

CRYPTO_ERROR_NONCE_EXHAUSTED 

Nonce counter exhausted (should rekey)

CRYPTO_ERROR_REKEY_IN_PROGRESS 

Rekey already in progress

CRYPTO_ERROR_REKEY_FAILED 

Rekey handshake failed

CRYPTO_ERROR_REKEY_RATE_LIMITED 

Too many rekey attempts (DDoS protection)

Definition at line 329 of file lib/crypto/crypto.h.

329 {
330 CRYPTO_OK = 0,
@ CRYPTO_ERROR_REKEY_IN_PROGRESS
@ CRYPTO_ERROR_KEY_GENERATION
@ CRYPTO_ERROR_REKEY_RATE_LIMITED
@ CRYPTO_ERROR_PASSWORD_DERIVATION
@ CRYPTO_ERROR_INVALID_MAC
@ CRYPTO_ERROR_DECRYPTION
@ CRYPTO_ERROR_ENCRYPTION
@ CRYPTO_OK
@ CRYPTO_ERROR_MEMORY
@ CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE
@ CRYPTO_ERROR_REKEY_FAILED
@ CRYPTO_ERROR_LIBSODIUM
@ CRYPTO_ERROR_INVALID_PARAMS
@ CRYPTO_ERROR_BUFFER_TOO_SMALL
@ CRYPTO_ERROR_NONCE_EXHAUSTED
@ CRYPTO_ERROR_INIT_FAILED

Function Documentation

◆ add_known_host()

asciichat_error_t add_known_host ( const char *  server_ip,
uint16_t  port,
const uint8_t  server_key[32] 
)

#include <known_hosts.h>

Add server to known_hosts.

Parameters
server_ipServer IP address (IPv4 or IPv6, must not be NULL)
portServer port number
server_keyServer's Ed25519 public key (32 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Adds server identity key to known_hosts file. Creates file if it doesn't exist and creates directory if needed.

Note
File format: Adds entry as <IP:port> x25519 <hex_key> [comment]. Uses proper bracket notation for IPv6 addresses.
File creation: Creates ~/.ascii-chat/known_hosts if it doesn't exist. Creates ~/.ascii-chat/ directory if needed.
File permissions: Sets file permissions to 0600 (Unix only). Windows does not have Unix-style permissions.
Append mode: Appends entry to end of file (does not overwrite existing entries). Multiple entries for same IP:port are allowed (e.g., key rotation).
Key format: Converts server key to hex string (64 hex chars) for storage. Key is stored as X25519 format (32 bytes).
Warning
File permissions: Sets restrictive permissions (0600) but function may fail if file was just created by fopen (chmod may fail). This is acceptable.

Definition at line 347 of file known_hosts.c.

347 {
348 // Validate parameters first
349 if (!server_ip || !server_key) {
350 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
351 }
352
353 // Check for empty string (must be after NULL check)
354 size_t ip_len = strlen(server_ip);
355 if (ip_len == 0) {
356 return SET_ERRNO(ERROR_INVALID_PARAM, "Empty hostname/IP");
357 }
358
359 const char *path = get_known_hosts_path();
360 if (!path || path[0] == '\0') {
361 SET_ERRNO(ERROR_CONFIG, "Failed to get known hosts file path");
362 return ERROR_CONFIG;
363 }
364
365 // Create parent directories recursively (like mkdir -p)
366 size_t path_len = strlen(path);
367 if (path_len == 0) {
368 SET_ERRNO(ERROR_CONFIG, "Empty known hosts file path");
369 return ERROR_CONFIG;
370 }
371 char *dir = SAFE_MALLOC(path_len + 1, char *);
372 defer(SAFE_FREE(dir));
373 if (!dir) {
374 SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
375 return ERROR_MEMORY;
376 }
377 memcpy(dir, path, path_len + 1);
378
379 // Find the last path separator
380 char *last_sep = strrchr(dir, PATH_DELIM);
381
382 if (last_sep) {
383 *last_sep = '\0'; // Truncate to get directory path
384 asciichat_error_t result = mkdir_recursive(dir);
385 if (result != ASCIICHAT_OK) {
386 return result; // Error already set by mkdir_recursive
387 }
388 }
389
390 // Create the file if it doesn't exist, then append to it
391 // Note: Temporarily removed log_debug to avoid potential crashes during debugging
392 // log_debug("KNOWN_HOSTS: Attempting to create/open file: %s", path);
393 // Use "a" mode for append-only (simpler and works better with chmod)
394 FILE *f = platform_fopen(path, "a");
395 defer(SAFE_FCLOSE(f));
396 if (!f) {
397 // log_debug("KNOWN_HOSTS: platform_fopen failed: %s (errno=%d)", SAFE_STRERROR(errno), errno);
398 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to create/open known hosts file: %s", path);
399 }
400 // Set secure permissions (0600) - only owner can read/write
401 // Note: chmod may fail if file was just created by fopen, but that's okay
403 // log_debug("KNOWN_HOSTS: Successfully opened file: %s", path);
404
405 // Format IP:port with proper bracket notation for IPv6
406 char ip_with_port[BUFFER_SIZE_MEDIUM];
407 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
408 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
409 }
410
411 // Convert key to hex for storage
412 char hex[CRYPTO_HEX_KEY_SIZE_NULL] = {0}; // Initialize to zeros for safety
413 bool is_placeholder = true;
414 // Build hex string byte by byte to avoid buffer overflow issues
415 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
416 // Convert each byte to 2 hex digits directly
417 uint8_t byte = server_key[i];
418 hex[i * 2] = "0123456789abcdef"[byte >> 4]; // High nibble
419 hex[i * 2 + 1] = "0123456789abcdef"[byte & 0xf]; // Low nibble
420 if (byte != 0) {
421 is_placeholder = false;
422 }
423 }
424 hex[CRYPTO_HEX_KEY_SIZE] = '\0'; // Ensure null termination (64 hex digits + null terminator)
425
426 // Write to file and check for errors
427 int fprintf_result;
428 if (is_placeholder) {
429 // Server has no identity key - store as placeholder
430 fprintf_result = safe_fprintf(f, "%s %s 0000000000000000000000000000000000000000000000000000000000000000 %s\n",
432 } else {
433 // Server has identity key - store normally
434 fprintf_result = safe_fprintf(f, "%s %s %s %s\n", ip_with_port, X25519_KEY_TYPE, hex, ASCII_CHAT_APP_NAME);
435 }
436
437 // Check if fprintf failed
438 if (fprintf_result < 0) {
439 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to write to known_hosts file: %s", path);
440 }
441
442 // Flush to ensure data is written
443 if (fflush(f) != 0) {
444 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to flush known_hosts file: %s", path);
445 }
446
447 log_debug("KNOWN_HOSTS: Successfully added host to known_hosts file: %s", path);
448
449 return ASCIICHAT_OK;
450}
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes)
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_FCLOSE(fp)
Definition common.h:330
#define ASCII_CHAT_APP_NAME
Application name for key comments ("ascii-chat")
Definition common.h:43
#define CRYPTO_HEX_KEY_SIZE
Hex string size for 32-byte key (64 hex characters)
const char * get_known_hosts_path(void)
Get the path to the known_hosts file.
Definition known_hosts.c:46
#define NO_IDENTITY_MARKER
No-identity entry marker ("no-identity")
#define ED25519_PUBLIC_KEY_SIZE
Ed25519 public key size in bytes.
#define CRYPTO_HEX_KEY_SIZE_NULL
Hex string size for 32-byte key with null terminator (65 bytes)
#define X25519_KEY_TYPE
X25519 key type string ("x25519")
#define defer(action)
Defer a cleanup action until function scope exit.
Definition defer.h:36
#define SET_ERRNO_SYS(code, context_msg,...)
Set error code with custom message and system error context.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
@ ERROR_MEMORY
Definition error_codes.h:53
@ ERROR_CONFIG
Definition error_codes.h:54
@ ERROR_INVALID_PARAM
int safe_fprintf(FILE *stream, const char *format,...)
Safe version of fprintf.
FILE * platform_fopen(const char *filename, const char *mode)
Safe file open stream (fopen replacement)
#define PATH_DELIM
Platform-specific path separator character.
Definition system.h:605
int platform_chmod(const char *pathname, int mode)
Change file permissions/mode.
#define FILE_PERM_PRIVATE
File permission: Private (owner read/write only)
Definition system.h:637
asciichat_error_t format_ip_with_port(const char *ip, uint16_t port, char *output, size_t output_size)
Format IP address with port number.
Definition ip.c:230

References ASCII_CHAT_APP_NAME, ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, CRYPTO_HEX_KEY_SIZE, CRYPTO_HEX_KEY_SIZE_NULL, defer, ED25519_PUBLIC_KEY_SIZE, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_MEMORY, FILE_PERM_PRIVATE, format_ip_with_port(), get_known_hosts_path(), log_debug, NO_IDENTITY_MARKER, PATH_DELIM, platform_chmod(), platform_fopen(), SAFE_FCLOSE, safe_fprintf(), SAFE_FREE, SAFE_MALLOC, SET_ERRNO, SET_ERRNO_SYS, and X25519_KEY_TYPE.

Referenced by crypto_handshake_client_key_exchange().

◆ check_known_host()

asciichat_error_t check_known_host ( const char *  server_ip,
uint16_t  port,
const uint8_t  server_key[32] 
)

#include <known_hosts.h>

Check if server key is in known_hosts.

Parameters
server_ipServer IP address (IPv4 or IPv6, must not be NULL)
portServer port number
server_keyServer's Ed25519 public key (32 bytes, must not be NULL)
Returns
ASCIICHAT_OK if server not in known_hosts (first connection), positive value if key matches (connection verified), ERROR_CRYPTO_VERIFICATION if key mismatch (MITM warning)

Checks if server key matches known hosts entry for the given IP:port. Returns different values based on verification result.

Note
Return values:
  • ASCIICHAT_OK (0): Server not in known_hosts (first connection, needs verification)
  • Positive value (1): Key matches known_hosts (connection verified, safe to proceed)
  • ERROR_CRYPTO_VERIFICATION: Key mismatch (MITM attack or key rotation, needs user confirmation)
File format: Searches for entries matching <IP:port> x25519 <hex_key>. Supports both IPv4 and IPv6 addresses with proper bracket notation.
Multiple entries: If multiple entries exist for same IP:port, searches all entries and uses first matching key. Continues searching if key doesn't match.
No-identity entries: Skips "no-identity" entries when checking identity keys. If server has identity key but entry is "no-identity", continues searching.
Zero key handling: Special case for no-identity servers with zero keys. If both server key and stored key are zero, returns match (verified no-identity connection).
Key comparison: Uses constant-time comparison (sodium_memcmp) to prevent timing attacks.
File location: Uses ~/.ascii-chat/known_hosts (or equivalent on Windows). Returns ASCIICHAT_OK if file doesn't exist (first connection).
Warning
This function should NOT be called for servers without identity keys. Use check_known_host_no_identity() for servers without identity keys.
Key mismatch: Returns ERROR_CRYPTO_VERIFICATION if key doesn't match. This indicates potential MITM attack or key rotation. User should verify.

Definition at line 77 of file known_hosts.c.

77 {
78 // Validate parameters first
79 if (!server_ip || !server_key) {
80 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
81 }
82
83 const char *path = get_known_hosts_path();
85 if (fd < 0) {
86 // File doesn't exist - this is an unknown host that needs verification
87 log_warn("Known hosts file does not exist: %s", path);
88 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
89 }
90 FILE *f = platform_fdopen(fd, "r");
92 if (!f) {
93 // Failed to open file descriptor as FILE*
95 log_warn("Failed to open known hosts file: %s", path);
96 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
97 }
98
99 char line[BUFFER_SIZE_XLARGE];
100 char expected_prefix[BUFFER_SIZE_MEDIUM];
101
102 // Format IP:port with proper bracket notation for IPv6
103 char ip_with_port[BUFFER_SIZE_MEDIUM];
104 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
105
106 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
107 }
108
109 // Add space after IP:port for prefix matching
110 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
111
112 // Search through ALL matching entries to find one that matches the server key
113 bool found_entries = false;
114 while (fgets(line, sizeof(line), f)) {
115 if (line[0] == '#')
116 continue; // Comment
117
118 if (strncmp(line, expected_prefix, strlen(expected_prefix)) == 0) {
119 // Found matching IP:port - check if this entry matches the server key
120 found_entries = true;
121 size_t prefix_len = strlen(expected_prefix);
122 size_t line_len = strlen(line);
123 if (line_len < prefix_len) {
124 // Line is too short to contain the prefix - skip this entry
125 continue;
126 }
127 char *key_type = line + prefix_len;
128
129 if (strncmp(key_type, NO_IDENTITY_MARKER, strlen(NO_IDENTITY_MARKER)) == 0) {
130 // This is a "no-identity" entry, but server is presenting an identity key
131 // This is a key mismatch - continue searching for a matching identity key
132 log_debug("SECURITY_DEBUG: Found no-identity entry, but server has identity key - continuing search");
133 continue;
134 }
135
136 // Parse key from line (normal identity key)
137 // Format: x25519 <hex_key> <comment>
138 // Extract just the hex key part
139 char *hex_key_start = strchr(key_type, ' ');
140 if (!hex_key_start) {
141 log_debug("SECURITY_DEBUG: No space found in key type: %s", key_type);
142 continue; // Try next entry
143 }
144 hex_key_start++; // Skip the space
145
146 // Find the end of the hex key (next space or end of line)
147 char *hex_key_end = strchr(hex_key_start, ' ');
148 if (hex_key_end) {
149 *hex_key_end = '\0'; // Null-terminate the hex key
150 }
151
152 public_key_t stored_key;
153 if (parse_public_key(hex_key_start, &stored_key) != 0) {
154 log_debug("SECURITY_DEBUG: Failed to parse key from hex: %s", hex_key_start);
155 continue; // Try next entry
156 }
157
158 // DEBUG: Print both keys for comparison
159 char server_key_hex[CRYPTO_HEX_KEY_SIZE_NULL], stored_key_hex[CRYPTO_HEX_KEY_SIZE_NULL];
160 for (int i = 0; i < CRYPTO_KEY_SIZE; i++) {
161 safe_snprintf(server_key_hex + i * 2, 3, "%02x", server_key[i]);
162 safe_snprintf(stored_key_hex + i * 2, 3, "%02x", stored_key.key[i]);
163 }
164 server_key_hex[CRYPTO_HEX_KEY_SIZE] = '\0';
165 stored_key_hex[CRYPTO_HEX_KEY_SIZE] = '\0';
166 log_debug("SECURITY_DEBUG: Server key: %s", server_key_hex);
167 log_debug("SECURITY_DEBUG: Stored key: %s", stored_key_hex);
168
169 // Check if server key is all zeros (no-identity server)
170 bool server_key_is_zero = true;
171 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
172 if (server_key[i] != 0) {
173 server_key_is_zero = false;
174 break;
175 }
176 }
177
178 // Check if stored key is all zeros
179 bool stored_key_is_zero = true;
180 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
181 if (stored_key.key[i] != 0) {
182 stored_key_is_zero = false;
183 break;
184 }
185 }
186
187 // If both keys are zero, this is a no-identity connection
188 // that was previously accepted by the user. Note: This provides weaker
189 // security than key-based verification since any server at this IP:port
190 // without an identity key will match.
191 if (server_key_is_zero && stored_key_is_zero) {
192 log_warn("SECURITY: Connecting to no-identity server at known IP:port. "
193 "This provides weaker security than key-based verification.");
194 return 1; // Match found (no-identity server)
195 }
196
197 // Compare keys (constant-time to prevent timing attacks)
198 if (sodium_memcmp(server_key, stored_key.key, ED25519_PUBLIC_KEY_SIZE) == 0) {
199 log_info("SECURITY: Server key matches known_hosts - connection verified");
200 return 1; // Match found!
201 }
202 log_debug("SECURITY_DEBUG: Key mismatch, continuing search...");
203 }
204 }
205
206 // No matching key found - check if we found any entries at all
207 if (found_entries) {
208 // We found entries for this IP:port but none matched the server key
209 // This is a key mismatch (potential MITM attack)
210 log_error("SECURITY: Server key does NOT match any known_hosts entries!");
211 log_error("SECURITY: This indicates a possible man-in-the-middle attack!");
212 return ERROR_CRYPTO_VERIFICATION; // Key mismatch - MITM warning!
213 }
214
215 // No entries found for this IP:port - first connection
216 return ASCIICHAT_OK; // Not found = first connection
217}
#define BUFFER_SIZE_XLARGE
Extra large buffer size (2048 bytes)
#define CRYPTO_KEY_SIZE
Ed25519 key size in bytes.
@ ERROR_CRYPTO_VERIFICATION
Definition error_codes.h:92
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
Parse SSH/GPG public key from any format (returns first key only)
Definition keys.c:24
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
FILE * platform_fdopen(int fd, const char *mode)
Convert file descriptor to stream (fdopen replacement)
int platform_close(int fd)
Safe file close (close replacement)
int platform_open(const char *pathname, int flags,...)
Safe file open (open replacement)
#define PLATFORM_O_RDONLY
Open file for reading only.
Definition file.h:44

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, CRYPTO_HEX_KEY_SIZE, CRYPTO_HEX_KEY_SIZE_NULL, CRYPTO_KEY_SIZE, defer, ED25519_PUBLIC_KEY_SIZE, ERROR_CRYPTO_VERIFICATION, ERROR_INVALID_PARAM, FILE_PERM_PRIVATE, format_ip_with_port(), get_known_hosts_path(), public_key_t::key, log_debug, log_error, log_info, log_warn, NO_IDENTITY_MARKER, parse_public_key(), platform_close(), platform_fdopen(), PLATFORM_O_RDONLY, platform_open(), SAFE_FCLOSE, safe_snprintf(), and SET_ERRNO.

Referenced by crypto_handshake_client_key_exchange().

◆ check_known_host_no_identity()

asciichat_error_t check_known_host_no_identity ( const char *  server_ip,
uint16_t  port 
)

#include <known_hosts.h>

Check known_hosts for servers without identity key (no-identity entries)

Parameters
server_ipServer IP address (IPv4 or IPv6, must not be NULL)
portServer port number
Returns
ASCIICHAT_OK if server not in known_hosts (first connection), positive value if no-identity entry found (connection previously accepted), ERROR_CRYPTO_VERIFICATION if server previously had identity key but now has none

Checks if server has "no-identity" entry in known_hosts. Used for servers that don't have identity keys (ephemeral keys only).

Note
Return values:
  • ASCIICHAT_OK (0): Server not in known_hosts (first connection, needs verification)
  • Positive value (1): No-identity entry found (connection previously accepted)
  • ERROR_CRYPTO_VERIFICATION: Server previously had identity key but now has none (security concern)
File format: Searches for entries matching <IP:port> no-identity. Does NOT verify keys (no keys to verify for no-identity servers).
Purpose: This function is NOT for key verification (no keys to verify). Use check_known_host() for servers with identity keys.
Security: Cannot verify server identity (no identity key to verify). Only tracks whether user previously accepted connection to this server.
Warning
This function should NOT be used for key verification. Use check_known_host() for servers with identity keys.
Security limitation: Cannot verify server identity (no keys to compare). Only provides tracking of previously accepted connections.

Definition at line 223 of file known_hosts.c.

223 {
224 const char *path = get_known_hosts_path();
226 if (fd < 0) {
227 // File doesn't exist - this is an unknown host that needs verification
228 log_warn("Known hosts file does not exist: %s", path);
229 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
230 }
231
232 FILE *f = platform_fdopen(fd, "r");
233 defer(SAFE_FCLOSE(f));
234 if (!f) {
235 // Failed to open file descriptor as FILE*
236 platform_close(fd);
237 log_warn("Failed to open known hosts file: %s", path);
238 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
239 }
240
241 char line[BUFFER_SIZE_XLARGE];
242 char expected_prefix[BUFFER_SIZE_MEDIUM];
243
244 // Format IP:port with proper bracket notation for IPv6
245 char ip_with_port[BUFFER_SIZE_MEDIUM];
246 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
247
248 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
249 }
250
251 // Add space after IP:port for prefix matching
252 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
253
254 while (fgets(line, sizeof(line), f)) {
255 if (line[0] == '#')
256 continue; // Comment
257
258 if (strncmp(line, expected_prefix, strlen(expected_prefix)) == 0) {
259 // Found matching IP:port
260
261 // Check if this is a "no-identity" entry
262 // Bounds check: ensure line is long enough to contain the prefix
263 size_t prefix_len = strlen(expected_prefix);
264 size_t line_len = strlen(line);
265 if (line_len < prefix_len) {
266 // Line is too short to contain the prefix - this shouldn't happen
267 // but let's be safe and treat as unknown host
268 return ASCIICHAT_OK;
269 }
270 char *key_type = line + prefix_len;
271 // Skip leading whitespace
272 while (*key_type == ' ' || *key_type == '\t') {
273 key_type++;
274 }
275 if (strncmp(key_type, "no-identity", 11) == 0) {
276 // This is a server without identity key that was previously accepted by the user
277 // No warnings or user confirmation needed - user already accepted this server
278 return 1; // Known host (no-identity entry) - secure connection
279 }
280
281 // If we found a normal identity key entry, this is a mismatch
282 // Server previously had identity key but now has none
283 log_warn("Server previously had identity key but now has none - potential security issue");
284 return ERROR_CRYPTO_VERIFICATION; // Mismatch - server changed from identity to no-identity
285 }
286 }
287
288 return ASCIICHAT_OK; // Not found = first connection
289}

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, defer, ERROR_CRYPTO_VERIFICATION, ERROR_INVALID_PARAM, FILE_PERM_PRIVATE, format_ip_with_port(), get_known_hosts_path(), log_warn, platform_close(), platform_fdopen(), PLATFORM_O_RDONLY, platform_open(), SAFE_FCLOSE, safe_snprintf(), and SET_ERRNO.

Referenced by crypto_handshake_client_key_exchange().

◆ compute_key_fingerprint()

void compute_key_fingerprint ( const uint8_t  key[32],
char  fingerprint[65] 
)

#include <known_hosts.h>

Compute SHA256 fingerprint of Ed25519 key for display.

Parameters
keyEd25519 public key (32 bytes, must not be NULL)
fingerprintOutput buffer for fingerprint (65 bytes including null terminator)

Computes SHA256 fingerprint of Ed25519 public key for display. Fingerprint is displayed in hex format (64 hex chars + null terminator).

Note
Fingerprint format: SHA256 hash of key, displayed as 64 hex characters. Example: "a1b2c3d4e5f6..."
Output format: Fingerprint is stored as null-terminated hex string (65 bytes total). First 64 bytes are hex characters, 65th byte is null terminator.
Key format: Accepts Ed25519 public key (32 bytes). Fingerprint is computed over raw key bytes.
Use case: Used for displaying key fingerprints in warnings and prompts. Helps users verify keys visually.
Warning
Output buffer must be at least 65 bytes (64 hex chars + null terminator). Function does not validate buffer size (may overflow).

◆ crypto_cleanup()

void crypto_cleanup ( crypto_context_t ctx)

#include <crypto.h>

Cleanup crypto context with secure memory wiping.

Parameters
ctxCrypto context to cleanup

Securely zeroes all sensitive data (keys, salts, etc.) before freeing. Always call this when done with a crypto context.

Note
Uses sodium_memzero() to prevent key material from persisting in memory.

Definition at line 183 of file lib/crypto/crypto.c.

183 {
184 if (!ctx || !ctx->initialized) {
185 return;
186 }
187
188 // Securely wipe sensitive data
189 secure_memzero(ctx->private_key, sizeof(ctx->private_key));
190 secure_memzero(ctx->shared_key, sizeof(ctx->shared_key));
191 secure_memzero(ctx->password_key, sizeof(ctx->password_key));
192 secure_memzero(ctx->password_salt, sizeof(ctx->password_salt));
193
194 log_debug("Crypto context cleaned up (encrypted: %lu bytes, decrypted: %lu bytes)", ctx->bytes_encrypted,
195 ctx->bytes_decrypted);
196
197 // Clear entire context
198 secure_memzero(ctx, sizeof(crypto_context_t));
199}
uint8_t password_salt[32]

References crypto_context_t::bytes_decrypted, crypto_context_t::bytes_encrypted, crypto_context_t::initialized, log_debug, crypto_context_t::password_key, crypto_context_t::password_salt, crypto_context_t::private_key, and crypto_context_t::shared_key.

Referenced by crypto_handshake_cleanup(), and crypto_init_with_password().

◆ crypto_combine_auth_data()

void crypto_combine_auth_data ( const uint8_t hmac,
const uint8_t challenge_nonce,
uint8_t combined_out 
)

#include <crypto.h>

Combine HMAC and challenge nonce for transmission.

Parameters
hmacHMAC to transmit (32 bytes)
challenge_nonceChallenge nonce to transmit (32 bytes)
combined_outOutput buffer for combined data (64 bytes)

Combines HMAC and challenge nonce into a single buffer: [HMAC:32][nonce:32] Used to pack authentication response data for transmission.

Note
Format: [HMAC:32][challenge_nonce:32] (64 bytes total)

Definition at line 1115 of file lib/crypto/crypto.c.

1115 {
1116 if (!hmac || !challenge_nonce || !combined_out) {
1117 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: hmac=%p, challenge_nonce=%p, combined_out=%p", hmac,
1118 challenge_nonce, combined_out);
1119 return;
1120 }
1121
1122 // Combine HMAC (32 bytes) + challenge nonce (32 bytes) = 64 bytes total
1123 memcpy(combined_out, hmac, 32);
1124 memcpy(combined_out + 32, challenge_nonce, 32);
1125}

References ERROR_INVALID_PARAM, and SET_ERRNO.

◆ crypto_compute_auth_response()

crypto_result_t crypto_compute_auth_response ( const crypto_context_t ctx,
const uint8_t  nonce[32],
uint8_t  hmac_out[32] 
)

#include <crypto.h>

Compute authentication response HMAC bound to DH shared_secret.

Parameters
ctxCrypto context with keys (must have completed key exchange)
nonceChallenge nonce from server (32 bytes)
hmac_outOutput HMAC (32 bytes)
Returns
CRYPTO_OK on success, error code on failure

Computes: HMAC(auth_key, nonce || shared_secret) This binds the password/key authentication to the DH exchange, preventing MITM.

Note
Key selection: Uses password_key if available, otherwise uses shared_key. This allows password authentication to be optional while still binding to DH.
Key exchange requirement: ctx->key_exchange_complete must be true. Returns CRYPTO_ERROR_INVALID_PARAMS if key exchange is not complete.
MITM prevention: By including shared_secret in the HMAC computation, an attacker cannot intercept and replay authentication responses without knowing the shared secret.
Warning
Context must have completed key exchange before calling this function.

Definition at line 871 of file lib/crypto/crypto.c.

872 {
873 if (!ctx || !nonce || !hmac_out) {
874 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_compute_auth_response: Invalid parameters (ctx=%p, nonce=%p, hmac_out=%p)",
875 ctx, nonce, hmac_out);
877 }
878
879 // Ensure shared secret is derived before computing HMAC
880 // This is critical for password HMAC which binds to the shared secret
881 if (!ctx->key_exchange_complete) {
882 SET_ERRNO(ERROR_CRYPTO, "Cannot compute auth response - key exchange not complete");
884 }
885
886 // Bind password HMAC to DH shared_secret to prevent MITM
887 // Combined data: nonce || shared_secret
888 uint8_t combined_data[64];
889 memcpy(combined_data, nonce, 32);
890 memcpy(combined_data + 32, ctx->shared_key, 32);
891
892 // Use password_key if available, otherwise use shared_key
893 const uint8_t *auth_key = ctx->has_password ? ctx->password_key : ctx->shared_key;
894
895 crypto_result_t result = crypto_compute_hmac_ex(ctx, auth_key, combined_data, 64, hmac_out);
896
897 // Securely zero sensitive data containing shared secret
898 sodium_memzero(combined_data, sizeof(combined_data));
899
900 return result;
901}
@ ERROR_CRYPTO
Definition error_codes.h:88

References crypto_compute_hmac_ex(), CRYPTO_ERROR_INVALID_PARAMS, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_password, crypto_context_t::key_exchange_complete, crypto_context_t::password_key, SET_ERRNO, and crypto_context_t::shared_key.

Referenced by crypto_handshake_server_complete().

◆ crypto_compute_hmac()

crypto_result_t crypto_compute_hmac ( crypto_context_t ctx,
const uint8_t  key[32],
const uint8_t  data[32],
uint8_t  hmac[32] 
)

#include <crypto.h>

Compute HMAC-SHA256 for fixed 32-byte data.

Parameters
ctxCrypto context (for libsodium initialization check)
keyHMAC key (32 bytes)
dataData to authenticate (32 bytes)
hmacOutput buffer for HMAC (32 bytes)
Returns
CRYPTO_OK on success, error code on failure

Computes HMAC-SHA256 over exactly 32 bytes of data. Useful for authenticating challenge nonces and other fixed-size values.

Note
Automatically initializes libsodium if not already initialized.

Definition at line 802 of file lib/crypto/crypto.c.

803 {
804 if (!ctx || !key || !data || !hmac) {
805 return SET_ERRNO(ERROR_INVALID_PARAM, "crypto_compute_hmac: Invalid parameters (ctx=%p, key=%p, data=%p, hmac=%p)",
806 ctx, key, data, hmac);
807 }
808
809 crypto_result_t result = init_libsodium();
810 if (result != CRYPTO_OK) {
811 return result;
812 }
813
814 crypto_auth_hmacsha256(hmac, data, 32, key);
815 return CRYPTO_OK;
816}

References CRYPTO_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ crypto_compute_hmac_ex()

crypto_result_t crypto_compute_hmac_ex ( const crypto_context_t ctx,
const uint8_t  key[32],
const uint8_t data,
size_t  data_len,
uint8_t  hmac[32] 
)

#include <crypto.h>

Compute HMAC-SHA256 for variable-length data.

Parameters
ctxCrypto context (for libsodium initialization check)
keyHMAC key (32 bytes)
dataData to authenticate (variable length)
data_lenLength of data (must be > 0)
hmacOutput buffer for HMAC (32 bytes)
Returns
CRYPTO_OK on success, error code on failure

Computes HMAC-SHA256 over variable-length data. Useful for authenticating combined values (e.g., nonce || shared_secret).

Note
Automatically initializes libsodium if not already initialized.

Definition at line 818 of file lib/crypto/crypto.c.

819 {
820 if (!ctx || !key || !data || !hmac || data_len == 0) {
822 "crypto_compute_hmac_ex: Invalid parameters (ctx=%p, key=%p, data=%p, data_len=%zu, hmac=%p)", ctx,
823 key, data, data_len, hmac);
824 }
825
826 crypto_result_t result = init_libsodium();
827 if (result != CRYPTO_OK) {
828 return result;
829 }
830
831 crypto_auth_hmacsha256(hmac, data, data_len, key);
832 return CRYPTO_OK;
833}

References CRYPTO_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by crypto_compute_auth_response(), and crypto_compute_password_hmac().

◆ crypto_compute_password_hmac()

asciichat_error_t crypto_compute_password_hmac ( crypto_context_t ctx,
const uint8_t password_key,
const uint8_t nonce,
const uint8_t shared_secret,
uint8_t hmac_out 
)

#include <crypto.h>

Compute password-based HMAC for authentication.

Parameters
ctxCrypto context
password_keyPassword-derived key (32 bytes)
nonceChallenge nonce (32 bytes)
shared_secretDH shared secret (32 bytes)
hmac_outOutput buffer for HMAC (32 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Computes: HMAC(password_key, nonce || shared_secret) Binds password authentication to DH key exchange, preventing MITM.

Note
Used by authentication handlers to prove knowledge of password AND shared secret.
Combined data: nonce || shared_secret (64 bytes total)

Definition at line 1045 of file lib/crypto/crypto.c.

1046 {
1047 if (!ctx || !password_key || !nonce || !shared_secret || !hmac_out) {
1049 "Invalid parameters: ctx=%p, password_key=%p, nonce=%p, shared_secret=%p, hmac_out=%p", ctx,
1050 password_key, nonce, shared_secret, hmac_out);
1051 }
1052
1053 // Combine nonce and shared_secret for HMAC computation
1054 // This binds the password to the DH shared secret, preventing MITM attacks
1055 uint8_t combined_data[64];
1056 memcpy(combined_data, nonce, 32);
1057 memcpy(combined_data + 32, shared_secret, 32);
1058
1059 // Compute HMAC using the password-derived key
1060 if (crypto_compute_hmac_ex(ctx, password_key, combined_data, 64, hmac_out) != 0) {
1061 // Securely zero sensitive data containing shared secret even on error
1062 sodium_memzero(combined_data, sizeof(combined_data));
1063 return SET_ERRNO(ERROR_CRYPTO, "Failed to compute password HMAC");
1064 }
1065
1066 // Securely zero sensitive data containing shared secret
1067 sodium_memzero(combined_data, sizeof(combined_data));
1068
1069 return ASCIICHAT_OK;
1070}

References ASCIICHAT_OK, crypto_compute_hmac_ex(), ERROR_CRYPTO, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ crypto_create_auth_challenge()

crypto_result_t crypto_create_auth_challenge ( const crypto_context_t ctx,
uint8_t packet_out,
size_t  packet_size,
size_t *  packet_len_out 
)

#include <crypto.h>

Create authentication challenge packet.

Parameters
ctxCrypto context (must be initialized)
packet_outOutput buffer for challenge packet
packet_sizeSize of output buffer
packet_len_outOutput parameter for actual packet length
Returns
CRYPTO_OK on success, error code on failure

Creates an authentication challenge packet: [type:4][nonce:auth_challenge_size] Generates a random nonce and stores it in ctx->auth_nonce for later verification.

Note
Packet format: [PACKET_TYPE_AUTH_CHALLENGE:4][random_nonce:32] Total size: sizeof(uint32_t) + ctx->auth_challenge_size (typically 36 bytes)
The generated nonce is stored in ctx->auth_nonce and should be used later to verify the authentication response.

Definition at line 939 of file lib/crypto/crypto.c.

940 {
941 if (!ctx || !ctx->initialized || !packet_out || !packet_len_out) {
942 SET_ERRNO(
944 "crypto_create_auth_challenge: Invalid parameters (ctx=%p, initialized=%d, packet_out=%p, packet_len_out=%p)",
945 ctx, ctx ? ctx->initialized : 0, packet_out, packet_len_out);
947 }
948
949 size_t required_size = sizeof(uint32_t) + ctx->auth_challenge_size; // type + nonce
950 if (packet_size < required_size) {
951 SET_ERRNO(ERROR_BUFFER, "crypto_create_auth_challenge: Buffer too small (size=%zu, required=%zu)", packet_size,
952 required_size);
954 }
955
956 // Generate random nonce (stores result in ctx->auth_nonce)
958 if (result != CRYPTO_OK) {
959 return result;
960 }
961
962 // Pack packet: [type:4][nonce:auth_challenge_size]
963 uint32_t packet_type = CRYPTO_PACKET_AUTH_CHALLENGE;
964 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
965 SAFE_MEMCPY(packet_out + sizeof(packet_type), ctx->auth_challenge_size, ctx->auth_nonce, ctx->auth_challenge_size);
966
967 *packet_len_out = required_size;
968 return CRYPTO_OK;
969}
crypto_result_t crypto_generate_nonce(uint8_t nonce[32])
Generate random nonce for authentication.
@ ERROR_BUFFER
Definition error_codes.h:96

References crypto_context_t::auth_challenge_size, crypto_context_t::auth_nonce, CRYPTO_ERROR_BUFFER_TOO_SMALL, CRYPTO_ERROR_INVALID_PARAMS, crypto_generate_nonce(), CRYPTO_OK, ERROR_BUFFER, ERROR_INVALID_PARAM, crypto_context_t::initialized, SAFE_MEMCPY, and SET_ERRNO.

◆ crypto_create_encrypted_packet()

crypto_result_t crypto_create_encrypted_packet ( crypto_context_t ctx,
const uint8_t data,
size_t  data_len,
uint8_t packet_out,
size_t  packet_size,
size_t *  packet_len_out 
)

#include <crypto.h>

Create encrypted data packet for network transmission.

Parameters
ctxCrypto context (must be initialized and ready)
dataPlaintext data to encrypt
data_lenLength of plaintext data
packet_outOutput buffer for encrypted packet
packet_sizeSize of output buffer
packet_len_outOutput parameter for actual packet length
Returns
CRYPTO_OK on success, error code on failure

Creates an encrypted data packet: [type:4][length:4][encrypted_data:var] Encrypts the data using crypto_encrypt() and prepends packet type and length.

Note
Packet format: [PACKET_TYPE_ENCRYPTED_DATA:4][data_length:4][encrypted_data] Encrypted data format: [nonce:24][encrypted_data][MAC:16] Total size: sizeof(uint32_t) + sizeof(uint32_t) + data_len + nonce_size + mac_size
Context must be ready (crypto_is_ready() returns true) before calling. This requires either completed key exchange or password-based encryption.
Warning
Ensure packet_size is large enough for encrypted data + headers.

Definition at line 680 of file lib/crypto/crypto.c.

681 {
682 if (!ctx || !data || !packet_out || !packet_len_out) {
684 "crypto_create_encrypted_packet: Invalid parameters (ctx=%p, data=%p, packet_out=%p, packet_len_out=%p)",
685 ctx, data, packet_out, packet_len_out);
687 }
688
689 if (!crypto_is_ready(ctx)) {
690 SET_ERRNO(ERROR_CRYPTO, "crypto_create_encrypted_packet: Crypto context not ready");
692 }
693
694 // CRITICAL: Validate data_len to prevent integer overflow in size calculations
695 if (data_len > CRYPTO_MAX_PLAINTEXT_SIZE) {
696 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_create_encrypted_packet: data_len %zu exceeds max %d", data_len,
699 }
700
701 // Check for integer overflow: data_len + nonce_size + mac_size
702 if (data_len > SIZE_MAX - ctx->nonce_size - ctx->mac_size) {
703 SET_ERRNO(ERROR_BUFFER, "crypto_create_encrypted_packet: encrypted_size overflow");
705 }
706 size_t encrypted_size = data_len + ctx->nonce_size + ctx->mac_size;
707
708 // Check for integer overflow: header + encrypted_size
709 size_t header_size = sizeof(uint32_t) + sizeof(uint32_t);
710 if (encrypted_size > SIZE_MAX - header_size) {
711 SET_ERRNO(ERROR_BUFFER, "crypto_create_encrypted_packet: required_size overflow");
713 }
714 size_t required_size = header_size + encrypted_size; // type + len + encrypted_data
715
716 if (packet_size < required_size) {
717 SET_ERRNO(ERROR_BUFFER, "crypto_create_encrypted_packet: Buffer too small (size=%zu, required=%zu)", packet_size,
718 required_size);
720 }
721
722 // Encrypt the data
723 size_t ciphertext_len;
724 uint8_t *encrypted_data = packet_out + sizeof(uint32_t) + sizeof(uint32_t);
725 crypto_result_t result = crypto_encrypt(ctx, data, data_len, encrypted_data,
726 packet_size - sizeof(uint32_t) - sizeof(uint32_t), &ciphertext_len);
727 if (result != CRYPTO_OK) {
728 return result;
729 }
730
731 // Pack packet: [type:4][length:4][encrypted_data:var]
732 uint32_t packet_type = CRYPTO_PACKET_ENCRYPTED_DATA;
733 uint32_t data_length = (uint32_t)ciphertext_len;
734
735 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
736 SAFE_MEMCPY(packet_out + sizeof(packet_type), sizeof(data_length), &data_length, sizeof(data_length));
737
738 *packet_len_out = required_size;
739 return CRYPTO_OK;
740}
crypto_result_t crypto_encrypt(crypto_context_t *ctx, const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext_out, size_t ciphertext_out_size, size_t *ciphertext_len_out)
Encrypt data using XSalsa20-Poly1305.
bool crypto_is_ready(const crypto_context_t *ctx)
Check if key exchange is complete and ready for encryption.
#define CRYPTO_MAX_PLAINTEXT_SIZE
Maximum plaintext size (1MB)

References crypto_encrypt(), CRYPTO_ERROR_BUFFER_TOO_SMALL, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE, crypto_is_ready(), CRYPTO_MAX_PLAINTEXT_SIZE, CRYPTO_OK, ERROR_BUFFER, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::mac_size, crypto_context_t::nonce_size, SAFE_MEMCPY, and SET_ERRNO.

◆ crypto_create_public_key_packet()

crypto_result_t crypto_create_public_key_packet ( const crypto_context_t ctx,
uint8_t packet_out,
size_t  packet_size,
size_t *  packet_len_out 
)

#include <crypto.h>

Create public key packet for network transmission.

Parameters
ctxCrypto context (must be initialized)
packet_outOutput buffer for packet
packet_sizeSize of output buffer
packet_len_outOutput parameter for actual packet length
Returns
CRYPTO_OK on success, error code on failure

Creates a public key packet: [type:4][public_key:public_key_size] Used during key exchange handshake to send ephemeral public key to peer.

Note
Packet format: [PACKET_TYPE_PUBLIC_KEY:4][ephemeral_public_key:32] Total size: sizeof(uint32_t) + ctx->public_key_size (typically 36 bytes)
This packet is NOT encrypted - it's part of the key exchange protocol.

Definition at line 623 of file lib/crypto/crypto.c.

624 {
625 if (!ctx || !ctx->initialized || !packet_out || !packet_len_out) {
627 "crypto_create_public_key_packet: Invalid parameters (ctx=%p, initialized=%d, packet_out=%p, "
628 "packet_len_out=%p)",
629 ctx, ctx ? ctx->initialized : 0, packet_out, packet_len_out);
631 }
632
633 size_t required_size = sizeof(uint32_t) + ctx->public_key_size; // type + key
634 if (packet_size < required_size) {
635 SET_ERRNO(ERROR_BUFFER, "crypto_create_public_key_packet: Buffer too small (size=%zu, required=%zu)", packet_size,
636 required_size);
638 }
639
640 // Pack packet: [type:4][public_key:32]
641 uint32_t packet_type = CRYPTO_PACKET_PUBLIC_KEY;
642 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
643 // Bounds check to prevent buffer overflow
644 size_t copy_size = (ctx->public_key_size <= X25519_KEY_SIZE) ? ctx->public_key_size : X25519_KEY_SIZE;
645 SAFE_MEMCPY(packet_out + sizeof(packet_type), copy_size, ctx->public_key, copy_size);
646
647 *packet_len_out = required_size;
648 return CRYPTO_OK;
649}
#define X25519_KEY_SIZE
X25519 key size in bytes.

References CRYPTO_ERROR_BUFFER_TOO_SMALL, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_OK, ERROR_BUFFER, ERROR_INVALID_PARAM, crypto_context_t::initialized, crypto_context_t::public_key, crypto_context_t::public_key_size, SAFE_MEMCPY, SET_ERRNO, and X25519_KEY_SIZE.

◆ crypto_decrypt()

crypto_result_t crypto_decrypt ( crypto_context_t ctx,
const uint8_t ciphertext,
size_t  ciphertext_len,
uint8_t plaintext_out,
size_t  plaintext_out_size,
size_t *  plaintext_len_out 
)

#include <crypto.h>

Decrypt data using XSalsa20-Poly1305.

Parameters
ctxCrypto context (must be initialized and ready)
ciphertextCiphertext data to decrypt
ciphertext_lenLength of ciphertext (must be >= nonce_size + mac_size)
plaintext_outOutput buffer for plaintext
plaintext_out_sizeSize of output buffer
plaintext_len_outOutput parameter for actual plaintext length
Returns
CRYPTO_OK on success, CRYPTO_ERROR_INVALID_MAC if MAC verification fails

Decrypts data using XSalsa20-Poly1305 authenticated encryption. Uses shared_key if key exchange is complete, otherwise falls back to password_key.

Note
Ciphertext format: [nonce:nonce_size][encrypted_data][MAC:mac_size] Nonce is extracted from the first nonce_size bytes of ciphertext.
MAC verification: Automatically verified during decryption. Returns CRYPTO_ERROR_INVALID_MAC if MAC verification fails (indicating tampering or wrong key).
Plaintext size: ciphertext_len - nonce_size - mac_size
Buffer requirements: plaintext_out_size must be >= ciphertext_len - nonce_size - mac_size
Warning
Context must be ready (crypto_is_ready() returns true) before calling this function.
Always check return value. CRYPTO_ERROR_INVALID_MAC indicates tampering or wrong key.

Definition at line 481 of file lib/crypto/crypto.c.

482 {
483 if (!ctx || !ctx->initialized || !ciphertext || !plaintext_out || !plaintext_len_out) {
485 "Invalid parameters: ctx=%p, initialized=%d, ciphertext=%p, plaintext_out=%p, plaintext_len_out=%p", ctx,
486 ctx ? ctx->initialized : 0, ciphertext, plaintext_out, plaintext_len_out);
488 }
489
490 if (!crypto_is_ready(ctx)) {
491 SET_ERRNO(ERROR_CRYPTO, "Crypto context not ready for decryption");
493 }
494
495 // Check minimum ciphertext size (nonce + MAC)
496 size_t min_ciphertext_size = ctx->nonce_size + ctx->mac_size;
497 if (ciphertext_len < min_ciphertext_size) {
498 SET_ERRNO(ERROR_INVALID_PARAM, "Ciphertext too small: %zu < %zu", ciphertext_len, min_ciphertext_size);
500 }
501
502 size_t plaintext_len = ciphertext_len - ctx->nonce_size - ctx->mac_size;
503 if (plaintext_out_size < plaintext_len) {
504 SET_ERRNO(ERROR_BUFFER, "Plaintext buffer too small: %zu < %zu", plaintext_out_size, plaintext_len);
506 }
507
508 // Extract nonce from beginning of ciphertext
509 const uint8_t *nonce = ciphertext;
510 const uint8_t *encrypted_data = ciphertext + ctx->nonce_size;
511
512 // Choose decryption key (prefer shared key over password key)
513 const uint8_t *decryption_key = NULL;
514 if (ctx->key_exchange_complete) {
515 decryption_key = ctx->shared_key;
516 } else if (ctx->has_password) {
517 decryption_key = ctx->password_key;
518 } else {
519 SET_ERRNO(ERROR_CRYPTO, "No decryption key available");
521 }
522
523 // Decrypt using NaCl secretbox (XSalsa20 + Poly1305)
524 if (crypto_secretbox_open_easy(plaintext_out, encrypted_data, ciphertext_len - ctx->nonce_size, nonce,
525 decryption_key) != 0) {
526 SET_ERRNO(ERROR_CRYPTO, "Decryption failed - invalid MAC or corrupted data");
528 }
529
530 *plaintext_len_out = plaintext_len;
531 ctx->bytes_decrypted += plaintext_len;
532
533 return CRYPTO_OK;
534}

References crypto_context_t::bytes_decrypted, CRYPTO_ERROR_BUFFER_TOO_SMALL, CRYPTO_ERROR_INVALID_MAC, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE, crypto_is_ready(), CRYPTO_OK, ERROR_BUFFER, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_password, crypto_context_t::initialized, crypto_context_t::key_exchange_complete, crypto_context_t::mac_size, crypto_context_t::nonce_size, crypto_context_t::password_key, SET_ERRNO, and crypto_context_t::shared_key.

Referenced by crypto_handshake_decrypt_packet(), crypto_handshake_process_rekey_complete(), crypto_process_encrypted_packet(), and receive_packet_secure().

◆ crypto_derive_password_encryption_key()

crypto_result_t crypto_derive_password_encryption_key ( const char *  password,
uint8_t  encryption_key[32] 
)

#include <crypto.h>

Derive deterministic encryption key from password for handshake.

Parameters
passwordPassword to derive key from
encryption_keyOutput buffer for derived key (CRYPTO_ENCRYPTION_KEY_SIZE bytes)
Returns
CRYPTO_OK on success, error code on failure

Derives a deterministic key using a fixed salt (for handshake purposes). This allows password-protected sessions without requiring key exchange to be completed first.

Note
Uses same deterministic salt ("ascii-chat-password-salt-v1") as crypto_derive_password_key() for consistency.
Same password always produces same key. Only use for handshake encryption, not for long-term key storage.

Definition at line 371 of file lib/crypto/crypto.c.

372 {
373 if (!password || !encryption_key) {
375 "crypto_derive_password_encryption_key: Invalid parameters (password=%p, encryption_key=%p)", password,
376 encryption_key);
378 }
379
380 // Validate password length requirements
381 crypto_result_t validation_result = crypto_validate_password(password);
382 if (validation_result != CRYPTO_OK) {
383 return validation_result;
384 }
385
386 // Use deterministic salt for consistent key derivation across client/server
387 // Salt must be exactly ARGON2ID_SALT_SIZE (32) bytes
388 const char *deterministic_salt = "ascii-chat-password-salt-v1";
389 uint8_t salt[ARGON2ID_SALT_SIZE]; // Use maximum size for buffer
390 size_t salt_str_len = strlen(deterministic_salt);
391
392 // Zero-initialize the salt buffer first
393 memset(salt, 0, ARGON2ID_SALT_SIZE);
394
395 // Copy the salt string (will be padded with zeros to ARGON2ID_SALT_SIZE)
396 memcpy(salt, deterministic_salt, (salt_str_len < ARGON2ID_SALT_SIZE) ? salt_str_len : ARGON2ID_SALT_SIZE);
397
398 // Derive key using Argon2id (memory-hard, secure against GPU attacks)
399 if (crypto_pwhash(encryption_key, SECRETBOX_KEY_SIZE, password, strlen(password), salt,
400 crypto_pwhash_OPSLIMIT_INTERACTIVE, // ~0.1 seconds
401 crypto_pwhash_MEMLIMIT_INTERACTIVE, // ~64MB
402 crypto_pwhash_ALG_DEFAULT) != 0) {
403 SET_ERRNO(ERROR_CRYPTO, "Password encryption key derivation failed - possibly out of memory");
405 }
406
407 log_debug("Password encryption key derived successfully using Argon2id");
408 return CRYPTO_OK;
409}
#define SECRETBOX_KEY_SIZE
Secretbox key size in bytes.
crypto_result_t crypto_validate_password(const char *password)
Validate password length requirements.
#define ARGON2ID_SALT_SIZE
Argon2id salt size in bytes.

References ARGON2ID_SALT_SIZE, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_PASSWORD_DERIVATION, CRYPTO_OK, crypto_validate_password(), ERROR_CRYPTO, ERROR_INVALID_PARAM, log_debug, SECRETBOX_KEY_SIZE, and SET_ERRNO.

◆ crypto_derive_password_key()

crypto_result_t crypto_derive_password_key ( crypto_context_t ctx,
const char *  password 
)

#include <crypto.h>

Derive key from password using Argon2id.

Parameters
ctxCrypto context (must be initialized)
passwordPassword to derive key from
Returns
CRYPTO_OK on success, error code on failure

Derives a 32-byte encryption key from the password using Argon2id KDF. The salt is deterministic ("ascii-chat-password-salt-v1") for consistent key derivation across client/server.

Note
Argon2id is memory-hard, making offline brute-force attacks expensive.
Salt: Uses deterministic salt ("ascii-chat-password-salt-v1") padded to ARGON2ID_SALT_SIZE (32 bytes) with zeros. This ensures the same password produces the same key on both client and server.
Argon2id parameters: Uses INTERACTIVE limits (~0.1 seconds, ~64MB memory).
Warning
Deterministic salt: Same password always produces same key. Only use for session encryption, not long-term key storage.

Definition at line 297 of file lib/crypto/crypto.c.

297 {
298 if (!ctx || !ctx->initialized || !password) {
300 "crypto_derive_password_key: Invalid parameters (ctx=%p, initialized=%d, password=%p)", ctx,
301 ctx ? ctx->initialized : 0, password);
303 }
304
305 // Validate password length requirements
306 crypto_result_t validation_result = crypto_validate_password(password);
307 if (validation_result != CRYPTO_OK) {
308 return validation_result;
309 }
310
311 // Use deterministic salt for consistent key derivation across client/server
312 // This ensures the same password produces the same key on both sides
313 // Salt must be exactly ARGON2ID_SALT_SIZE (32) bytes
314 const char *deterministic_salt = "ascii-chat-password-salt-v1";
315 size_t salt_str_len = strlen(deterministic_salt);
316
317 // Zero-initialize the salt buffer first
318 memset(ctx->password_salt, 0, ctx->salt_size);
319
320 // Copy the salt string (will be padded with zeros to ctx->salt_size)
321 memcpy(ctx->password_salt, deterministic_salt, (salt_str_len < ctx->salt_size) ? salt_str_len : ctx->salt_size);
322
323 // Derive key using Argon2id (memory-hard, secure against GPU attacks)
324 if (crypto_pwhash(ctx->password_key, ctx->encryption_key_size, password, strlen(password), ctx->password_salt,
325 crypto_pwhash_OPSLIMIT_INTERACTIVE, // ~0.1 seconds
326 crypto_pwhash_MEMLIMIT_INTERACTIVE, // ~64MB
327 crypto_pwhash_ALG_DEFAULT) != 0) {
328 SET_ERRNO(ERROR_CRYPTO, "Password key derivation failed - possibly out of memory");
330 }
331
332 log_debug("Password key derived successfully using Argon2id with deterministic salt");
333 return CRYPTO_OK;
334}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_PASSWORD_DERIVATION, CRYPTO_OK, crypto_validate_password(), crypto_context_t::encryption_key_size, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::initialized, log_debug, crypto_context_t::password_key, crypto_context_t::password_salt, crypto_context_t::salt_size, and SET_ERRNO.

Referenced by client_crypto_init(), crypto_handshake_client_auth_response(), and crypto_init_with_password().

◆ crypto_encrypt()

crypto_result_t crypto_encrypt ( crypto_context_t ctx,
const uint8_t plaintext,
size_t  plaintext_len,
uint8_t ciphertext_out,
size_t  ciphertext_out_size,
size_t *  ciphertext_len_out 
)

#include <crypto.h>

Encrypt data using XSalsa20-Poly1305.

Parameters
ctxCrypto context (must be initialized and ready)
plaintextPlaintext data to encrypt
plaintext_lenLength of plaintext (must be > 0 and <= CRYPTO_MAX_PLAINTEXT_SIZE)
ciphertext_outOutput buffer for ciphertext
ciphertext_out_sizeSize of output buffer
ciphertext_len_outOutput parameter for actual ciphertext length
Returns
CRYPTO_OK on success, error code on failure

Encrypts data using XSalsa20-Poly1305 authenticated encryption. Uses shared_key if key exchange is complete, otherwise falls back to password_key.

Note
Nonce generation: Automatically generates nonce as session_id || counter. Nonce is prepended to ciphertext: [nonce:24][encrypted_data + MAC]. Counter increments after each encryption to prevent nonce reuse.
Ciphertext format: [nonce:nonce_size][encrypted_data][MAC:mac_size] Total size = plaintext_len + nonce_size + mac_size
Buffer requirements: ciphertext_out_size must be >= plaintext_len + nonce_size + mac_size
Nonce counter: Starts at 1 (0 reserved for testing). Returns CRYPTO_ERROR_NONCE_EXHAUSTED if counter reaches 0 or UINT64_MAX (extremely unlikely, but triggers rekeying).
Maximum plaintext size: CRYPTO_MAX_PLAINTEXT_SIZE (1MB)
Rekeying: Automatically increments rekey_packet_count. Check crypto_should_rekey() after encryption to determine if rekeying should be initiated.
Warning
Context must be ready (crypto_is_ready() returns true) before calling this function.

Definition at line 415 of file lib/crypto/crypto.c.

416 {
417 if (!ctx || !ctx->initialized || !plaintext || !ciphertext_out || !ciphertext_len_out) {
419 "Invalid parameters: ctx=%p, initialized=%d, plaintext=%p, ciphertext_out=%p, ciphertext_len_out=%p", ctx,
420 ctx ? ctx->initialized : 0, plaintext, ciphertext_out, ciphertext_len_out);
422 }
423
424 if (plaintext_len == 0 || plaintext_len > CRYPTO_MAX_PLAINTEXT_SIZE) {
425 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid plaintext length: %zu (max: %d)", plaintext_len, CRYPTO_MAX_PLAINTEXT_SIZE);
427 }
428
429 if (!crypto_is_ready(ctx)) {
430 SET_ERRNO(ERROR_CRYPTO, "Crypto context not ready for encryption");
432 }
433
434 // Check output buffer size
435 size_t required_size = plaintext_len + ctx->nonce_size + ctx->mac_size;
436 if (ciphertext_out_size < required_size) {
437 SET_ERRNO(ERROR_BUFFER, "Ciphertext buffer too small: %zu < %zu", ciphertext_out_size, required_size);
439 }
440
441 // Check for nonce counter exhaustion (extremely unlikely in practice)
442 // Starting from 1, reaching UINT64_MAX would require ~292 billion years at 60 FPS.
443 // With key rotation required at 1M packets (~16 seconds), exhaustion is virtually impossible.
444 // This check is a safety fallback that should never trigger in practice.
445 if (ctx->nonce_counter == 0 || ctx->nonce_counter == UINT64_MAX) {
446 SET_ERRNO(ERROR_CRYPTO, "Nonce counter exhausted - key rotation required");
448 }
449
450 // Generate nonce and place at beginning of ciphertext
451 uint8_t nonce[XSALSA20_NONCE_SIZE]; // Use maximum nonce size for buffer
452 generate_nonce(ctx, nonce);
453 SAFE_MEMCPY(ciphertext_out, ctx->nonce_size, nonce, ctx->nonce_size);
454
455 // Choose encryption key (prefer shared key over password key)
456 const uint8_t *encryption_key = NULL;
457 if (ctx->key_exchange_complete) {
458 encryption_key = ctx->shared_key;
459 } else if (ctx->has_password) {
460 encryption_key = ctx->password_key;
461 } else {
462 SET_ERRNO(ERROR_CRYPTO, "No encryption key available");
464 }
465
466 // Encrypt using NaCl secretbox (XSalsa20 + Poly1305)
467 if (crypto_secretbox_easy(ciphertext_out + ctx->nonce_size, plaintext, plaintext_len, nonce, encryption_key) != 0) {
468 SET_ERRNO(ERROR_CRYPTO, "Encryption failed");
470 }
471
472 *ciphertext_len_out = required_size;
473 ctx->bytes_encrypted += plaintext_len;
474
475 // Increment rekey packet counter for rekeying trigger detection
476 ctx->rekey_packet_count++;
477
478 return CRYPTO_OK;
479}
#define XSALSA20_NONCE_SIZE
XSalsa20 nonce size in bytes.

References crypto_context_t::bytes_encrypted, CRYPTO_ERROR_BUFFER_TOO_SMALL, CRYPTO_ERROR_ENCRYPTION, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE, CRYPTO_ERROR_NONCE_EXHAUSTED, crypto_is_ready(), CRYPTO_MAX_PLAINTEXT_SIZE, CRYPTO_OK, ERROR_BUFFER, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_password, crypto_context_t::initialized, crypto_context_t::key_exchange_complete, crypto_context_t::mac_size, crypto_context_t::nonce_counter, crypto_context_t::nonce_size, crypto_context_t::password_key, crypto_context_t::rekey_packet_count, SAFE_MEMCPY, SET_ERRNO, crypto_context_t::shared_key, and XSALSA20_NONCE_SIZE.

Referenced by crypto_create_encrypted_packet(), crypto_handshake_encrypt_packet(), crypto_handshake_rekey_complete(), and send_packet_secure().

◆ crypto_extract_auth_data()

void crypto_extract_auth_data ( const uint8_t combined_data,
uint8_t hmac_out,
uint8_t challenge_out 
)

#include <crypto.h>

Extract HMAC and challenge nonce from combined data.

Parameters
combined_dataCombined data received from peer (64 bytes)
hmac_outOutput buffer for HMAC (32 bytes)
challenge_outOutput buffer for challenge nonce (32 bytes)

Extracts HMAC and challenge nonce from combined buffer: [HMAC:32][nonce:32] Used to unpack authentication response data received from peer.

Note
Format: [HMAC:32][challenge_nonce:32] (64 bytes total)

Definition at line 1127 of file lib/crypto/crypto.c.

1127 {
1128 if (!combined_data || !hmac_out || !challenge_out) {
1129 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: combined_data=%p, hmac_out=%p, challenge_out=%p", combined_data,
1130 hmac_out, challenge_out);
1131 return;
1132 }
1133
1134 // Extract HMAC (first 32 bytes) and challenge nonce (last 32 bytes)
1135 memcpy(hmac_out, combined_data, 32);
1136 memcpy(challenge_out, combined_data + 32, 32);
1137}

References ERROR_INVALID_PARAM, and SET_ERRNO.

◆ crypto_generate_keypair()

crypto_result_t crypto_generate_keypair ( crypto_context_t ctx)

#include <crypto.h>

Generate new X25519 key pair for key exchange.

Parameters
ctxCrypto context
Returns
CRYPTO_OK on success, error code on failure

Generates a new ephemeral X25519 key pair. Called automatically by crypto_init(). Can be called again to regenerate keys (e.g., for rekeying).

Definition at line 201 of file lib/crypto/crypto.c.

201 {
202 if (!ctx) {
203 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_generate_keypair: NULL context");
205 }
206
207 // Generate X25519 key pair for key exchange
208 if (crypto_box_keypair(ctx->public_key, ctx->private_key) != 0) {
209 SET_ERRNO(ERROR_CRYPTO, "Failed to generate X25519 key pair");
211 }
212
213 log_debug("Generated X25519 key pair for key exchange");
214 return CRYPTO_OK;
215}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_GENERATION, CRYPTO_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, log_debug, crypto_context_t::private_key, crypto_context_t::public_key, and SET_ERRNO.

Referenced by crypto_init().

◆ crypto_generate_nonce()

crypto_result_t crypto_generate_nonce ( uint8_t  nonce[32])

#include <crypto.h>

Generate random nonce for authentication.

Parameters
nonceOutput buffer for 32-byte random nonce
Returns
CRYPTO_OK on success, error code on failure

Generates a cryptographically secure random nonce for authentication challenges. Uses libsodium's secure random number generator.

Note
Automatically initializes libsodium if not already initialized.

Definition at line 787 of file lib/crypto/crypto.c.

787 {
788 if (!nonce) {
789 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_generate_nonce: NULL nonce buffer");
791 }
792
793 crypto_result_t result = init_libsodium();
794 if (result != CRYPTO_OK) {
795 return result;
796 }
797
798 randombytes_buf(nonce, 32);
799 return CRYPTO_OK;
800}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by crypto_create_auth_challenge(), and crypto_handshake_server_auth_challenge().

◆ crypto_get_public_key()

crypto_result_t crypto_get_public_key ( const crypto_context_t ctx,
uint8_t public_key_out 
)

#include <crypto.h>

Get public key for sending to peer (step 1 of handshake)

Parameters
ctxCrypto context
public_key_outOutput buffer for public key (must be CRYPTO_PUBLIC_KEY_SIZE bytes)
Returns
CRYPTO_OK on success, error code on failure

Retrieves our ephemeral public key for transmission to the peer. This is the first step in the key exchange handshake.

Definition at line 221 of file lib/crypto/crypto.c.

221 {
222 if (!ctx || !ctx->initialized || !public_key_out) {
224 "crypto_get_public_key: Invalid parameters (ctx=%p, initialized=%d, public_key_out=%p)", ctx,
225 ctx ? ctx->initialized : 0, public_key_out);
227 }
228
229 // Bounds check to prevent buffer overflow
230 size_t copy_size = (ctx->public_key_size <= X25519_KEY_SIZE) ? ctx->public_key_size : X25519_KEY_SIZE;
231 SAFE_MEMCPY(public_key_out, copy_size, ctx->public_key, copy_size);
232 return CRYPTO_OK;
233}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_OK, ERROR_INVALID_PARAM, crypto_context_t::initialized, crypto_context_t::public_key, crypto_context_t::public_key_size, SAFE_MEMCPY, SET_ERRNO, and X25519_KEY_SIZE.

◆ crypto_get_rekey_status()

void crypto_get_rekey_status ( const crypto_context_t ctx,
char *  status_buffer,
size_t  buffer_size 
)

#include <crypto.h>

Get the current rekeying state for debugging/logging.

Parameters
ctxCrypto context
status_bufferOutput buffer for status string
buffer_sizeSize of status buffer

Formats a human-readable status string with rekeying state:

  • Packet count since last rekey
  • Time since last rekey
  • Rekey in progress status
  • Failure count
  • Threshold values
Note
Safe to call with NULL ctx or buffer (does nothing).

Definition at line 1337 of file lib/crypto/crypto.c.

1337 {
1338 if (!ctx || !status_buffer || buffer_size == 0) {
1339 return;
1340 }
1341
1342 time_t now = time(NULL);
1343 time_t elapsed = now - ctx->rekey_last_time;
1344 time_t remaining_time = (ctx->rekey_time_threshold > elapsed) ? (ctx->rekey_time_threshold - elapsed) : 0;
1345 uint64_t remaining_packets = (ctx->rekey_packet_threshold > ctx->rekey_packet_count)
1347 : 0;
1348
1349 snprintf(status_buffer, buffer_size,
1350 "Rekey status: %s | "
1351 "Packets: %llu/%llu (%llu remaining) | "
1352 "Time: %ld/%ld sec (%ld sec remaining) | "
1353 "Rekeys: %llu | Failures: %d",
1354 ctx->rekey_in_progress ? "IN_PROGRESS" : "IDLE", (unsigned long long)ctx->rekey_packet_count,
1355 (unsigned long long)ctx->rekey_packet_threshold, (unsigned long long)remaining_packets, (long)elapsed,
1356 (long)ctx->rekey_time_threshold, (long)remaining_time, (unsigned long long)ctx->rekey_count,
1357 ctx->rekey_failure_count);
1358}
uint64_t rekey_packet_threshold

References crypto_context_t::rekey_count, crypto_context_t::rekey_failure_count, crypto_context_t::rekey_in_progress, crypto_context_t::rekey_last_time, crypto_context_t::rekey_packet_count, crypto_context_t::rekey_packet_threshold, and crypto_context_t::rekey_time_threshold.

◆ crypto_get_status()

void crypto_get_status ( const crypto_context_t ctx,
char *  status_buffer,
size_t  buffer_size 
)

#include <crypto.h>

Get crypto context status information for debugging.

Parameters
ctxCrypto context
status_bufferOutput buffer for status string
buffer_sizeSize of status buffer

Formats a human-readable status string with context state information:

  • Initialization status
  • Password status
  • Key exchange status
  • Ready status
  • Encrypted/decrypted byte counts
  • Nonce counter value
Note
Safe to call with NULL ctx or buffer (does nothing).

Definition at line 579 of file lib/crypto/crypto.c.

579 {
580 if (!ctx || !status_buffer || buffer_size == 0) {
581 return;
582 }
583
584 if (!ctx->initialized) {
585 SAFE_SNPRINTF(status_buffer, buffer_size, "Not initialized");
586 return;
587 }
588
589 SAFE_SNPRINTF(status_buffer, buffer_size,
590 "Initialized: %s, Password: %s, Key Exchange: %s, Ready: %s, "
591 "Encrypted: %" PRIu64 " bytes, Decrypted: %" PRIu64 " bytes, Nonce: %" PRIu64,
592 ctx->initialized ? "yes" : "no", ctx->has_password ? "yes" : "no",
593 ctx->key_exchange_complete ? "complete" : "incomplete", crypto_is_ready(ctx) ? "yes" : "no",
594 ctx->bytes_encrypted, ctx->bytes_decrypted, ctx->nonce_counter);
595}
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:412

References crypto_context_t::bytes_decrypted, crypto_context_t::bytes_encrypted, crypto_is_ready(), crypto_context_t::has_password, crypto_context_t::initialized, crypto_context_t::key_exchange_complete, crypto_context_t::nonce_counter, and SAFE_SNPRINTF.

◆ crypto_init()

crypto_result_t crypto_init ( crypto_context_t ctx)

#include <crypto.h>

Initialize libsodium and crypto context.

Parameters
ctxCrypto context to initialize
Returns
CRYPTO_OK on success, error code on failure

Initializes libsodium (thread-safe, idempotent) and generates a new X25519 key pair for key exchange.

Note
libsodium initialization is global and thread-safe. Multiple calls to crypto_init() will only initialize libsodium once.
Generates a new ephemeral key pair automatically. The nonce counter starts at 1 (0 is reserved for testing).

Definition at line 78 of file lib/crypto/crypto.c.

78 {
79 if (!ctx) {
80 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
82 }
83
84 // Initialize libsodium
85 crypto_result_t result = init_libsodium();
86 if (result != CRYPTO_OK) {
87 return result;
88 }
89
90 // Clear context
91 secure_memzero(ctx, sizeof(crypto_context_t));
92
93 // Generate key pair for X25519 key exchange
94 result = crypto_generate_keypair(ctx);
95 if (result != CRYPTO_OK) {
96 return result;
97 }
98
99 ctx->initialized = true;
100 ctx->has_password = false;
101 ctx->key_exchange_complete = false;
102 ctx->peer_key_received = false;
103 ctx->handshake_complete = false;
104 ctx->nonce_counter = 1; // Start from 1 (0 reserved for testing)
105 ctx->bytes_encrypted = 0;
106 ctx->bytes_decrypted = 0;
107
108 // Set default algorithm-specific parameters (can be overridden during handshake)
119
120 // Generate unique session ID to prevent replay attacks across connections
121 randombytes_buf(ctx->session_id, sizeof(ctx->session_id));
122
123 // Initialize rekeying state
124 ctx->rekey_packet_count = 0;
125 ctx->rekey_last_time = time(NULL); // Initialize to current time
126 ctx->rekey_last_request_time = 0; // No rekey request yet
127 ctx->rekey_in_progress = false;
128 ctx->rekey_failure_count = 0;
129 ctx->has_temp_key = false;
130 ctx->rekey_count = 0;
131
132 // SECURITY: Use production-safe rekey thresholds by default
133 // Rekey every 1 hour OR 1 million packets (whichever comes first)
134 // Only use test mode if explicitly requested via environment variable
135 if (is_test_environment()) {
138 char duration_str[32];
139 format_duration_s((double)ctx->rekey_time_threshold, duration_str, sizeof(duration_str));
140 log_info("Crypto context initialized with X25519 key exchange (TEST MODE rekey thresholds: %llu packets, %s)",
141 (unsigned long long)ctx->rekey_packet_threshold, duration_str);
142 } else {
143 ctx->rekey_packet_threshold = REKEY_DEFAULT_PACKET_THRESHOLD; // 1 million packets
144 ctx->rekey_time_threshold = REKEY_DEFAULT_TIME_THRESHOLD; // 3600 seconds (1 hour)
145 char duration_str[32];
146 format_duration_s((double)ctx->rekey_time_threshold, duration_str, sizeof(duration_str));
147 log_info("Crypto context initialized with X25519 key exchange (rekey thresholds: %llu packets, %s)",
148 (unsigned long long)ctx->rekey_packet_threshold, duration_str);
149 }
150 return CRYPTO_OK;
151}
#define ED25519_SIGNATURE_SIZE
Ed25519 signature size in bytes.
#define AUTH_CHALLENGE_SIZE
Challenge nonce size (32 bytes)
#define HMAC_SHA256_SIZE
HMAC-SHA256 output size in bytes.
#define REKEY_DEFAULT_TIME_THRESHOLD
Default rekey time threshold (1 hour in seconds)
crypto_result_t crypto_generate_keypair(crypto_context_t *ctx)
Generate new X25519 key pair for key exchange.
#define REKEY_TEST_PACKET_THRESHOLD
Test mode rekey packet threshold (1000 packets)
#define REKEY_TEST_TIME_THRESHOLD
Test mode rekey time threshold (30 seconds)
#define REKEY_DEFAULT_PACKET_THRESHOLD
Default rekey packet threshold (1 million packets)
#define POLY1305_MAC_SIZE
Poly1305 MAC size in bytes.
int format_duration_s(double seconds, char *buffer, size_t buffer_size)
Format seconds as human-readable duration string.
Definition time.c:275

References ARGON2ID_SALT_SIZE, AUTH_CHALLENGE_SIZE, crypto_context_t::auth_challenge_size, crypto_context_t::bytes_decrypted, crypto_context_t::bytes_encrypted, CRYPTO_ERROR_INVALID_PARAMS, crypto_generate_keypair(), CRYPTO_OK, ED25519_SIGNATURE_SIZE, crypto_context_t::encryption_key_size, ERROR_INVALID_PARAM, format_duration_s(), crypto_context_t::handshake_complete, crypto_context_t::has_password, crypto_context_t::has_temp_key, HMAC_SHA256_SIZE, crypto_context_t::hmac_size, crypto_context_t::initialized, crypto_context_t::key_exchange_complete, log_info, crypto_context_t::mac_size, crypto_context_t::nonce_counter, crypto_context_t::nonce_size, crypto_context_t::peer_key_received, POLY1305_MAC_SIZE, crypto_context_t::private_key_size, crypto_context_t::public_key_size, crypto_context_t::rekey_count, REKEY_DEFAULT_PACKET_THRESHOLD, REKEY_DEFAULT_TIME_THRESHOLD, crypto_context_t::rekey_failure_count, crypto_context_t::rekey_in_progress, crypto_context_t::rekey_last_request_time, crypto_context_t::rekey_last_time, crypto_context_t::rekey_packet_count, crypto_context_t::rekey_packet_threshold, REKEY_TEST_PACKET_THRESHOLD, REKEY_TEST_TIME_THRESHOLD, crypto_context_t::rekey_time_threshold, crypto_context_t::salt_size, SECRETBOX_KEY_SIZE, crypto_context_t::session_id, SET_ERRNO, crypto_context_t::shared_key_size, crypto_context_t::signature_size, X25519_KEY_SIZE, and XSALSA20_NONCE_SIZE.

Referenced by crypto_handshake_init(), and crypto_init_with_password().

◆ crypto_init_with_password()

crypto_result_t crypto_init_with_password ( crypto_context_t ctx,
const char *  password 
)

#include <crypto.h>

Initialize with password-based encryption.

Parameters
ctxCrypto context to initialize
passwordPassword for key derivation
Returns
CRYPTO_OK on success, error code on failure

Initializes context and derives encryption key from password using Argon2id. The password is used as an additional layer on top of DH key exchange.

Note
Password must meet length requirements (8-256 characters).

Definition at line 153 of file lib/crypto/crypto.c.

153 {
154 if (!ctx || !password) {
155 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, password=%p", ctx, password);
157 }
158
159 if (strlen(password) == 0) {
160 SET_ERRNO(ERROR_INVALID_PARAM, "Password cannot be empty");
162 }
163
164 // First initialize basic crypto
165 crypto_result_t result = crypto_init(ctx);
166 if (result != CRYPTO_OK) {
167 return result;
168 }
169
170 // Derive password key
171 result = crypto_derive_password_key(ctx, password);
172 if (result != CRYPTO_OK) {
173 crypto_cleanup(ctx);
174 return result;
175 }
176
177 ctx->has_password = true;
178
179 log_info("Crypto context initialized with password-based encryption");
180 return CRYPTO_OK;
181}
void crypto_cleanup(crypto_context_t *ctx)
Cleanup crypto context with secure memory wiping.
crypto_result_t crypto_init(crypto_context_t *ctx)
Initialize libsodium and crypto context.
crypto_result_t crypto_derive_password_key(crypto_context_t *ctx, const char *password)
Derive key from password using Argon2id.

References crypto_cleanup(), crypto_derive_password_key(), CRYPTO_ERROR_INVALID_PARAMS, crypto_init(), CRYPTO_OK, ERROR_INVALID_PARAM, crypto_context_t::has_password, log_info, and SET_ERRNO.

Referenced by crypto_handshake_init_with_password().

◆ crypto_is_ready()

bool crypto_is_ready ( const crypto_context_t ctx)

#include <crypto.h>

Check if key exchange is complete and ready for encryption.

Parameters
ctxCrypto context
Returns
true if key exchange is complete, false otherwise

Returns true only after both parties have exchanged public keys and the shared secret has been computed.

Definition at line 261 of file lib/crypto/crypto.c.

261 {
262 if (!ctx || !ctx->initialized) {
263 return false;
264 }
265
266 // Ready if either key exchange is complete OR password is set
267 return ctx->key_exchange_complete || ctx->has_password;
268}

References crypto_context_t::has_password, crypto_context_t::initialized, and crypto_context_t::key_exchange_complete.

Referenced by crypto_create_encrypted_packet(), crypto_decrypt(), crypto_encrypt(), crypto_get_status(), crypto_handshake_is_ready(), crypto_process_encrypted_packet(), packet_send_error(), packet_send_remote_log(), and send_packet_secure().

◆ crypto_process_auth_challenge()

crypto_result_t crypto_process_auth_challenge ( crypto_context_t ctx,
const uint8_t packet,
size_t  packet_len 
)

#include <crypto.h>

Process authentication challenge packet.

Parameters
ctxCrypto context (must be initialized)
packetChallenge packet received from peer
packet_lenLength of challenge packet
Returns
CRYPTO_OK on success, error code on failure

Processes an authentication challenge packet: [type:4][nonce:auth_challenge_size] Extracts the nonce and stores it in ctx->auth_nonce for generating the response.

Note
Packet format: [PACKET_TYPE_AUTH_CHALLENGE:4][nonce:32] Expected size: sizeof(uint32_t) + ctx->auth_challenge_size (typically 36 bytes)
The extracted nonce is stored in ctx->auth_nonce and should be used with crypto_compute_auth_response() to generate the authentication response.

Definition at line 971 of file lib/crypto/crypto.c.

971 {
972 if (!ctx || !ctx->initialized || !packet) {
974 "crypto_process_auth_challenge: Invalid parameters (ctx=%p, initialized=%d, packet=%p)", ctx,
975 ctx ? ctx->initialized : 0, packet);
977 }
978
979 size_t expected_size = sizeof(uint32_t) + ctx->auth_challenge_size; // type + nonce
980 if (packet_len != expected_size) {
981 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_challenge: Invalid packet size (expected=%zu, got=%zu)",
982 expected_size, packet_len);
984 }
985
986 // Unpack packet: [type:4][nonce:auth_challenge_size]
987 uint32_t packet_type;
988 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
989
990 if (packet_type != CRYPTO_PACKET_AUTH_CHALLENGE) {
991 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_challenge: Invalid packet type (expected=%u, got=%u)",
992 CRYPTO_PACKET_AUTH_CHALLENGE, packet_type);
994 }
995
996 // Store the nonce for HMAC computation (use buffer size for memcpy size, actual size from context)
997 SAFE_MEMCPY(ctx->auth_nonce, sizeof(ctx->auth_nonce), packet + sizeof(packet_type), ctx->auth_challenge_size);
998
999 log_debug("Auth challenge received and processed");
1000 return CRYPTO_OK;
1001}

References crypto_context_t::auth_challenge_size, crypto_context_t::auth_nonce, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_OK, ERROR_INVALID_PARAM, crypto_context_t::initialized, log_debug, SAFE_MEMCPY, and SET_ERRNO.

◆ crypto_process_auth_response()

crypto_result_t crypto_process_auth_response ( crypto_context_t ctx,
const uint8_t packet,
size_t  packet_len 
)

#include <crypto.h>

Process authentication response packet.

Parameters
ctxCrypto context (must be initialized and have completed key exchange)
packetResponse packet received from peer
packet_lenLength of response packet
Returns
CRYPTO_OK on success, error code on failure

Processes an authentication response packet containing HMAC. Verifies the HMAC using crypto_verify_auth_response().

Note
Packet format depends on authentication method:
  • Password: [HMAC:32][challenge_nonce:32] (64 bytes)
  • Signature: [signature:64][challenge_nonce:32] (96 bytes)
Warning
Context must have completed key exchange before calling this function.

Definition at line 1003 of file lib/crypto/crypto.c.

1003 {
1004 if (!ctx || !ctx->initialized || !packet) {
1006 "crypto_process_auth_response: Invalid context or packet (ctx=%p, initialized=%d, packet=%p)", ctx,
1007 ctx ? ctx->initialized : 0, packet);
1009 }
1010
1011 size_t expected_size = sizeof(uint32_t) + 32; // type + hmac
1012 if (packet_len != expected_size) {
1013 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_response: Invalid packet size (expected=%zu, got=%zu)",
1014 expected_size, packet_len);
1016 }
1017
1018 // Unpack packet: [type:4][hmac:32]
1019 uint32_t packet_type;
1020 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
1021
1022 if (packet_type != CRYPTO_PACKET_AUTH_RESPONSE) {
1023 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_response: Invalid packet type (expected=0x%x, got=0x%x)",
1024 CRYPTO_PACKET_AUTH_RESPONSE, packet_type);
1026 }
1027
1028 const uint8_t *received_hmac = packet + sizeof(packet_type);
1029
1030 // Verify HMAC using shared secret
1031 if (!crypto_verify_hmac(ctx->shared_key, ctx->auth_nonce, received_hmac)) {
1032 SET_ERRNO(ERROR_CRYPTO, "crypto_process_auth_response: HMAC verification failed");
1034 }
1035
1036 ctx->handshake_complete = true;
1037 log_debug("Authentication successful - handshake complete");
1038 return CRYPTO_OK;
1039}
bool crypto_verify_hmac(const uint8_t key[32], const uint8_t data[32], const uint8_t expected_hmac[32])
Verify HMAC-SHA256 for fixed 32-byte data.

References crypto_context_t::auth_nonce, CRYPTO_ERROR_INVALID_MAC, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_OK, crypto_verify_hmac(), ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::handshake_complete, crypto_context_t::initialized, log_debug, SAFE_MEMCPY, SET_ERRNO, and crypto_context_t::shared_key.

◆ crypto_process_encrypted_packet()

crypto_result_t crypto_process_encrypted_packet ( crypto_context_t ctx,
const uint8_t packet,
size_t  packet_len,
uint8_t data_out,
size_t  data_size,
size_t *  data_len_out 
)

#include <crypto.h>

Process received encrypted packet from peer.

Parameters
ctxCrypto context (must be initialized and ready)
packetEncrypted packet received from peer
packet_lenLength of packet
data_outOutput buffer for decrypted plaintext
data_sizeSize of output buffer
data_len_outOutput parameter for actual plaintext length
Returns
CRYPTO_OK on success, CRYPTO_ERROR_INVALID_MAC if MAC verification fails

Processes an encrypted data packet: [type:4][length:4][encrypted_data:var] Decrypts the data using crypto_decrypt().

Note
Packet format: [PACKET_TYPE_ENCRYPTED_DATA:4][data_length:4][encrypted_data] Encrypted data format: [nonce:24][encrypted_data][MAC:16] Plaintext size: data_length - nonce_size - mac_size
Context must be ready (crypto_is_ready() returns true) before calling.
Warning
Always check return value. CRYPTO_ERROR_INVALID_MAC indicates tampering or wrong key.

Definition at line 742 of file lib/crypto/crypto.c.

743 {
744 if (!ctx || !packet || !data_out || !data_len_out) {
746 "crypto_process_encrypted_packet: Invalid parameters (ctx=%p, packet=%p, data_out=%p, data_len_out=%p)",
747 ctx, packet, data_out, data_len_out);
749 }
750
751 if (!crypto_is_ready(ctx)) {
752 SET_ERRNO(ERROR_CRYPTO, "crypto_process_encrypted_packet: Crypto context not ready");
754 }
755
756 if (packet_len < sizeof(uint32_t) + sizeof(uint32_t)) {
757 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_encrypted_packet: Packet too small (size=%zu)", packet_len);
759 }
760
761 // Unpack packet: [type:4][length:4][encrypted_data:var]
762 uint32_t packet_type;
763 uint32_t data_length;
764 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
765 SAFE_MEMCPY(&data_length, sizeof(data_length), packet + sizeof(packet_type), sizeof(data_length));
766
767 if (packet_type != CRYPTO_PACKET_ENCRYPTED_DATA) {
768 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_encrypted_packet: Invalid packet type (expected=%u, got=%u)",
769 CRYPTO_PACKET_ENCRYPTED_DATA, packet_type);
771 }
772
773 if (packet_len != sizeof(uint32_t) + sizeof(uint32_t) + data_length) {
774 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_encrypted_packet: Packet length mismatch (expected=%zu, got=%zu)",
775 sizeof(uint32_t) + sizeof(uint32_t) + data_length, packet_len);
777 }
778
779 const uint8_t *encrypted_data = packet + sizeof(uint32_t) + sizeof(uint32_t);
780 return crypto_decrypt(ctx, encrypted_data, data_length, data_out, data_size, data_len_out);
781}
crypto_result_t crypto_decrypt(crypto_context_t *ctx, const uint8_t *ciphertext, size_t ciphertext_len, uint8_t *plaintext_out, size_t plaintext_out_size, size_t *plaintext_len_out)
Decrypt data using XSalsa20-Poly1305.

References crypto_decrypt(), CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE, crypto_is_ready(), ERROR_CRYPTO, ERROR_INVALID_PARAM, SAFE_MEMCPY, and SET_ERRNO.

◆ crypto_process_public_key_packet()

crypto_result_t crypto_process_public_key_packet ( crypto_context_t ctx,
const uint8_t packet,
size_t  packet_len 
)

#include <crypto.h>

Process received public key packet from peer.

Parameters
ctxCrypto context (must be initialized)
packetPublic key packet received from peer
packet_lenLength of packet
Returns
CRYPTO_OK on success, error code on failure

Processes a public key packet: [type:4][public_key:public_key_size] Extracts peer's public key and computes shared secret automatically.

Note
Packet format: [PACKET_TYPE_PUBLIC_KEY:4][peer_public_key:32] Expected size: sizeof(uint32_t) + ctx->public_key_size (typically 36 bytes)
Automatically computes shared secret via crypto_set_peer_public_key(). After this call, crypto_is_ready() may return true if key exchange is complete.

Definition at line 651 of file lib/crypto/crypto.c.

651 {
652 if (!ctx || !ctx->initialized || !packet) {
654 "crypto_process_public_key_packet: Invalid parameters (ctx=%p, initialized=%d, packet=%p)", ctx,
655 ctx ? ctx->initialized : 0, packet);
657 }
658
659 size_t expected_size = sizeof(uint32_t) + ctx->public_key_size;
660 if (packet_len != expected_size) {
661 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_public_key_packet: Invalid packet size (expected=%zu, got=%zu)",
662 expected_size, packet_len);
664 }
665
666 // Unpack packet: [type:4][public_key:32]
667 uint32_t packet_type;
668 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
669
670 if (packet_type != CRYPTO_PACKET_PUBLIC_KEY) {
671 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_public_key_packet: Invalid packet type (expected=%u, got=%u)",
672 CRYPTO_PACKET_PUBLIC_KEY, packet_type);
674 }
675
676 const uint8_t *peer_public_key = packet + sizeof(packet_type);
677 return crypto_set_peer_public_key(ctx, peer_public_key);
678}
crypto_result_t crypto_set_peer_public_key(crypto_context_t *ctx, const uint8_t *peer_public_key)
Set peer's public key and compute shared secret (step 2 of handshake)

References CRYPTO_ERROR_INVALID_PARAMS, crypto_set_peer_public_key(), ERROR_INVALID_PARAM, crypto_context_t::initialized, crypto_context_t::public_key_size, SAFE_MEMCPY, and SET_ERRNO.

◆ crypto_random_bytes()

crypto_result_t crypto_random_bytes ( uint8_t buffer,
size_t  len 
)

#include <crypto.h>

Generate cryptographically secure random bytes.

Parameters
bufferOutput buffer for random bytes
lenNumber of random bytes to generate (must be > 0)
Returns
CRYPTO_OK on success, error code on failure

Uses libsodium's secure random number generator (randombytes_buf). Suitable for generating nonces, keys, salts, and other cryptographic material.

Note
Automatically initializes libsodium if not already initialized.
Warning
Returns CRYPTO_ERROR_INVALID_PARAMS if buffer is NULL or len is 0.

Definition at line 604 of file lib/crypto/crypto.c.

604 {
605 if (!buffer || len == 0) {
606 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_random_bytes: Invalid parameters (buffer=%p, len=%zu)", buffer, len);
608 }
609
610 crypto_result_t result = init_libsodium();
611 if (result != CRYPTO_OK) {
612 return result;
613 }
614
615 randombytes_buf(buffer, len);
616 return CRYPTO_OK;
617}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ crypto_rekey_abort()

void crypto_rekey_abort ( crypto_context_t ctx)

#include <crypto.h>

Abort rekeying and fallback to old keys.

Parameters
ctxCrypto context

Aborts ongoing rekey and clears temp_* keys. Called on rekey failure (timeout, bad keys, decryption failure, etc.).

Note
Clears temp_* keys and sets rekey_in_progress = false.
Old keys remain active (no service interruption).
Increments rekey_failure_count for exponential backoff.

Definition at line 1315 of file lib/crypto/crypto.c.

1315 {
1316 if (!ctx) {
1317 return;
1318 }
1319
1320 log_warn("Aborting rekey (attempt %d failed), keeping old encryption key", ctx->rekey_failure_count + 1);
1321
1322 // Wipe temporary keys securely
1323 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1324 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1325 secure_memzero(ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1326
1327 // Reset rekey state
1328 ctx->has_temp_key = false;
1329 ctx->rekey_in_progress = false;
1330
1331 // Increment failure counter for exponential backoff
1332 ctx->rekey_failure_count++;
1333
1334 // Continue using old key - no disruption to connection
1335}

References crypto_context_t::has_temp_key, log_warn, crypto_context_t::rekey_failure_count, crypto_context_t::rekey_in_progress, crypto_context_t::temp_private_key, crypto_context_t::temp_public_key, and crypto_context_t::temp_shared_key.

Referenced by crypto_handshake_process_rekey_complete(), crypto_handshake_process_rekey_request(), crypto_handshake_process_rekey_response(), crypto_handshake_rekey_complete(), crypto_handshake_rekey_request(), crypto_handshake_rekey_response(), and crypto_rekey_process_response().

◆ crypto_rekey_commit()

crypto_result_t crypto_rekey_commit ( crypto_context_t ctx)

#include <crypto.h>

Commit to new keys after successful REKEY_COMPLETE.

Parameters
ctxCrypto context
Returns
CRYPTO_OK on success, error code on failure

Switches from old shared_key to temp_shared_key, resets counters. Called after REKEY_COMPLETE is verified (decrypts successfully with new key).

Note
Replaces shared_key with temp_shared_key.
Resets rekey_packet_count, rekey_last_time, and rekey_in_progress.
Clears temp_* keys (they are now the active keys).
Warning
Only call after verifying REKEY_COMPLETE decrypts with new key.

Definition at line 1271 of file lib/crypto/crypto.c.

1271 {
1272 if (!ctx || !ctx->initialized) {
1273 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_commit: Invalid context");
1275 }
1276
1277 if (!ctx->rekey_in_progress || !ctx->has_temp_key) {
1278 SET_ERRNO(ERROR_CRYPTO, "No rekey in progress to commit");
1280 }
1281
1282 // Wipe old shared secret securely
1283 secure_memzero(ctx->shared_key, sizeof(ctx->shared_key));
1284
1285 // Switch to new shared secret
1286 SAFE_MEMCPY(ctx->shared_key, sizeof(ctx->shared_key), ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1287
1288 // Reset nonce counter to 1 (fresh start with new key)
1289 ctx->nonce_counter = 1;
1290
1291 // Generate new session ID to prevent cross-session replay
1292 randombytes_buf(ctx->session_id, sizeof(ctx->session_id));
1293
1294 // Reset rekey tracking
1295 ctx->rekey_packet_count = 0;
1296 ctx->rekey_last_time = time(NULL);
1297 ctx->rekey_count++;
1298
1299 // Clear rekey state
1300 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1301 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1302 secure_memzero(ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1303 ctx->has_temp_key = false;
1304 ctx->rekey_in_progress = false;
1305
1306 // Reset failure counter on successful rekey
1307 ctx->rekey_failure_count = 0;
1308
1309 log_info("Rekey committed successfully (rekey #%llu, nonce reset to 1, new session_id generated)",
1310 (unsigned long long)ctx->rekey_count);
1311
1312 return CRYPTO_OK;
1313}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_REKEY_FAILED, CRYPTO_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_temp_key, crypto_context_t::initialized, log_info, crypto_context_t::nonce_counter, crypto_context_t::rekey_count, crypto_context_t::rekey_failure_count, crypto_context_t::rekey_in_progress, crypto_context_t::rekey_last_time, crypto_context_t::rekey_packet_count, SAFE_MEMCPY, crypto_context_t::session_id, SET_ERRNO, crypto_context_t::shared_key, crypto_context_t::temp_private_key, crypto_context_t::temp_public_key, and crypto_context_t::temp_shared_key.

Referenced by crypto_handshake_process_rekey_complete(), and crypto_handshake_rekey_complete().

◆ crypto_rekey_init()

crypto_result_t crypto_rekey_init ( crypto_context_t ctx)

#include <crypto.h>

Initiate rekeying by generating new ephemeral keys.

Parameters
ctxCrypto context
Returns
CRYPTO_OK on success, error code on failure

Generates new ephemeral key pair and stores in temp_* fields. Called by the initiator (client or server) before sending REKEY_REQUEST.

Note
New keys are stored in temp_public_key and temp_private_key. They are NOT active until crypto_rekey_commit() is called.
Sets ctx->rekey_in_progress = true and updates rekey_last_request_time.
Warning
Do not call if rekey is already in progress.

Definition at line 1177 of file lib/crypto/crypto.c.

1177 {
1178 if (!ctx || !ctx->initialized) {
1179 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_init: Invalid context");
1181 }
1182
1183 if (ctx->rekey_in_progress) {
1184 SET_ERRNO(ERROR_CRYPTO, "Rekey already in progress");
1186 }
1187
1188 // Rate limiting: check minimum interval since last rekey
1189 time_t now = time(NULL);
1190 time_t since_last_rekey = now - ctx->rekey_last_time;
1191 if (since_last_rekey < REKEY_MIN_INTERVAL) {
1192 char elapsed_str[32], min_str[32];
1193 format_duration_s((double)since_last_rekey, elapsed_str, sizeof(elapsed_str));
1194 format_duration_s((double)REKEY_MIN_INTERVAL, min_str, sizeof(min_str));
1195 log_warn("Rekey rate limited: %s since last rekey (minimum: %s)", elapsed_str, min_str);
1197 }
1198
1199 // Check if too many consecutive failures
1201 log_error("Too many consecutive rekey failures (%d), giving up", ctx->rekey_failure_count);
1203 }
1204
1205 // Generate new ephemeral X25519 keypair for rekeying
1206 if (crypto_box_keypair(ctx->temp_public_key, ctx->temp_private_key) != 0) {
1207 SET_ERRNO(ERROR_CRYPTO, "Failed to generate rekey ephemeral keypair");
1209 }
1210
1211 ctx->rekey_in_progress = true;
1212 ctx->has_temp_key = true;
1213
1214 log_info("Rekey initiated (packets: %llu, time elapsed: %ld sec, attempt %d)",
1215 (unsigned long long)ctx->rekey_packet_count, (long)since_last_rekey, ctx->rekey_failure_count + 1);
1216
1217 return CRYPTO_OK;
1218}
#define REKEY_MIN_INTERVAL
Minimum time interval between rekey requests (3 seconds for testing, 60 for production)
#define REKEY_MAX_FAILURE_COUNT
Maximum consecutive rekey failures before giving up.

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_GENERATION, CRYPTO_ERROR_REKEY_FAILED, CRYPTO_ERROR_REKEY_IN_PROGRESS, CRYPTO_ERROR_REKEY_RATE_LIMITED, CRYPTO_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, format_duration_s(), crypto_context_t::has_temp_key, crypto_context_t::initialized, log_error, log_info, log_warn, crypto_context_t::rekey_failure_count, crypto_context_t::rekey_in_progress, crypto_context_t::rekey_last_time, REKEY_MAX_FAILURE_COUNT, REKEY_MIN_INTERVAL, crypto_context_t::rekey_packet_count, SET_ERRNO, crypto_context_t::temp_private_key, and crypto_context_t::temp_public_key.

Referenced by crypto_handshake_process_rekey_request(), and crypto_handshake_rekey_request().

◆ crypto_rekey_process_request()

crypto_result_t crypto_rekey_process_request ( crypto_context_t ctx,
const uint8_t peer_new_public_key 
)

#include <crypto.h>

Process REKEY_REQUEST from peer (responder side)

Parameters
ctxCrypto context
peer_new_public_keyPeer's new ephemeral public key (32 bytes)
Returns
CRYPTO_OK on success, error code on failure

Processes peer's new ephemeral public key from REKEY_REQUEST. Generates our own new ephemeral keys and computes new shared secret.

Note
Generates new temp_* keys and computes temp_shared_key. Keys are NOT active until crypto_rekey_commit() is called.
Should send REKEY_RESPONSE with our new public key after this call.

Definition at line 1220 of file lib/crypto/crypto.c.

1220 {
1221 if (!ctx || !ctx->initialized || !peer_new_public_key) {
1222 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_process_request: Invalid parameters");
1224 }
1225
1226 // Generate our own new ephemeral keypair (responder side)
1227 if (crypto_box_keypair(ctx->temp_public_key, ctx->temp_private_key) != 0) {
1228 SET_ERRNO(ERROR_CRYPTO, "Failed to generate rekey ephemeral keypair");
1230 }
1231
1232 // Compute new shared secret: DH(our_new_private_key, peer_new_public_key)
1233 if (crypto_box_beforenm(ctx->temp_shared_key, peer_new_public_key, ctx->temp_private_key) != 0) {
1234 SET_ERRNO(ERROR_CRYPTO, "Failed to compute rekey shared secret");
1235 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1236 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1238 }
1239
1240 ctx->rekey_in_progress = true;
1241 ctx->has_temp_key = true;
1242
1243 log_debug("Rekey request processed (responder side), new shared secret computed");
1244
1245 return CRYPTO_OK;
1246}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_GENERATION, CRYPTO_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_temp_key, crypto_context_t::initialized, log_debug, crypto_context_t::rekey_in_progress, SET_ERRNO, crypto_context_t::temp_private_key, crypto_context_t::temp_public_key, and crypto_context_t::temp_shared_key.

Referenced by crypto_handshake_process_rekey_request().

◆ crypto_rekey_process_response()

crypto_result_t crypto_rekey_process_response ( crypto_context_t ctx,
const uint8_t peer_new_public_key 
)

#include <crypto.h>

Process REKEY_RESPONSE from peer (initiator side)

Parameters
ctxCrypto context
peer_new_public_keyPeer's new ephemeral public key (32 bytes)
Returns
CRYPTO_OK on success, error code on failure

Processes peer's new ephemeral public key from REKEY_RESPONSE. Computes new shared secret using our temp_private_key and peer's temp_public_key.

Note
Computes temp_shared_key. Keys are NOT active until crypto_rekey_commit() is called.
Should send REKEY_COMPLETE encrypted with NEW key after this call.

Definition at line 1248 of file lib/crypto/crypto.c.

1248 {
1249 if (!ctx || !ctx->initialized || !peer_new_public_key) {
1250 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_process_response: Invalid parameters");
1252 }
1253
1254 if (!ctx->rekey_in_progress || !ctx->has_temp_key) {
1255 SET_ERRNO(ERROR_CRYPTO, "No rekey in progress");
1257 }
1258
1259 // Compute new shared secret: DH(our_new_private_key, peer_new_public_key)
1260 if (crypto_box_beforenm(ctx->temp_shared_key, peer_new_public_key, ctx->temp_private_key) != 0) {
1261 SET_ERRNO(ERROR_CRYPTO, "Failed to compute rekey shared secret");
1262 crypto_rekey_abort(ctx);
1264 }
1265
1266 log_debug("Rekey response processed (initiator side), new shared secret computed");
1267
1268 return CRYPTO_OK;
1269}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_GENERATION, CRYPTO_ERROR_REKEY_FAILED, CRYPTO_OK, crypto_rekey_abort(), ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_temp_key, crypto_context_t::initialized, log_debug, crypto_context_t::rekey_in_progress, SET_ERRNO, crypto_context_t::temp_private_key, and crypto_context_t::temp_shared_key.

Referenced by crypto_handshake_process_rekey_response().

◆ crypto_result_to_string()

const char * crypto_result_to_string ( crypto_result_t  result)

#include <crypto.h>

Convert crypto result to human-readable string.

Parameters
resultCrypto result code
Returns
Human-readable error string (never NULL)

Returns a descriptive string for each crypto_result_t value. Useful for logging and error reporting.

Definition at line 540 of file lib/crypto/crypto.c.

540 {
541 switch (result) {
542 case CRYPTO_OK:
543 return "Success";
545 return "Initialization failed";
547 return "Invalid parameters";
549 return "Memory allocation failed";
551 return "Libsodium error";
553 return "Key generation failed";
555 return "Password derivation failed";
557 return "Encryption failed";
559 return "Decryption failed";
561 return "Invalid MAC or corrupted data";
563 return "Buffer too small";
565 return "Key exchange not complete";
567 return "Nonce counter exhausted";
569 return "Rekey already in progress";
571 return "Rekey failed";
573 return "Rekey rate limited";
574 default:
575 return "Unknown error";
576 }
577}

References CRYPTO_ERROR_BUFFER_TOO_SMALL, CRYPTO_ERROR_DECRYPTION, CRYPTO_ERROR_ENCRYPTION, CRYPTO_ERROR_INIT_FAILED, CRYPTO_ERROR_INVALID_MAC, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_EXCHANGE_INCOMPLETE, CRYPTO_ERROR_KEY_GENERATION, CRYPTO_ERROR_LIBSODIUM, CRYPTO_ERROR_MEMORY, CRYPTO_ERROR_NONCE_EXHAUSTED, CRYPTO_ERROR_PASSWORD_DERIVATION, CRYPTO_ERROR_REKEY_FAILED, CRYPTO_ERROR_REKEY_IN_PROGRESS, CRYPTO_ERROR_REKEY_RATE_LIMITED, and CRYPTO_OK.

Referenced by client_crypto_init(), crypto_handshake_client_auth_response(), crypto_handshake_client_key_exchange(), crypto_handshake_decrypt_packet(), crypto_handshake_encrypt_packet(), crypto_handshake_init(), crypto_handshake_init_with_password(), crypto_handshake_process_rekey_complete(), crypto_handshake_process_rekey_request(), crypto_handshake_process_rekey_response(), crypto_handshake_rekey_complete(), crypto_handshake_rekey_request(), crypto_handshake_server_auth_challenge(), crypto_handshake_server_complete(), receive_packet_secure(), and send_packet_secure().

◆ crypto_secure_compare()

bool crypto_secure_compare ( const uint8_t lhs,
const uint8_t rhs,
size_t  len 
)

#include <crypto.h>

Secure constant-time comparison of byte arrays.

Parameters
lhsFirst byte array
rhsSecond byte array
lenLength of arrays to compare
Returns
true if arrays are equal, false otherwise

Uses constant-time comparison (sodium_memcmp) to prevent timing attacks. Always compares all bytes, regardless of where difference is found.

Note
Use this for comparing sensitive data: keys, MACs, HMACs, signatures. Do NOT use regular memcmp() for cryptographic comparisons.
Warning
Returns false if either pointer is NULL.

Definition at line 597 of file lib/crypto/crypto.c.

597 {
598 if (!lhs || !rhs) {
599 return false;
600 }
601 return sodium_memcmp(lhs, rhs, len) == 0;
602}

◆ crypto_set_peer_public_key()

crypto_result_t crypto_set_peer_public_key ( crypto_context_t ctx,
const uint8_t peer_public_key 
)

#include <crypto.h>

Set peer's public key and compute shared secret (step 2 of handshake)

Parameters
ctxCrypto context
peer_public_keyPeer's ephemeral public key (CRYPTO_PUBLIC_KEY_SIZE bytes)
Returns
CRYPTO_OK on success, error code on failure

Receives peer's public key and computes the shared secret using X25519. After this call, crypto_is_ready() will return true.

Note
This function computes the shared secret immediately. The shared secret is used for encryption/decryption after key exchange is complete.

Definition at line 235 of file lib/crypto/crypto.c.

235 {
236 if (!ctx || !ctx->initialized || !peer_public_key) {
238 "crypto_set_peer_public_key: Invalid parameters (ctx=%p, initialized=%d, peer_public_key=%p)", ctx,
239 ctx ? ctx->initialized : 0, peer_public_key);
241 }
242
243 // Store peer's public key
244 // Bounds check to prevent buffer overflow
245 size_t copy_size = (ctx->public_key_size <= X25519_KEY_SIZE) ? ctx->public_key_size : X25519_KEY_SIZE;
246 SAFE_MEMCPY(ctx->peer_public_key, copy_size, peer_public_key, copy_size);
247 ctx->peer_key_received = true;
248
249 // Compute shared secret using X25519
250 if (crypto_box_beforenm(ctx->shared_key, peer_public_key, ctx->private_key) != 0) {
251 SET_ERRNO(ERROR_CRYPTO, "Failed to compute shared secret from peer public key");
253 }
254
255 ctx->key_exchange_complete = true;
256
257 log_debug("Key exchange completed - shared secret computed");
258 return CRYPTO_OK;
259}
uint8_t peer_public_key[32]

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_GENERATION, CRYPTO_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::initialized, crypto_context_t::key_exchange_complete, log_debug, crypto_context_t::peer_key_received, crypto_context_t::peer_public_key, crypto_context_t::private_key, crypto_context_t::public_key_size, SAFE_MEMCPY, SET_ERRNO, crypto_context_t::shared_key, and X25519_KEY_SIZE.

Referenced by crypto_handshake_client_key_exchange(), crypto_handshake_server_auth_challenge(), and crypto_process_public_key_packet().

◆ crypto_should_rekey()

bool crypto_should_rekey ( const crypto_context_t ctx)

#include <crypto.h>

Check if rekeying should be triggered based on time or packet count thresholds.

Parameters
ctxCrypto context
Returns
true if rekey should be initiated, false otherwise

Checks if rekeying should be triggered based on:

  • Time since last rekey >= rekey_time_threshold
  • Packet count since last rekey >= rekey_packet_threshold
  • Minimum interval since last rekey request (DDoS protection)
Note
Should be called after each packet encryption.
Test environment: Automatically uses test thresholds if CRITERION_TEST or TESTING env var is set.
DDoS protection: Requires at least REKEY_MIN_REQUEST_INTERVAL seconds between requests.

Definition at line 1143 of file lib/crypto/crypto.c.

1143 {
1144 if (!ctx || !ctx->initialized) {
1145 return false;
1146 }
1147
1148 // Don't trigger rekey if one is already in progress
1149 if (ctx->rekey_in_progress) {
1150 return false;
1151 }
1152
1153 // Don't trigger rekey if handshake isn't complete yet
1154 if (!ctx->handshake_complete) {
1155 return false;
1156 }
1157
1158 // Check packet count threshold
1159 if (ctx->rekey_packet_count >= ctx->rekey_packet_threshold) {
1160 log_debug("Rekey triggered: packet count (%llu) >= threshold (%llu)", (unsigned long long)ctx->rekey_packet_count,
1161 (unsigned long long)ctx->rekey_packet_threshold);
1162 return true;
1163 }
1164
1165 // Check time threshold
1166 time_t now = time(NULL);
1167 time_t elapsed = now - ctx->rekey_last_time;
1168 if (elapsed >= ctx->rekey_time_threshold) {
1169 log_debug("Rekey triggered: time elapsed (%ld sec) >= threshold (%ld sec)", (long)elapsed,
1170 (long)ctx->rekey_time_threshold);
1171 return true;
1172 }
1173
1174 return false;
1175}

References crypto_context_t::handshake_complete, crypto_context_t::initialized, log_debug, crypto_context_t::rekey_in_progress, crypto_context_t::rekey_last_time, crypto_context_t::rekey_packet_count, crypto_context_t::rekey_packet_threshold, and crypto_context_t::rekey_time_threshold.

Referenced by crypto_handshake_should_rekey().

◆ crypto_sign_ephemeral_key()

asciichat_error_t crypto_sign_ephemeral_key ( const private_key_t private_key,
const uint8_t ephemeral_key,
size_t  ephemeral_key_size,
uint8_t signature_out 
)

#include <crypto.h>

Sign ephemeral key with private key.

Parameters
private_keyEd25519 private key for signing (64 bytes)
ephemeral_keyEphemeral public key to sign (variable size)
ephemeral_key_sizeSize of ephemeral key (typically 32 bytes for X25519)
signature_outOutput buffer for Ed25519 signature (64 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Signs ephemeral public key with Ed25519 identity key. Used during authenticated key exchange to prove server identity.

Note
Signature is over the ephemeral public key itself. Allows verification without revealing the identity key.
Used in authenticated key exchange format (ephemeral + identity + signature).

Definition at line 1091 of file lib/crypto/crypto.c.

1092 {
1093 if (!private_key || !ephemeral_key || !signature_out) {
1094 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: private_key=%p, ephemeral_key=%p, signature_out=%p",
1095 private_key, ephemeral_key, signature_out);
1096 }
1097
1098 if (ephemeral_key_size == 0) {
1099 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ephemeral key size: %zu", ephemeral_key_size);
1100 return ERROR_INVALID_PARAM;
1101 }
1102
1103 // Sign the ephemeral key with our Ed25519 private key
1104 if (private_key->type == KEY_TYPE_ED25519) {
1105 if (crypto_sign_detached(signature_out, NULL, ephemeral_key, ephemeral_key_size, private_key->key.ed25519) != 0) {
1106 return SET_ERRNO(ERROR_CRYPTO, "Failed to sign ephemeral key");
1107 }
1108 } else {
1109 return SET_ERRNO(ERROR_CRYPTO, "Unsupported private key type for signing");
1110 }
1111
1112 return ASCIICHAT_OK;
1113}
key_type_t type
Definition key_types.h:92

References ASCIICHAT_OK, private_key_t::ed25519, ERROR_CRYPTO, ERROR_INVALID_PARAM, private_key_t::key, KEY_TYPE_ED25519, SET_ERRNO, and private_key_t::type.

◆ crypto_validate_password()

crypto_result_t crypto_validate_password ( const char *  password)

#include <crypto.h>

Validate password length requirements.

Parameters
passwordPassword to validate
Returns
CRYPTO_OK if valid, CRYPTO_ERROR_INVALID_PARAMS if too short/long

Validates that password meets length requirements (MIN_PASSWORD_LENGTH to MAX_PASSWORD_LENGTH characters).

Note
Password must be between MIN_PASSWORD_LENGTH (8) and MAX_PASSWORD_LENGTH (256) characters.

Definition at line 274 of file lib/crypto/crypto.c.

274 {
275 if (!password) {
276 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_validate_password: Password is NULL");
278 }
279
280 size_t password_len = strlen(password);
281
282 if (password_len < MIN_PASSWORD_LENGTH) {
283 SET_ERRNO(ERROR_INVALID_PARAM, "Password too short (minimum %d characters, got %zu)", MIN_PASSWORD_LENGTH,
284 password_len);
286 }
287
288 if (password_len > MAX_PASSWORD_LENGTH) {
289 SET_ERRNO(ERROR_INVALID_PARAM, "Password too long (maximum %d characters, got %zu)", MAX_PASSWORD_LENGTH,
290 password_len);
292 }
293
294 return CRYPTO_OK;
295}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_OK, ERROR_INVALID_PARAM, MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH, and SET_ERRNO.

Referenced by crypto_derive_password_encryption_key(), and crypto_derive_password_key().

◆ crypto_verify_auth_response()

bool crypto_verify_auth_response ( const crypto_context_t ctx,
const uint8_t  nonce[32],
const uint8_t  expected_hmac[32] 
)

#include <crypto.h>

Verify authentication response HMAC bound to DH shared_secret.

Parameters
ctxCrypto context with keys (must have completed key exchange)
nonceChallenge nonce that was sent (32 bytes)
expected_hmacExpected HMAC to verify (32 bytes)
Returns
true if HMAC is valid, false otherwise

Verifies: HMAC(auth_key, nonce || shared_secret)

Note
Key selection: Uses password_key if available, otherwise uses shared_key. Must match the key used by crypto_compute_auth_response() on the peer side.
Key exchange requirement: ctx->key_exchange_complete must be true. Returns false if key exchange is not complete.
Warning
Always check return value. False indicates authentication failure or wrong key.

Definition at line 903 of file lib/crypto/crypto.c.

904 {
905 if (!ctx || !nonce || !expected_hmac) {
907 "crypto_verify_auth_response: Invalid parameters (ctx=%p, nonce=%p, expected_hmac=%p)", ctx, nonce,
908 expected_hmac);
909 return false;
910 }
911
912 // Ensure shared secret is derived before verifying HMAC
913 // This is critical for password HMAC verification which binds to the shared secret
914 if (!ctx->key_exchange_complete) {
915 SET_ERRNO(ERROR_CRYPTO, "Cannot verify auth response - key exchange not complete");
916 return false;
917 }
918
919 // Bind password HMAC to DH shared_secret to prevent MITM
920 // Combined data: nonce || shared_secret
921 uint8_t combined_data[64];
922 memcpy(combined_data, nonce, 32);
923 memcpy(combined_data + 32, ctx->shared_key, 32);
924
925 // Use password_key if available, otherwise use shared_key
926 const uint8_t *auth_key = ctx->has_password ? ctx->password_key : ctx->shared_key;
927
928 log_debug("Verifying auth response: has_password=%d, key_exchange_complete=%d, using_password_key=%d",
929 ctx->has_password, ctx->key_exchange_complete, (auth_key == ctx->password_key));
930
931 bool result = crypto_verify_hmac_ex(auth_key, combined_data, 64, expected_hmac);
932
933 // Securely zero sensitive data containing shared secret
934 sodium_memzero(combined_data, sizeof(combined_data));
935
936 return result;
937}

References crypto_verify_hmac_ex(), ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_password, crypto_context_t::key_exchange_complete, log_debug, crypto_context_t::password_key, SET_ERRNO, and crypto_context_t::shared_key.

Referenced by crypto_handshake_client_complete(), and crypto_handshake_server_complete().

◆ crypto_verify_hmac()

bool crypto_verify_hmac ( const uint8_t  key[32],
const uint8_t  data[32],
const uint8_t  expected_hmac[32] 
)

#include <crypto.h>

Verify HMAC-SHA256 for fixed 32-byte data.

Parameters
keyHMAC key (32 bytes)
dataData that was authenticated (32 bytes)
expected_hmacExpected HMAC to verify (32 bytes)
Returns
true if HMAC is valid, false otherwise

Verifies HMAC-SHA256 using constant-time comparison. Prevents timing attacks during verification.

Note
Returns false if any parameter is NULL.

Definition at line 835 of file lib/crypto/crypto.c.

835 {
836 if (!key || !data || !expected_hmac) {
837 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_verify_hmac: Invalid parameters (key=%p, data=%p, expected_hmac=%p)", key,
838 data, expected_hmac);
839 return false;
840 }
841
842 uint8_t computed_hmac[32];
843 if (crypto_auth_hmacsha256(computed_hmac, data, 32, key) != 0) {
844 return false;
845 }
846
847 return sodium_memcmp(computed_hmac, expected_hmac, 32) == 0;
848}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by crypto_process_auth_response().

◆ crypto_verify_hmac_ex()

bool crypto_verify_hmac_ex ( const uint8_t  key[32],
const uint8_t data,
size_t  data_len,
const uint8_t  expected_hmac[32] 
)

#include <crypto.h>

Verify HMAC-SHA256 for variable-length data.

Parameters
keyHMAC key (32 bytes)
dataData that was authenticated (variable length)
data_lenLength of data (must be > 0)
expected_hmacExpected HMAC to verify (32 bytes)
Returns
true if HMAC is valid, false otherwise

Verifies HMAC-SHA256 using constant-time comparison. Prevents timing attacks during verification.

Note
Returns false if any parameter is NULL or data_len is 0.

Definition at line 850 of file lib/crypto/crypto.c.

851 {
852 if (!key || !data || !expected_hmac || data_len == 0) {
854 "crypto_verify_hmac_ex: Invalid parameters (key=%p, data=%p, data_len=%zu, expected_hmac=%p)", key, data,
855 data_len, expected_hmac);
856 return false;
857 }
858
859 uint8_t computed_hmac[32];
860 if (crypto_auth_hmacsha256(computed_hmac, data, data_len, key) != 0) {
861 return false;
862 }
863
864 return sodium_memcmp(computed_hmac, expected_hmac, 32) == 0;
865}

References ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by crypto_verify_auth_response().

◆ crypto_verify_password()

bool crypto_verify_password ( const crypto_context_t ctx,
const char *  password 
)

#include <crypto.h>

Verify password matches stored salt/key.

Parameters
ctxCrypto context (must be initialized and have password)
passwordPassword to verify
Returns
true if password matches, false otherwise

Verifies that the provided password, when derived with the stored salt, produces the same key as stored in the context.

Note
Uses constant-time comparison to prevent timing attacks.
Salt: Uses same deterministic salt as crypto_derive_password_key().

Definition at line 336 of file lib/crypto/crypto.c.

336 {
337 if (!ctx || !ctx->initialized || !ctx->has_password || !password) {
338 return false;
339 }
340
341 uint8_t test_key[SECRETBOX_KEY_SIZE]; // Use maximum size for buffer
342
343 // Use the same deterministic salt for verification
344 // Salt must be exactly ARGON2ID_SALT_SIZE (32) bytes
345 const char *deterministic_salt = "ascii-chat-password-salt-v1";
346 uint8_t salt[ARGON2ID_SALT_SIZE]; // Use maximum size for buffer
347 size_t salt_str_len = strlen(deterministic_salt);
348
349 // Zero-initialize the salt buffer first
350 memset(salt, 0, ARGON2ID_SALT_SIZE);
351
352 // Copy the salt string (will be padded with zeros to ctx->salt_size)
353 memcpy(salt, deterministic_salt, (salt_str_len < ctx->salt_size) ? salt_str_len : ctx->salt_size);
354
355 // Derive key with same salt
356 if (crypto_pwhash(test_key, ctx->encryption_key_size, password, strlen(password), salt,
357 crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE,
358 crypto_pwhash_ALG_DEFAULT) != 0) {
359 secure_memzero(test_key, sizeof(test_key));
360 return false;
361 }
362
363 // Constant-time comparison
364 bool match = (sodium_memcmp(test_key, ctx->password_key, ctx->encryption_key_size) == 0);
365
366 secure_memzero(test_key, sizeof(test_key));
367 return match;
368}

References ARGON2ID_SALT_SIZE, crypto_context_t::encryption_key_size, crypto_context_t::has_password, crypto_context_t::initialized, crypto_context_t::password_key, crypto_context_t::salt_size, and SECRETBOX_KEY_SIZE.

◆ crypto_verify_peer_signature()

asciichat_error_t crypto_verify_peer_signature ( const uint8_t peer_public_key,
const uint8_t ephemeral_key,
size_t  ephemeral_key_size,
const uint8_t signature 
)

#include <crypto.h>

Verify peer's signature on ephemeral key.

Parameters
peer_public_keyPeer's Ed25519 public key (32 bytes)
ephemeral_keyEphemeral public key that was signed (variable size)
ephemeral_key_sizeSize of ephemeral key (typically 32 bytes for X25519)
signatureEd25519 signature (64 bytes)
Returns
ASCIICHAT_OK if signature is valid, error code on failure

Verifies Ed25519 signature on ephemeral public key using peer's identity key. Used during authenticated key exchange to verify server identity.

Note
Signature is over the ephemeral public key itself, proving ownership of the identity key without revealing it.
Used in authenticated key exchange format (ephemeral + identity + signature).

Definition at line 1072 of file lib/crypto/crypto.c.

1073 {
1074 if (!peer_public_key || !ephemeral_key || !signature) {
1075 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: peer_public_key=%p, ephemeral_key=%p, signature=%p",
1076 peer_public_key, ephemeral_key, signature);
1077 }
1078
1079 if (ephemeral_key_size == 0) {
1080 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ephemeral key size: %zu", ephemeral_key_size);
1081 }
1082
1083 // Verify the signature using Ed25519
1084 if (crypto_sign_verify_detached(signature, ephemeral_key, ephemeral_key_size, peer_public_key) != 0) {
1085 return SET_ERRNO(ERROR_CRYPTO, "Peer signature verification failed");
1086 }
1087
1088 return ASCIICHAT_OK;
1089}

References ASCIICHAT_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ display_mitm_warning()

bool display_mitm_warning ( const char *  server_ip,
uint16_t  port,
const uint8_t  expected_key[32],
const uint8_t  received_key[32] 
)

#include <known_hosts.h>

Display MITM warning with key comparison and prompt user for confirmation.

Parameters
server_ipServer IP address (IPv4 or IPv6, must not be NULL)
portServer port number
expected_keyExpected server key from known_hosts (32 bytes, must not be NULL)
received_keyReceived server key from connection (32 bytes, must not be NULL)
Returns
true if user accepts the risk and wants to continue, false otherwise

Displays man-in-the-middle warning with key comparison and prompts user for confirmation. Shows both expected and received keys in hex format for comparison.

Note
Warning display: Shows formatted warning message with:
  • Server IP:port
  • Expected key fingerprint (SHA256)
  • Received key fingerprint (SHA256)
  • Prompt for user confirmation
Key fingerprints: Displays SHA256 fingerprints of both keys for easy comparison. Fingerprints are displayed in hex format (64 hex chars).
User prompt: Prompts user to accept risk (continue) or abort connection. Returns true if user accepts, false if user aborts.
Non-interactive mode: If not connected to TTY (snapshot mode), automatically accepts the connection (returns true). This allows automated connections.
Security: Key mismatch indicates potential MITM attack or key rotation. User should verify keys before accepting.
Warning
Always check return value. If false, connection should be aborted.
MITM risk: Key mismatch may indicate man-in-the-middle attack. User should verify keys before accepting connection.

Definition at line 621 of file known_hosts.c.

622 {
623 char expected_fp[CRYPTO_HEX_KEY_SIZE_NULL], received_fp[CRYPTO_HEX_KEY_SIZE_NULL];
624 compute_key_fingerprint(expected_key, expected_fp);
625 compute_key_fingerprint(received_key, received_fp);
626
627 const char *known_hosts_path = get_known_hosts_path();
628
629 // Format IP:port with proper bracket notation for IPv6
630 char ip_with_port[BUFFER_SIZE_MEDIUM];
631 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
632 // Fallback to basic format if error
633 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
634 }
635
636 char escaped_ip_with_port[128];
637 escape_ascii(ip_with_port, "[]", escaped_ip_with_port, 128);
638 log_warn("\n"
639 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
640 "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n"
641 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
642 "\n"
643 "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"
644 "Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n"
645 "It is also possible that the host key has just been changed.\n"
646 "\n"
647 "The fingerprint for the Ed25519 key sent by the remote host is:\n"
648 "SHA256:%s\n"
649 "\n"
650 "Expected fingerprint:\n"
651 "SHA256:%s\n"
652 "\n"
653 "Please contact your system administrator.\n"
654 "\n"
655 "Add correct host key in %s to get rid of this message.\n"
656 "Offending key for IP address %s was found at:\n"
657 "%s\n"
658 "\n"
659 "To update the key, run:\n"
660 " # Linux/macOS:\n"
661 " sed -i '' '/%s /d' ~/.ascii-chat/known_hosts\n"
662 " # or run this instead:\n"
663 " cat ~/.ascii-chat/known_hosts | grep -v '%s ' > /tmp/x; cp /tmp/x ~/.ascii-chat/known_hosts\n"
664 " # Windows PowerShell:\n"
665 " (Get-Content ~/.ascii-chat/known_hosts) | Where-Object { $_ -notmatch '^%s ' } | Set-Content "
666 "~/.ascii-chat/known_hosts\n"
667 " # Or manually edit ~/.ascii-chat/known_hosts to remove lines starting with '%s '\n"
668 "\n"
669 "Host key verification failed.\n"
670 "\n",
671 received_fp, expected_fp, known_hosts_path, ip_with_port, known_hosts_path, ip_with_port,
672 escaped_ip_with_port, ip_with_port, ip_with_port);
673
674 return false;
675}
void escape_ascii(const char *str, const char *escape_char, char *out_buffer, size_t out_buffer_size)
Escape ASCII characters in a string.
Definition string.c:12
void compute_key_fingerprint(const uint8_t key[ED25519_PUBLIC_KEY_SIZE], char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL])

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, compute_key_fingerprint(), CRYPTO_HEX_KEY_SIZE_NULL, escape_ascii(), format_ip_with_port(), get_known_hosts_path(), log_warn, and safe_snprintf().

Referenced by crypto_handshake_client_key_exchange().

◆ free_ta_contents()

void free_ta_contents ( br_x509_trust_anchor *  ta)

#include <pem_utils.h>

Free the contents of a trust anchor.

Parameters
taPointer to trust anchor to free (must not be NULL)

Releases all dynamically allocated memory within a trust anchor structure, but does not free the structure itself. Call this for each trust anchor before freeing the anchor_list buffer.

Note
Memory cleanup: Frees certificate data, public key, and other dynamically allocated fields within trust anchor structure.
Structure preservation: Does not free the trust anchor structure itself. Only frees dynamically allocated fields within the structure.
Usage: Must be called for each trust anchor in anchor_list.buf before freeing anchor_list.buf. Loop through anchors and call for each one.
Warning
Memory leak: Must call this for each trust anchor before freeing anchor_list.buf. Failing to do so will leak memory.

Definition at line 419 of file pem_utils.c.

419 {
420 if (!ta) {
421 return;
422 }
423 xfree(ta->dn.data);
424 switch (ta->pkey.key_type) {
425 case BR_KEYTYPE_RSA:
426 xfree((void *)ta->pkey.key.rsa.n);
427 xfree((void *)ta->pkey.key.rsa.e);
428 break;
429 case BR_KEYTYPE_EC:
430 xfree((void *)ta->pkey.key.ec.q);
431 break;
432 default:
433 SET_ERRNO(ERROR_CRYPTO, "Unknown public key type in CA");
434 break;
435 }
436}

References ERROR_CRYPTO, and SET_ERRNO.

Referenced by https_get(), and read_trust_anchors_from_memory().

◆ get_known_hosts_path()

const char * get_known_hosts_path ( void  )

#include <known_hosts.c>

Get the path to the known_hosts file.

Get known_hosts file path.

Returns the path to the known_hosts file, using the same directory as all other ascii-chat configuration files (get_config_dir()).

PATH RESOLUTION:

  • Unix: $XDG_CONFIG_HOME/ascii-chat/known_hosts or ~/.config/ascii-chat/known_hosts
  • Windows: APPDATA%\ascii-chat\known_hosts
Returns
Pointer to cached known_hosts path (do not free), or NULL on failure
Note
The returned pointer is valid for the lifetime of the program
Returns
Path to known_hosts file (never NULL, cached)

Returns path to known_hosts file, expanding user directory if needed. Path is cached after first call.

Note
File location: Returns ~/.ascii-chat/known_hosts (or equivalent on Windows). Uses expand_path() to resolve user directory.
Path caching: Path is cached after first call to avoid repeated expansion. Cache is freed by known_hosts_cleanup().
Platform-specific paths (same directory as config.toml):
  • Unix: $XDG_CONFIG_HOME/ascii-chat/known_hosts if set, otherwise ~/.ascii-chat/known_hosts
  • Windows: APPDATA%\ascii-chat\known_hosts if set, otherwise ~\.ascii-chat\known_hosts
Warning
Path may not exist: Function returns path even if file doesn't exist. File will be created when first entry is added.

Definition at line 46 of file known_hosts.c.

46 {
47 // Return cached path if already determined
48 if (!g_known_hosts_path_cache) {
49 char *config_dir = get_config_dir();
50 if (!config_dir) {
51 log_error("Failed to determine configuration directory for known_hosts");
52 return NULL;
53 }
54
55 // Build path: config_dir + "known_hosts"
56 size_t config_len = strlen(config_dir);
57 size_t total_len = config_len + strlen("known_hosts") + 1;
58 char *path = SAFE_MALLOC(total_len, char *);
59 if (!path) {
60 SAFE_FREE(config_dir);
61 log_error("Failed to allocate memory for known_hosts path");
62 return NULL;
63 }
64
65 safe_snprintf(path, total_len, "%sknown_hosts", config_dir);
66 SAFE_FREE(config_dir);
67
68 g_known_hosts_path_cache = path;
69 log_debug("KNOWN_HOSTS: Using path %s", g_known_hosts_path_cache);
70 }
71 return g_known_hosts_path_cache;
72}
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:223

References get_config_dir(), log_debug, log_error, SAFE_FREE, SAFE_MALLOC, and safe_snprintf().

Referenced by add_known_host(), check_known_host(), check_known_host_no_identity(), crypto_handshake_client_key_exchange(), display_mitm_warning(), prompt_unknown_host(), and remove_known_host().

◆ gpg_agent_connect()

int gpg_agent_connect ( void  )

#include <agent.h>

Connect to gpg-agent.

Returns
Socket/pipe handle on success, -1 on error

Establishes connection to GPG agent using Assuan protocol. Connects to agent socket/pipe and performs initial handshake.

Note
Connection method:
  • Unix: Connects to Unix domain socket (~/.gnupg/S.gpg-agent or GPG_AGENT_INFO)
  • Windows: Connects to named pipe (\.\pipe\gpg-agent or from GPG_AGENT_INFO)
Socket location:
  • Unix: $GPG_AGENT_INFO or ~/.gnupg/S.gpg-agent
  • Windows: Named pipe from GPG_AGENT_INFO or default pipe
Assuan protocol: After connection, sends initial commands:
  • Receives "OK Pleased to meet you" greeting
  • No additional initialization needed for signing operations
Connection ownership: Caller must call gpg_agent_disconnect() when done. Failing to disconnect will leak socket handles.
Warning
Agent requirement: GPG agent must be running and accessible. Returns -1 if agent is not available (not running, wrong path, etc.).
Socket cleanup: Must call gpg_agent_disconnect() to close socket. Failing to do so will leak file descriptors/handles.

Definition at line 267 of file agent.c.

267 {
268 char socket_path[PLATFORM_MAX_PATH_LENGTH];
269 if (get_agent_socket_path(socket_path, sizeof(socket_path)) != 0) {
270 log_error("Failed to get GPG agent socket path");
271 return -1;
272 }
273
274 log_debug("Connecting to GPG agent at: %s", socket_path);
275
276 int sock = socket(AF_UNIX, SOCK_STREAM, 0);
277 if (sock < 0) {
278 log_error("Failed to create socket: %s", SAFE_STRERROR(errno));
279 return -1;
280 }
281
282 struct sockaddr_un addr;
283 memset(&addr, 0, sizeof(addr));
284 addr.sun_family = AF_UNIX;
285 SAFE_STRNCPY(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
286
287 if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
288 log_error("Failed to connect to GPG agent: %s", SAFE_STRERROR(errno));
289 close(sock);
290 return -1;
291 }
292
293 // Read initial greeting
294 char response[GPG_AGENT_MAX_RESPONSE];
295 if (read_agent_line(sock, response, sizeof(response)) != 0) {
296 log_error("Failed to read GPG agent greeting");
297 close(sock);
298 return -1;
299 }
300
301 if (!is_ok_response(response)) {
302 log_error("Unexpected GPG agent greeting: %s", response);
303 close(sock);
304 return -1;
305 }
306
307 log_debug("Connected to GPG agent successfully");
308
309 // Set loopback pinentry mode to avoid interactive prompts
310 // This allows GPG agent to work in non-interactive environments
311 if (send_agent_command(sock, "OPTION pinentry-mode=loopback") != 0) {
312 log_warn("Failed to set loopback pinentry mode (continuing anyway)");
313 } else {
314 // Read response for OPTION command
315 if (read_agent_line(sock, response, sizeof(response)) != 0) {
316 log_warn("Failed to read OPTION command response (continuing anyway)");
317 } else if (is_ok_response(response)) {
318 log_debug("Loopback pinentry mode enabled");
319 } else {
320 log_warn("Failed to enable loopback pinentry mode: %s (continuing anyway)", response);
321 }
322 }
323
324 return sock;
325}
#define GPG_AGENT_MAX_RESPONSE
Definition agent.c:34
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_STRERROR(errnum)
Definition common.h:385
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:91
int errno

References errno, GPG_AGENT_MAX_RESPONSE, log_debug, log_error, log_warn, PLATFORM_MAX_PATH_LENGTH, SAFE_STRERROR, and SAFE_STRNCPY.

Referenced by ed25519_sign_message(), gpg_agent_is_available(), and gpg_get_public_key().

◆ gpg_agent_disconnect()

void gpg_agent_disconnect ( int  sock)

#include <agent.h>

Disconnect from gpg-agent.

Parameters
sockSocket/pipe handle from gpg_agent_connect()

Closes connection to GPG agent and releases socket resources. Safe to call with invalid socket (does nothing if sock < 0).

Note
Socket closure: Closes socket handle using platform-specific close function. On Unix, uses close(). On Windows, uses CloseHandle().
Assuan protocol: No goodbye message needed - just closes socket. GPG agent handles disconnections gracefully.
Safe disconnect: Function validates socket handle before closing. Safe to call with -1 or invalid handles.
Warning
Always call this after gpg_agent_connect() to avoid resource leaks.

Definition at line 337 of file agent.c.

337 {
338 if (sock >= 0) {
339 send_agent_command(sock, "BYE");
340 close(sock);
341 }
342}

Referenced by ed25519_sign_message(), gpg_agent_is_available(), and gpg_get_public_key().

◆ gpg_agent_is_available()

bool gpg_agent_is_available ( void  )

#include <agent.h>

Check if GPG agent is available.

Returns
true if gpg-agent is running and accessible, false otherwise

Checks if GPG agent is running by attempting to connect and immediately disconnecting. Uses gpg_agent_connect() internally.

Note
Agent detection: Attempts actual connection to verify agent is running. Returns false if connection fails for any reason.
Platform differences:
  • Unix: Checks for socket existence and accessibility (~/.gnupg/S.gpg-agent)
  • Windows: Attempts to connect to named pipe
Connection test: Creates temporary connection and closes it immediately. Does not leave connection open.
Performance: Involves actual socket connection - may be slow if agent is not running. Consider caching result if calling frequently.
Warning
Agent may not be running: Function returns false if agent is not running, socket/pipe doesn't exist, or permissions prevent access.

Definition at line 539 of file agent.c.

539 {
540 int sock = gpg_agent_connect();
541 if (sock < 0) {
542 return false;
543 }
545 return true;
546}
int gpg_agent_connect(void)
Connect to gpg-agent.
Definition agent.c:267
void gpg_agent_disconnect(int sock)
Disconnect from gpg-agent.
Definition agent.c:337

References gpg_agent_connect(), and gpg_agent_disconnect().

◆ gpg_agent_sign()

int gpg_agent_sign ( int  sock,
const char *  keygrip,
const uint8_t message,
size_t  message_len,
uint8_t signature_out,
size_t *  signature_len_out 
)

#include <agent.h>

Sign a message using GPG agent.

Parameters
sockSocket/pipe handle from gpg_agent_connect() (must be valid)
keygripGPG keygrip (40-char hex string, must not be NULL)
messageMessage to sign (must not be NULL)
message_lenMessage length (must be > 0)
signature_outOutput buffer for signature (must be >= 64 bytes for Ed25519)
signature_len_outOutput parameter for signature length (must not be NULL)
Returns
0 on success, -1 on error

Signs message using GPG agent Assuan protocol PKSIGN command. Key stays in GPG agent - private key never enters application memory.

Note
Assuan protocol commands:
  1. RESET - Clear any previous state
  2. SIGKEY <keygrip> - Select key to use for signing
  3. SETHASH –hash=sha256 <hex_hash> - Set message hash (SHA-256 of message)
  4. PKSIGN - Perform signature operation
Key selection: Uses keygrip to identify key in agent. Keygrip is 40-char hex string computed from public key material.
Hash algorithm: Uses SHA-256 hash of message for signing. GPG agent expects hex-encoded hash, not raw message.
Signature format: Returns raw Ed25519 signature (64 bytes). Format: R || S (32 bytes each) for Ed25519.
Error handling: Returns -1 on any error:
  • Agent connection lost
  • Key not found in agent (wrong keygrip)
  • Signature operation failed
  • Protocol error (malformed response)
Connection requirement: Requires active connection from gpg_agent_connect(). Does not create or close connection - caller manages connection lifecycle.
Warning
Agent connection: Requires valid socket from gpg_agent_connect(). Returns -1 if socket is invalid or connection is closed.
Key availability: Key must be in GPG agent and identified by keygrip. Returns -1 if key not found or keygrip is invalid.
Buffer size: signature_out must be at least 64 bytes for Ed25519. Function writes up to 64 bytes and sets signature_len_out accordingly.

Definition at line 345 of file agent.c.

346 {
347 if (handle_as_int < 0 || !keygrip || !message || !signature_out || !signature_len_out) {
348 log_error("Invalid arguments to gpg_agent_sign");
349 return -1;
350 }
351
352#ifdef _WIN32
353 HANDLE handle = (HANDLE)(intptr_t)handle_as_int;
354#else
355 int handle = handle_as_int;
356#endif
357
358 char response[GPG_AGENT_MAX_RESPONSE];
359
360 // 1. Set the key to use (SIGKEY command)
361 char sigkey_cmd[128];
362 safe_snprintf(sigkey_cmd, sizeof(sigkey_cmd), "SIGKEY %s", keygrip);
363 if (send_agent_command(handle, sigkey_cmd) != 0) {
364 log_error("Failed to send SIGKEY command");
365 return -1;
366 }
367
368 if (read_agent_line(handle, response, sizeof(response)) != 0) {
369 log_error("Failed to read SIGKEY response");
370 return -1;
371 }
372
373 if (!is_ok_response(response)) {
374 log_error("SIGKEY failed: %s", response);
375 return -1;
376 }
377
378 // 2. For EdDSA/Ed25519, GPG agent requires SETHASH with a hash algorithm
379 // GPG agent doesn't support --inquire for SETHASH - the command syntax is:
380 // SETHASH (--hash=<name>)|(<algonumber>) <hexstring>
381 // For Ed25519, we hash the message with SHA512 (algo 10) first
382
383 // Hash the message with SHA512 using libsodium
384 uint8_t hash[crypto_hash_sha512_BYTES];
385 crypto_hash_sha512(hash, message, message_len);
386
387 // Build SETHASH command with SHA512 hash (algo 10)
388 // Format: "SETHASH 10 <128 hex chars for 64-byte SHA512 hash>"
389 char sethash_cmd[256];
390 int offset = safe_snprintf(sethash_cmd, sizeof(sethash_cmd), "SETHASH 10 ");
391 for (size_t i = 0; i < crypto_hash_sha512_BYTES; i++) {
392 offset += safe_snprintf(sethash_cmd + offset, sizeof(sethash_cmd) - (size_t)offset, "%02X", hash[i]);
393 }
394
395 log_debug("Sending SETHASH command with SHA512 hash");
396 if (send_agent_command(handle, sethash_cmd) != 0) {
397 log_error("Failed to send SETHASH command");
398 return -1;
399 }
400
401 // Read SETHASH response
402 if (read_agent_line(handle, response, sizeof(response)) != 0) {
403 log_error("Failed to read SETHASH response");
404 return -1;
405 }
406
407 if (!is_ok_response(response)) {
408 log_error("SETHASH failed: %s", response);
409 return -1;
410 }
411
412 // 3. Request signature using PKSIGN
413 if (send_agent_command(handle, "PKSIGN") != 0) {
414 log_error("Failed to send PKSIGN command");
415 return -1;
416 }
417
418 // Read response - skip status/error lines and wait for data line (D ...)
419 // GPG agent sends informational ERR lines that are not fatal (e.g., "Not implemented")
420 // Keep reading until we get the actual signature data
421 bool found_data = false;
422 for (int attempts = 0; attempts < 20; attempts++) {
423 if (read_agent_line(handle, response, sizeof(response)) != 0) {
424 log_error("Failed to read PKSIGN response");
425 return -1;
426 }
427
428 log_debug("PKSIGN response line %d: %s", attempts + 1, response);
429
430 // Skip status lines (S INQUIRE_MAXLEN, etc)
431 if (response[0] == 'S' && response[1] == ' ') {
432 log_debug("Skipping PKSIGN status line: %s", response);
433 continue;
434 }
435
436 // Skip informational ERR lines (GPG agent sends these even on success)
437 // Common ERR codes: 67109141 (IPC cancelled), 67108933 (Not implemented)
438 if (strncmp(response, "ERR", 3) == 0) {
439 log_debug("Skipping PKSIGN error line (informational): %s", response);
440 continue;
441 }
442
443 // Check if it's a data line (D followed by space)
444 if (response[0] == 'D' && response[1] == ' ') {
445 log_debug("Found signature data line");
446 found_data = true;
447 break;
448 }
449
450 // Check for OK (success without data would be unexpected)
451 if (strncmp(response, "OK", 2) == 0) {
452 log_warn("PKSIGN returned OK without data line");
453 continue; // Keep trying in case D line follows
454 }
455
456 // Check if GPG agent is sending another INQUIRE (shouldn't happen)
457 if (strncmp(response, "INQUIRE", 7) == 0) {
458 log_error("Unexpected INQUIRE after PKSIGN: %s", response);
459 return -1;
460 }
461
462 // Unknown response type
463 log_warn("Unexpected PKSIGN response (attempt %d): %s", attempts + 1, response);
464 }
465
466 if (!found_data) {
467 log_error("Expected D line from PKSIGN after %d attempts", 20);
468 return -1;
469 }
470
471 // Parse S-expression signature from GPG agent
472 // GPG agent returns: D <percent-encoded-sexp>
473 // Example: D (7:sig-val(5:eddsa(1:r32:%<hex>)(1:s32:%<hex>)))
474 // The signature is 64 bytes total: R (32) + S (32)
475
476 // DEBUG: Print first 200 chars of response to see format
477 char debug_buf[201];
478 size_t response_len = strlen(response);
479 size_t debug_len = response_len < 200 ? response_len : 200;
480 memcpy(debug_buf, response, debug_len);
481 debug_buf[debug_len] = '\0';
482 log_debug("GPG agent D line (first 200 bytes): %s", debug_buf);
483
484 const char *data = response + 2; // Skip "D "
485
486 // The response format from GPG agent for EdDSA is percent-encoded
487 // We need to decode it to get the raw binary signature
488 // For now, let's try the simple approach: find the raw data
489
490 // Look for the pattern that indicates where R starts: "(1:r32:"
491 const char *r_marker = strstr(data, "(1:r32:");
492 if (!r_marker) {
493 log_error("Could not find r value marker in S-expression");
494 return -1;
495 }
496
497 // Skip the marker to get to the actual R data
498 const char *r_data = r_marker + 7; // strlen("(1:r32:")
499
500 // Look for the pattern that indicates where S starts: "(1:s32:"
501 const char *s_marker = strstr(r_data + 32, "(1:s32:");
502 if (!s_marker) {
503 log_error("Could not find s value marker in S-expression");
504 return -1;
505 }
506
507 // Skip the marker to get to the actual S data
508 const char *s_data = s_marker + 7; // strlen("(1:s32:")
509
510 // Copy the raw binary data
511 memcpy(signature_out, r_data, 32);
512 memcpy(signature_out + 32, s_data, 32);
513
514 *signature_len_out = 64;
515
516 // DEBUG: Print signature in hex
517 char sig_hex[129];
518 for (int i = 0; i < 64; i++) {
519 safe_snprintf(sig_hex + i * 2, 3, "%02x", (unsigned char)signature_out[i]);
520 }
521 sig_hex[128] = '\0';
522 log_debug("Extracted signature (64 bytes): %s", sig_hex);
523
524 // Read final OK
525 if (read_agent_line(handle, response, sizeof(response)) != 0) {
526 log_error("Failed to read final PKSIGN response");
527 return -1;
528 }
529
530 if (!is_ok_response(response)) {
531 log_error("PKSIGN final response not OK: %s", response);
532 return -1;
533 }
534
535 log_debug("Successfully signed message with GPG agent");
536 return 0;
537}

References GPG_AGENT_MAX_RESPONSE, log_debug, log_error, log_warn, and safe_snprintf().

Referenced by ed25519_sign_message().

◆ gpg_get_public_key()

int gpg_get_public_key ( const char *  key_id,
uint8_t public_key_out,
char *  keygrip_out 
)

#include <export.h>

Get public key from GPG keyring by key ID.

Parameters
key_idGPG key ID (8/16/40-char hex string, must not be NULL)
public_key_outOutput buffer for 32-byte Ed25519 public key (must not be NULL)
keygrip_outOutput buffer for 40-char keygrip + null terminator (can be NULL if not needed)
Returns
0 on success, -1 on error

Retrieves Ed25519 public key from GPG keyring using gpg --export command. Parses OpenPGP packet format to extract raw 32-byte Ed25519 public key.

Note
Key ID format: Accepts short (8-char), long (16-char), or full (40-char) hex key IDs. Prefix "0x" is optional and will be added automatically if missing. Examples: "7FE90A79F2E80ED3", "0x7FE90A79F2E80ED3", "EDDAE1DA7360D7F4"
Export method: Uses gpg --export 0x<KEY_ID> to export public key. Output is in binary OpenPGP packet format, which is then parsed.
OpenPGP parsing: Parses OpenPGP packet structure to locate Ed25519 public key packet. Extracts 32-byte Ed25519 public key material from packet (algorithm ID 22). Skips 0x40 prefix byte if present (standard OpenPGP MPI format).
Keygrip extraction: If keygrip_out is not NULL, extracts keygrip using gpg --with-keygrip --list-keys. Keygrip is 40-char hex string that identifies key in GPG agent. Useful for subsequent gpg_agent_sign() operations.
Key validation: Validates that key is Ed25519 (algorithm 22 in OpenPGP). Returns error if key is RSA, ECDSA, or other unsupported algorithm.
Buffer requirements:
  • public_key_out: Must be at least 32 bytes
  • keygrip_out: Must be at least 41 bytes (40 hex chars + null terminator) if provided
Error conditions: Returns -1 if:
  • Key ID not found in keyring
  • Key is not Ed25519 (RSA/ECDSA not supported)
  • gpg binary not found in PATH
  • OpenPGP packet parsing fails
  • Key export produces empty output
Warning
GPG binary required: Requires gpg binary in PATH. Returns -1 if GPG is not installed or not accessible.
Ed25519 only: Only Ed25519 keys are supported (OpenPGP algorithm 22). RSA/ECDSA keys will return error.
Key ID must exist: Key must exist in local GPG keyring. Returns -1 if key is not found (check with gpg --list-keys <KEY_ID>).

Definition at line 251 of file export.c.

251 {
252 if (!key_id || !public_key_out) {
253 log_error("Invalid arguments to gpg_get_public_key");
254 return -1;
255 }
256
257 // SECURITY: Validate key_id to prevent command injection
258 // GPG key IDs should be hexadecimal (0-9, a-f, A-F)
259 if (!validate_shell_safe(key_id, NULL)) {
260 log_error("Invalid GPG key ID format - contains unsafe characters: %s", key_id);
261 return -1;
262 }
263
264 // Additional validation: ensure key_id is hex alphanumeric
265 for (size_t i = 0; key_id[i] != '\0'; i++) {
266 if (!isxdigit((unsigned char)key_id[i])) {
267 log_error("Invalid GPG key ID format - must be hexadecimal: %s", key_id);
268 return -1;
269 }
270 }
271
272 // Escape key_id for safe use in shell command (single quotes)
273 char escaped_key_id[BUFFER_SIZE_MEDIUM];
274 if (!escape_shell_single_quotes(key_id, escaped_key_id, sizeof(escaped_key_id))) {
275 log_error("Failed to escape GPG key ID for shell command");
276 return -1;
277 }
278
279 // Use gpg to list the key and get the keygrip
280 char cmd[BUFFER_SIZE_LARGE];
281#ifdef _WIN32
282 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-keygrip --with-colons 0x%s 2>nul", escaped_key_id);
283#else
284 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-keygrip --with-colons 0x%s 2>/dev/null", escaped_key_id);
285#endif
286 FILE *fp = SAFE_POPEN(cmd, "r");
287 if (!fp) {
288 log_error("Failed to run gpg command - GPG may not be installed");
289#ifdef _WIN32
290 log_error("To install GPG on Windows, download Gpg4win from:");
291 log_error(" https://www.gpg4win.org/download.html");
292#elif defined(__APPLE__)
293 log_error("To install GPG on macOS, use Homebrew:");
294 log_error(" brew install gnupg");
295#else
296 log_error("To install GPG on Linux:");
297 log_error(" Debian/Ubuntu: sudo apt-get install gnupg");
298 log_error(" Fedora/RHEL: sudo dnf install gnupg2");
299 log_error(" Arch Linux: sudo pacman -S gnupg");
300 log_error(" Alpine Linux: sudo apk add gnupg");
301#endif
302 return -1;
303 }
304
305 char line[BUFFER_SIZE_XLARGE];
306 char found_keygrip[128] = {0};
307 bool found_key = false;
308
309 // Parse gpg output
310 // Format: pub:..., grp:::::::::<keygrip>:
311 while (fgets(line, sizeof(line), fp)) {
312 if (strncmp(line, "pub:", 4) == 0) {
313 // Found the public key line
314 found_key = true;
315 } else if (found_key && strncmp(line, "grp:", 4) == 0) {
316 // Extract keygrip
317 // Format: grp:::::::::D52FF935FBA59609EE65E1685287828242A1EA1A:
318 // (8 empty fields, then keygrip, then final colon)
319 const char *grp_start = line + 4;
320 int colon_count = 0;
321 while (*grp_start && colon_count < 8) {
322 if (*grp_start == ':') {
323 colon_count++;
324 }
325 grp_start++;
326 }
327
328 if (colon_count == 8) {
329 const char *grp_end = strchr(grp_start, ':');
330 if (grp_end) {
331 size_t grp_len = grp_end - grp_start;
332 if (grp_len < sizeof(found_keygrip)) {
333 memcpy(found_keygrip, grp_start, grp_len);
334 found_keygrip[grp_len] = '\0';
335
336 if (keygrip_out) {
337 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
338 }
339 }
340 }
341 }
342 break;
343 }
344 }
345
346 SAFE_PCLOSE(fp);
347
348 if (!found_key || strlen(found_keygrip) == 0) {
349 log_error("Could not find GPG key with ID: %s", key_id);
350 return -1;
351 }
352
353 log_debug("Found keygrip for key %s: %s", key_id, found_keygrip);
354
355 // Try to use GPG agent API to read the public key directly via READKEY command
356 int agent_sock = gpg_agent_connect();
357 if (agent_sock < 0) {
358 log_info("GPG agent not available, falling back to gpg --export for public key extraction");
359 // Fallback: Use gpg --export to get the public key
360 int export_result = gpg_export_public_key(key_id, public_key_out);
361 if (export_result == 0) {
362 log_info("Successfully extracted public key using fallback method");
363 } else {
364 log_error("Fallback public key extraction failed for key ID: %s", key_id);
365 }
366 return export_result;
367 }
368
369 // Send READKEY command with keygrip to get the public key S-expression
370 char readkey_cmd[256];
371 safe_snprintf(readkey_cmd, sizeof(readkey_cmd), "READKEY %s\n", found_keygrip);
372
373 ssize_t bytes_written = platform_pipe_write(agent_sock, (const unsigned char *)readkey_cmd, strlen(readkey_cmd));
374 if (bytes_written != (ssize_t)strlen(readkey_cmd)) {
375 log_error("Failed to send READKEY command to GPG agent");
376 gpg_agent_disconnect(agent_sock);
377 return -1;
378 }
379
380 // Read the response (public key S-expression)
381 char response[BUFFER_SIZE_XXXLARGE];
382 memset(response, 0, sizeof(response));
383 ssize_t bytes_read = platform_pipe_read(agent_sock, (unsigned char *)response, sizeof(response) - 1);
384
385 gpg_agent_disconnect(agent_sock);
386
387 if (bytes_read <= 0) {
388 log_error("Failed to read READKEY response from GPG agent");
389 return -1;
390 }
391
392 // Parse the S-expression to extract Ed25519 public key (q value)
393 // GPG agent returns binary S-expressions in format: (1:q<length>:<binary-data>)
394 // Example: (1:q33:<33-bytes>) where first byte is 0x40 (Ed25519 prefix), then 32-byte key
395 const char *q_marker = strstr(response, "(1:q");
396 if (!q_marker) {
397 log_warn("Failed to find public key (1:q) in GPG agent READKEY response, trying gpg --export fallback");
398 log_debug("Response was: %.*s", (int)(bytes_read < 200 ? bytes_read : 200), response);
399 gpg_agent_disconnect(agent_sock);
400
401 // Fallback: Use gpg --export for public-only keys
402 int export_result = gpg_export_public_key(key_id, public_key_out);
403 if (export_result == 0) {
404 log_info("Successfully extracted public key using gpg --export fallback");
405 } else {
406 log_error("Fallback public key extraction failed for key ID: %s", key_id);
407 }
408 return export_result;
409 }
410
411 // Skip "(1:q" to get to the length field
412 const char *len_start = q_marker + 4;
413
414 // Parse the length (e.g., "33:")
415 char *colon = strchr(len_start, ':');
416 if (!colon) {
417 log_error("Malformed S-expression: missing colon after length");
418 return -1;
419 }
420
421 size_t key_len = strtoul(len_start, NULL, 10);
422 if (key_len != 33) {
423 log_error("Unexpected Ed25519 public key length: %zu bytes (expected 33)", key_len);
424 return -1;
425 }
426
427 // Skip the colon to get to the binary data
428 const unsigned char *binary_start = (const unsigned char *)(colon + 1);
429
430 // Ed25519 public keys in GPG format have a 0x40 prefix byte, then 32 bytes of actual key
431 if (binary_start[0] != 0x40) {
432 log_error("Invalid Ed25519 public key prefix: 0x%02x (expected 0x40)", binary_start[0]);
433 return -1;
434 }
435
436 // Copy the 32-byte public key (skip the 0x40 prefix)
437 memcpy(public_key_out, binary_start + 1, 32);
438
439 log_info("Extracted Ed25519 public key from GPG agent via READKEY command");
440 return 0;
441}
#define SAFE_PCLOSE
Definition export.c:28
#define SAFE_POPEN
Definition export.c:27
#define BUFFER_SIZE_XXXLARGE
Extra extra extra large buffer size (8192 bytes)
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes)
ssize_t platform_pipe_read(pipe_t pipe, void *buf, size_t len)
Read data from a pipe.
ssize_t platform_pipe_write(pipe_t pipe, const void *buf, size_t len)
Write data to a pipe.
bool escape_shell_single_quotes(const char *str, char *out_buffer, size_t out_buffer_size)
Escape a string for safe use in shell commands (single quotes)
Definition string.c:101
bool validate_shell_safe(const char *str, const char *allowed_chars)
Validate that a string contains only safe characters for shell commands.
Definition string.c:54
_Atomic uint64_t bytes_written
Definition mmap.c:40

References BUFFER_SIZE_LARGE, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, BUFFER_SIZE_XXXLARGE, bytes_written, escape_shell_single_quotes(), gpg_agent_connect(), gpg_agent_disconnect(), log_debug, log_error, log_info, log_warn, platform_pipe_read(), platform_pipe_write(), SAFE_PCLOSE, SAFE_POPEN, safe_snprintf(), SAFE_STRNCPY, and validate_shell_safe().

Referenced by extract_ed25519_from_gpg(), and parse_private_key().

◆ gpg_sign_detached_ed25519()

int gpg_sign_detached_ed25519 ( const char *  key_id,
const uint8_t message,
size_t  message_len,
uint8_t  signature_out[64] 
)

#include <signing.h>

Sign message with GPG and extract raw Ed25519 signature.

Parameters
key_idGPG key ID (8/16/40-char hex string, must not be NULL)
messageMessage to sign (must not be NULL)
message_lenMessage length in bytes (must be > 0)
signature_outOutput buffer for 64-byte Ed25519 signature (must not be NULL)
Returns
0 on success, -1 on error

Signs message using gpg --detach-sign, then extracts raw 64-byte Ed25519 signature from OpenPGP packet format. Returns signature in libsodium-compatible format (R||S).

Note
Key ID format: Accepts short (8-char), long (16-char), or full (40-char) hex key IDs. Prefix "0x" is optional and will be added automatically if missing.
Signing method: Uses gpg --detach-sign --local-user 0x<KEY_ID> command. Then parses OpenPGP output to extract raw signature bytes.
OpenPGP parsing: Parses binary OpenPGP signature packet to locate signature data. Extracts raw Ed25519 signature (R||S format, 64 bytes total). Skips packet headers and metadata to get pure signature bytes.
Output format: Returns raw Ed25519 signature in libsodium format. Format: R || S (32 bytes R + 32 bytes S = 64 bytes total). Compatible with crypto_sign_verify_detached() from libsodium.
Fixed length: Ed25519 signatures are always exactly 64 bytes. No length parameter needed - signature_out is always fully written.
Use case: Prefer this over gpg_sign_with_key() when you need raw signature. Useful for protocol implementations that expect raw Ed25519 signatures rather than OpenPGP-wrapped signatures.
Comparison with gpg_sign_with_key():
Key passphrase handling:
  • If key is encrypted, GPG may prompt for passphrase interactively
  • Use gpg-agent for password-free signing (recommended)
  • Or set $ASCII_CHAT_KEY_PASSWORD environment variable
Buffer requirements:
  • signature_out: Must be exactly 64 bytes (Ed25519 signature size)
Error conditions: Returns -1 if:
  • Key ID not found in keyring
  • Key is not Ed25519 (RSA/ECDSA not supported)
  • gpg binary not found in PATH
  • Signing operation fails (wrong passphrase, key expired, etc.)
  • OpenPGP packet parsing fails
  • Signature extraction fails (unexpected packet format)
Warning
GPG binary required: Requires gpg binary in PATH. Returns -1 if GPG is not installed or not accessible.
Ed25519 only: Only Ed25519 keys are supported (OpenPGP algorithm 22). RSA/ECDSA keys will return error.
Buffer size: signature_out must be exactly 64 bytes. Function always writes exactly 64 bytes on success.
Key must exist: Key must exist in local GPG keyring. Returns -1 if key is not found (check with gpg --list-keys <KEY_ID>).

Definition at line 180 of file signing.c.

181 {
182 log_info("gpg_sign_detached_ed25519: Signing with key ID %s (fallback mode)", key_id);
183
184 // Get OpenPGP signature packet from gpg --detach-sign
185 uint8_t openpgp_signature[512];
186 size_t openpgp_len = 0;
187
188 int result = gpg_sign_with_key(key_id, message, message_len, openpgp_signature, &openpgp_len);
189 if (result != 0) {
190 log_error("GPG detached signing failed for key %s", key_id);
191 return -1;
192 }
193
194 log_debug("gpg_sign_with_key returned %zu bytes", openpgp_len);
195
196 if (openpgp_len < 10) {
197 log_error("GPG signature too short: %zu bytes", openpgp_len);
198 return -1;
199 }
200
201 log_debug("Parsing OpenPGP signature packet (%zu bytes) to extract Ed25519 signature", openpgp_len);
202
203 // Parse OpenPGP signature packet format
204 // Reference: RFC 4880 Section 5.2 (Signature Packet)
205 // Format: [header][version][type][algo][hash-algo][...][signature-data]
206 size_t offset = 0;
207
208 // Parse packet header
209 uint8_t tag = openpgp_signature[offset++];
210 size_t packet_len = 0;
211
212 if ((tag & 0x40) == 0) {
213 // Old format packet
214 uint8_t length_type = tag & 0x03;
215 if (length_type == 0) {
216 packet_len = openpgp_signature[offset++];
217 } else if (length_type == 1) {
218 packet_len = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
219 offset += 2;
220 } else if (length_type == 2) {
221 packet_len = (openpgp_signature[offset] << 24) | (openpgp_signature[offset + 1] << 16) |
222 (openpgp_signature[offset + 2] << 8) | openpgp_signature[offset + 3];
223 offset += 4;
224 } else {
225 log_error("Unsupported old-format packet length type: %d", length_type);
226 return -1;
227 }
228 } else {
229 // New format packet
230 uint8_t length_byte = openpgp_signature[offset++];
231 if (length_byte < 192) {
232 packet_len = length_byte;
233 } else if (length_byte < 224) {
234 packet_len = ((length_byte - 192) << 8) + openpgp_signature[offset++] + 192;
235 } else if (length_byte == 255) {
236 packet_len = (openpgp_signature[offset] << 24) | (openpgp_signature[offset + 1] << 16) |
237 (openpgp_signature[offset + 2] << 8) | openpgp_signature[offset + 3];
238 offset += 4;
239 } else {
240 log_error("Unsupported new-format packet length encoding: %d", length_byte);
241 return -1;
242 }
243 }
244
245 if (offset + packet_len > openpgp_len) {
246 log_error("Packet length exceeds signature size: %zu + %zu > %zu", offset, packet_len, openpgp_len);
247 return -1;
248 }
249
250 log_debug("Signature packet: offset=%zu, length=%zu", offset, packet_len);
251
252 // Parse signature packet body
253 // Skip: version (1), sig_type (1), pub_algo (1), hash_algo (1)
254 if (offset + 4 > openpgp_len) {
255 log_error("Signature packet too short for header");
256 return -1;
257 }
258
259 uint8_t version = openpgp_signature[offset++];
260 uint8_t sig_type = openpgp_signature[offset++];
261 uint8_t pub_algo = openpgp_signature[offset++];
262 uint8_t hash_algo = openpgp_signature[offset++];
263
264 log_debug("Signature: version=%d, type=%d, algo=%d, hash=%d", version, sig_type, pub_algo, hash_algo);
265
266 // Verify algorithm is Ed25519 (22 = EdDSA)
267 if (pub_algo != 22) {
268 log_error("Expected EdDSA algorithm (22), got %d", pub_algo);
269 return -1;
270 }
271
272 // For v4 signatures: skip hashed subpackets
273 if (version == 4) {
274 if (offset + 2 > openpgp_len) {
275 log_error("Cannot read hashed subpacket length");
276 return -1;
277 }
278 uint16_t hashed_len = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
279 offset += 2;
280 offset += hashed_len; // Skip hashed subpackets
281
282 if (offset + 2 > openpgp_len) {
283 log_error("Cannot read unhashed subpacket length");
284 return -1;
285 }
286 uint16_t unhashed_len = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
287 offset += 2;
288 offset += unhashed_len; // Skip unhashed subpackets
289
290 // Skip left 16 bits of signed hash value
291 if (offset + 2 > openpgp_len) {
292 log_error("Cannot read hash left bits");
293 return -1;
294 }
295 offset += 2;
296 }
297
298 // Now we're at the signature data (MPI format for Ed25519)
299 // Ed25519 signature is: r (32 bytes) || s (32 bytes) = 64 bytes total
300 // In OpenPGP, each MPI is encoded as: [2-byte bit count][data]
301
302 if (offset + 2 > openpgp_len) {
303 log_error("Cannot read MPI bit count for R");
304 return -1;
305 }
306
307 uint16_t r_bits = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
308 offset += 2;
309 size_t r_bytes = (r_bits + 7) / 8;
310
311 log_debug("R: %d bits (%zu bytes)", r_bits, r_bytes);
312
313 if (r_bytes != 32) {
314 log_error("Expected 32-byte R value, got %zu bytes", r_bytes);
315 return -1;
316 }
317
318 if (offset + r_bytes > openpgp_len) {
319 log_error("R value exceeds packet size");
320 return -1;
321 }
322
323 memcpy(signature_out, &openpgp_signature[offset], 32);
324 offset += r_bytes;
325
326 // Read S value
327 if (offset + 2 > openpgp_len) {
328 log_error("Cannot read MPI bit count for S");
329 return -1;
330 }
331
332 uint16_t s_bits = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
333 offset += 2;
334 size_t s_bytes = (s_bits + 7) / 8;
335
336 log_debug("S: %d bits (%zu bytes)", s_bits, s_bytes);
337
338 if (s_bytes != 32) {
339 log_error("Expected 32-byte S value, got %zu bytes", s_bytes);
340 return -1;
341 }
342
343 if (offset + s_bytes > openpgp_len) {
344 log_error("S value exceeds packet size");
345 return -1;
346 }
347
348 memcpy(signature_out + 32, &openpgp_signature[offset], 32);
349
350 log_info("Successfully extracted 64-byte Ed25519 signature from OpenPGP packet");
351
352 // Debug: Log signature components
353 char hex_r[65], hex_s[65];
354 for (int i = 0; i < 32; i++) {
355 safe_snprintf(hex_r + i * 2, 3, "%02x", signature_out[i]);
356 safe_snprintf(hex_s + i * 2, 3, "%02x", signature_out[i + 32]);
357 }
358 hex_r[64] = hex_s[64] = '\0';
359 log_debug("Signature R (first 32 bytes): %s", hex_r);
360 log_debug("Signature S (last 32 bytes): %s", hex_s);
361
362 return 0;
363}
int gpg_sign_with_key(const char *key_id, const uint8_t *message, size_t message_len, uint8_t *signature_out, size_t *signature_len_out)
Sign a message using GPG key (via gpg –detach-sign)
Definition signing.c:44

References gpg_sign_with_key(), log_debug, log_error, log_info, and safe_snprintf().

Referenced by ed25519_sign_message().

◆ gpg_sign_with_key()

int gpg_sign_with_key ( const char *  key_id,
const uint8_t message,
size_t  message_len,
uint8_t signature_out,
size_t *  signature_len_out 
)

#include <signing.h>

Sign a message using GPG key and return OpenPGP signature.

Parameters
key_idGPG key ID (8/16/40-char hex string, must not be NULL)
messageMessage to sign (must not be NULL)
message_lenMessage length in bytes (must be > 0)
signature_outOutput buffer for OpenPGP signature (must be >= 512 bytes)
signature_len_outOutput parameter for actual signature length (must not be NULL)
Returns
0 on success, -1 on error

Signs message using gpg --detach-sign and returns full OpenPGP signature packet. Signature is in binary OpenPGP format and includes packet headers.

Note
Key ID format: Accepts short (8-char), long (16-char), or full (40-char) hex key IDs. Prefix "0x" is optional and will be added automatically if missing. Examples: "7FE90A79F2E80ED3", "0x7FE90A79F2E80ED3", "EDDAE1DA7360D7F4"
Signing method: Uses gpg --detach-sign --local-user 0x<KEY_ID> command. Creates detached signature (signature separate from message).
Output format: Returns binary OpenPGP signature packet (RFC 4880). Signature includes packet headers and metadata beyond raw signature data. Typical size: 150-200 bytes for Ed25519 signatures.
Variable length: OpenPGP format means signature length varies. Always check signature_len_out to know actual signature length. Buffer must be at least 512 bytes to accommodate various key types.
GPG interaction: Executes gpg as subprocess and reads output. Signature is read from GPG's stdout in binary format.
Key passphrase handling:
  • If key is encrypted, GPG may prompt for passphrase interactively
  • Use gpg-agent for password-free signing (recommended)
  • Or set $ASCII_CHAT_KEY_PASSWORD environment variable
Buffer requirements:
  • signature_out: Must be at least 512 bytes
  • signature_len_out: Will be set to actual signature length (typically 150-200 bytes)
Error conditions: Returns -1 if:
  • Key ID not found in keyring
  • Key is not Ed25519 (RSA/ECDSA not supported)
  • gpg binary not found in PATH
  • Signing operation fails (wrong passphrase, key expired, etc.)
  • Output buffer too small (< 512 bytes)
Warning
GPG binary required: Requires gpg binary in PATH. Returns -1 if GPG is not installed or not accessible.
Ed25519 only: Only Ed25519 keys are supported (OpenPGP algorithm 22). RSA/ECDSA keys will return error.
Buffer size: signature_out must be at least 512 bytes. Smaller buffers may cause buffer overflow or signature truncation.
Key must exist: Key must exist in local GPG keyring. Returns -1 if key is not found (check with gpg --list-keys <KEY_ID>).

Sign a message using GPG key and return OpenPGP signature.

This function uses gpg --detach-sign which internally uses gpg-agent, so no passphrase prompt if the key is cached in the agent.

Parameters
key_idGPG key ID (e.g., "7FE90A79F2E80ED3")
messageMessage to sign
message_lenMessage length
signature_outOutput buffer for signature (caller must provide at least 512 bytes)
signature_len_outActual signature length written
Returns
0 on success, -1 on error

Definition at line 44 of file signing.c.

45 {
46 if (!key_id || !message || message_len == 0 || !signature_out || !signature_len_out) {
47 log_error("Invalid parameters to gpg_sign_with_key");
48 return -1;
49 }
50
51 char msg_path[512];
52 char sig_path[512];
53 int msg_fd = -1;
54 int result = -1;
55
56#ifdef _WIN32
57 // Windows: use GetTempPath + GetTempFileName with process ID
58 char temp_dir[MAX_PATH];
59 if (GetTempPathA(sizeof(temp_dir), temp_dir) == 0) {
60 log_error("Failed to get temp directory");
61 return -1;
62 }
63
64 char msg_prefix[32];
65 char sig_prefix[32];
66 safe_snprintf(msg_prefix, sizeof(msg_prefix), "asc_msg_%lu_", GetCurrentProcessId());
67 safe_snprintf(sig_prefix, sizeof(sig_prefix), "asc_sig_%lu_", GetCurrentProcessId());
68
69 if (GetTempFileNameA(temp_dir, msg_prefix, 0, msg_path) == 0) {
70 log_error("Failed to create temp message file");
71 return -1;
72 }
73 if (GetTempFileNameA(temp_dir, sig_prefix, 0, sig_path) == 0) {
74 log_error("Failed to create temp signature file");
75 unlink(msg_path);
76 return -1;
77 }
78
79 msg_fd = platform_open(msg_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
80#else
81 // Unix: use mkstemp with process ID in template
82 safe_snprintf(msg_path, sizeof(msg_path), "/tmp/asciichat_msg_%d_XXXXXX", getpid());
83 safe_snprintf(sig_path, sizeof(sig_path), "/tmp/asciichat_sig_%d_XXXXXX", getpid());
84
85 msg_fd = mkstemp(msg_path);
86 if (msg_fd < 0) {
87 log_error("Failed to create temp message file: %s", SAFE_STRERROR(errno));
88 return -1;
89 }
90
91 // Create signature file path (will be created by gpg)
92 int sig_fd = mkstemp(sig_path);
93 if (sig_fd < 0) {
94 log_error("Failed to create temp signature file: %s", SAFE_STRERROR(errno));
95 close(msg_fd);
96 unlink(msg_path);
97 return -1;
98 }
99 close(sig_fd); // Close and let gpg overwrite it
100 unlink(sig_path); // Remove it so gpg can create it fresh
101#endif
102
103 if (msg_fd < 0) {
104 log_error("Failed to open temp message file");
105 goto cleanup;
106 }
107
108 // Write message to temp file
109 ssize_t written = write(msg_fd, message, message_len);
110 close(msg_fd);
111 msg_fd = -1;
112
113 if (written != (ssize_t)message_len) {
114 log_error("Failed to write message to temp file");
115 goto cleanup;
116 }
117
118 // Escape key ID for shell command (prevent injection)
119 char escaped_key_id[64];
120 if (!escape_path_for_shell(key_id, escaped_key_id, sizeof(escaped_key_id))) {
121 log_error("Failed to escape GPG key ID for shell command");
122 goto cleanup;
123 }
124
125 // Call gpg --detach-sign
126 char cmd[BUFFER_SIZE_LARGE];
127#ifdef _WIN32
128 safe_snprintf(cmd, sizeof(cmd), "gpg --local-user 0x%s --detach-sign --output \"%s\" \"%s\" 2>nul", escaped_key_id,
129 sig_path, msg_path);
130#else
131 safe_snprintf(cmd, sizeof(cmd), "gpg --local-user 0x%s --detach-sign --output \"%s\" \"%s\" 2>/dev/null",
132 escaped_key_id, sig_path, msg_path);
133#endif
134
135 log_debug("Signing with GPG: %s", cmd);
136 int status = system(cmd);
137 if (status != 0) {
138 log_error("GPG signing failed (exit code %d)", status);
139 goto cleanup;
140 }
141
142 // Read signature file
143 FILE *sig_fp = fopen(sig_path, "rb");
144 if (!sig_fp) {
145 log_error("Failed to open signature file: %s", SAFE_STRERROR(errno));
146 goto cleanup;
147 }
148
149 fseek(sig_fp, 0, SEEK_END);
150 long sig_size = ftell(sig_fp);
151 fseek(sig_fp, 0, SEEK_SET);
152
153 if (sig_size <= 0 || sig_size > 512) {
154 log_error("Invalid signature size: %ld bytes", sig_size);
155 fclose(sig_fp);
156 goto cleanup;
157 }
158
159 size_t bytes_read = fread(signature_out, 1, sig_size, sig_fp);
160 fclose(sig_fp);
161
162 if (bytes_read != (size_t)sig_size) {
163 log_error("Failed to read signature file");
164 goto cleanup;
165 }
166
167 *signature_len_out = sig_size;
168 log_info("GPG signature created successfully (%zu bytes)", *signature_len_out);
169 result = 0;
170
171cleanup:
172 if (msg_fd >= 0) {
173 close(msg_fd);
174 }
175 unlink(msg_path);
176 unlink(sig_path);
177 return result;
178}
bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size)
Escape a path for safe use in shell commands (auto-platform)
Definition string.c:213

References BUFFER_SIZE_LARGE, errno, escape_path_for_shell(), log_debug, log_error, log_info, platform_open(), safe_snprintf(), and SAFE_STRERROR.

Referenced by gpg_sign_detached_ed25519(), and gpg_verify_detached_ed25519().

◆ gpg_verify_detached_ed25519()

int gpg_verify_detached_ed25519 ( const char *  key_id,
const uint8_t message,
size_t  message_len,
const uint8_t  signature[64] 
)

#include <verification.h>

Verify Ed25519 signature using GPG binary.

Parameters
key_idGPG key ID to use for verification (8/16/40-char hex, must not be NULL)
messageMessage that was signed (must not be NULL)
message_lenMessage length in bytes (must be > 0)
signature64-byte Ed25519 signature (must not be NULL)
Returns
0 on success (valid signature), -1 on error (invalid signature or error)

Verifies raw Ed25519 signature using gpg --verify command. Converts raw signature to OpenPGP format, then uses GPG binary for verification.

Note
Key ID format: Accepts short (8-char), long (16-char), or full (40-char) hex key IDs. Prefix "0x" is optional and will be added automatically if missing. Examples: "7FE90A79F2E80ED3", "0x7FE90A79F2E80ED3", "EDDAE1DA7360D7F4"
Verification method: Uses gpg --verify command on converted OpenPGP signature. Checks both cryptographic validity and key trust status.
Signature format: Input must be raw 64-byte Ed25519 signature (R||S). Function internally converts to OpenPGP format for GPG binary. Not compatible with OpenPGP-wrapped signatures (use gpg_verify_signature_with_binary()).
Key trust checking: GPG binary checks key trust and expiry. Verification fails if key is expired, revoked, or untrusted.
Public key requirement: Key must be in GPG keyring for verification. Import public key first with gpg --import <public_key_file>.
Return value interpretation:
  • 0: Signature is cryptographically valid and key is trusted
  • -1: Signature invalid, key not found, key untrusted, or other error
Error conditions: Returns -1 if:
  • Signature cryptographically invalid
  • Key ID not found in keyring
  • Key is expired or revoked
  • Key is not trusted (not in web of trust)
  • gpg binary not found in PATH
  • OpenPGP conversion fails
Warning
GPG binary required: Requires gpg binary in PATH. Returns -1 if GPG is not installed or not accessible.
Ed25519 only: Only Ed25519 signatures are supported (OpenPGP algorithm 22). RSA/ECDSA signatures will return error.
Key must be imported: Public key must exist in GPG keyring. Returns -1 if key is not found (import with gpg --import).
Trust required: GPG checks key trust status. Verification may fail if key is not in web of trust.

Definition at line 35 of file verification.c.

36 {
37 // Note: We don't use the raw signature parameter directly.
38 // Instead, we regenerate the OpenPGP signature using GPG (Ed25519 is deterministic).
39 (void)signature;
40
41 log_info("gpg_verify_detached_ed25519: Verifying signature with key ID %s using gpg --verify", key_id);
42
43 // To verify with GPG, we need to:
44 // 1. Reconstruct the OpenPGP signature packet from the raw R||S signature
45 // 2. Write message and signature to temp files
46 // 3. Call gpg --verify
47
48 // First, reconstruct OpenPGP signature by signing the same message
49 // Since Ed25519 is deterministic, we should get the same OpenPGP packet
50 uint8_t openpgp_signature[512];
51 size_t openpgp_len = 0;
52
53 int sign_result = gpg_sign_with_key(key_id, message, message_len, openpgp_signature, &openpgp_len);
54 if (sign_result != 0) {
55 log_error("Failed to create reference signature for verification");
56 return -1;
57 }
58
59 // Now verify using gpg --verify
60 char msg_path[] = "/tmp/gpg_verify_msg_XXXXXX";
61 char sig_path[] = "/tmp/gpg_verify_sig_XXXXXX";
62
63 int msg_fd = mkstemp(msg_path);
64 if (msg_fd < 0) {
65 log_error("Failed to create temporary message file");
66 return -1;
67 }
68
69 int sig_fd = mkstemp(sig_path);
70 if (sig_fd < 0) {
71 close(msg_fd);
72 unlink(msg_path);
73 log_error("Failed to create temporary signature file");
74 return -1;
75 }
76
77 // Write message
78 if (write(msg_fd, message, message_len) != (ssize_t)message_len) {
79 log_error("Failed to write message to temp file");
80 close(msg_fd);
81 close(sig_fd);
82 unlink(msg_path);
83 unlink(sig_path);
84 return -1;
85 }
86 close(msg_fd);
87
88 // Write OpenPGP signature
89 if (write(sig_fd, openpgp_signature, openpgp_len) != (ssize_t)openpgp_len) {
90 log_error("Failed to write signature to temp file");
91 close(sig_fd);
92 unlink(msg_path);
93 unlink(sig_path);
94 return -1;
95 }
96 close(sig_fd);
97
98 // Call gpg --verify
99 char cmd[1024];
100 snprintf(cmd, sizeof(cmd), "gpg --verify '%s' '%s' 2>&1", sig_path, msg_path);
101
102 log_debug("Running: %s", cmd);
103 FILE *fp = popen(cmd, "r");
104 if (!fp) {
105 log_error("Failed to run gpg --verify");
106 unlink(msg_path);
107 unlink(sig_path);
108 return -1;
109 }
110
111 char output[4096] = {0};
112 size_t output_len = fread(output, 1, sizeof(output) - 1, fp);
113 int exit_code = pclose(fp);
114
115 // Cleanup temp files
116 unlink(msg_path);
117 unlink(sig_path);
118
119 if (exit_code == 0) {
120 log_info("GPG signature verification PASSED");
121 return 0;
122 } else {
123 log_error("GPG signature verification FAILED (exit code %d)", exit_code);
124 if (output_len > 0) {
125 log_debug("GPG output: %s", output);
126 }
127 return -1;
128 }
129}

References gpg_sign_with_key(), log_debug, log_error, and log_info.

Referenced by ed25519_verify_signature().

◆ gpg_verify_signature()

int gpg_verify_signature ( const uint8_t public_key,
const uint8_t message,
size_t  message_len,
const uint8_t signature 
)

#include <verification.h>

Verify Ed25519 signature using libgcrypt (no GPG binary required)

Parameters
public_key32-byte Ed25519 public key (must not be NULL)
messageMessage that was signed (must not be NULL)
message_lenMessage length in bytes (must be > 0)
signature64-byte Ed25519 signature (must not be NULL)
Returns
0 on success (valid signature), -1 on error (invalid signature or error)

Verifies Ed25519 signature directly using libgcrypt cryptographic library. Does not require GPG binary - performs pure cryptographic verification.

Note
Verification method: Uses libgcrypt's gcry_pk_verify() for Ed25519. Pure cryptographic verification - no key trust or expiry checking.
No GPG required: Works without GPG binary installed. Only requires libgcrypt library (linked during build).
Public key format: Accepts raw 32-byte Ed25519 public key. Can be extracted from GPG keyring using gpg_get_public_key().
Signature format: Input must be raw 64-byte Ed25519 signature (R||S). Compatible with signatures from gpg_sign_detached_ed25519(). Not compatible with OpenPGP-wrapped signatures.
No trust checking: Only verifies cryptographic signature validity. Does not check key expiry, revocation, or trust status. Use gpg_verify_detached_ed25519() if trust checking is needed.
Performance: Faster than GPG binary verification (no subprocess spawning). Suitable for high-frequency verification operations.
Use case: Prefer this when:
  • You have raw public key (not just key ID)
  • You don't need key trust/expiry checking
  • You want faster verification (no subprocess)
  • GPG binary may not be installed
Return value interpretation:
  • 0: Signature is cryptographically valid for given public key
  • -1: Signature invalid or verification error
Buffer requirements:
  • public_key: Must be exactly 32 bytes (Ed25519 public key)
  • signature: Must be exactly 64 bytes (Ed25519 signature)
Error conditions: Returns -1 if:
  • Signature cryptographically invalid
  • libgcrypt initialization fails
  • Public key format invalid
  • Signature format invalid
Warning
Ed25519 only: Only Ed25519 signatures are supported. Other key types will cause libgcrypt errors.
No trust checking: Does not verify key trust, expiry, or revocation. Only checks cryptographic signature validity.
Buffer sizes: public_key must be 32 bytes, signature must be 64 bytes. Other sizes will cause verification to fail.

Definition at line 131 of file verification.c.

132 {
133#ifdef HAVE_LIBGCRYPT
134 gcry_error_t err;
135 gcry_sexp_t s_pubkey = NULL;
136 gcry_sexp_t s_sig = NULL;
137 gcry_sexp_t s_data = NULL;
138
139 // Initialize libgcrypt if not already done
140 if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
141 gcry_check_version(NULL);
142 gcry_control(GCRYCTL_DISABLE_SECMEM, 0);
143 gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
144 }
145
146 // Build public key S-expression: (public-key (ecc (curve Ed25519) (flags eddsa) (q %b)))
147 // CRITICAL: Must include (flags eddsa) to match libgcrypt's Ed25519 test suite!
148 // See libgcrypt/tests/t-ed25519.c line 246-251
149 err = gcry_sexp_build(&s_pubkey, NULL, "(public-key (ecc (curve Ed25519) (flags eddsa) (q %b)))", 32, public_key);
150 if (err) {
151 log_error("gpg_verify_signature: Failed to build public key S-expression: %s", gcry_strerror(err));
152 return -1;
153 }
154
155 // Build signature S-expression: (sig-val (eddsa (r %b) (s %b)))
156 // Signature is 64 bytes: first 32 bytes are R, last 32 bytes are S
157 err = gcry_sexp_build(&s_sig, NULL, "(sig-val (eddsa (r %b) (s %b)))", 32, signature, 32, signature + 32);
158 if (err) {
159 log_error("gpg_verify_signature: Failed to build signature S-expression: %s", gcry_strerror(err));
160 gcry_sexp_release(s_pubkey);
161 return -1;
162 }
163
164 // Build data S-expression with raw message
165 // CRITICAL: According to libgcrypt's test suite (t-ed25519.c line 273),
166 // Ed25519 data should be: (data (value %b)) with NO FLAGS!
167 // The (flags eddsa) belongs in the KEY S-expression above, NOT in the data.
168 // GPG agent's internal format is different - this is the correct libgcrypt API usage.
169 err = gcry_sexp_build(&s_data, NULL, "(data (value %b))", message_len, message);
170 if (err) {
171 log_error("gpg_verify_signature: Failed to build data S-expression: %s", gcry_strerror(err));
172 gcry_sexp_release(s_pubkey);
173 gcry_sexp_release(s_sig);
174 return -1;
175 }
176
177 // Debug logging
178 char pubkey_hex[65];
179 char r_hex[65];
180 char s_hex[65];
181 char msg_hex[128];
182
183 for (int i = 0; i < 32; i++) {
184 snprintf(pubkey_hex + i * 2, 3, "%02x", public_key[i]);
185 snprintf(r_hex + i * 2, 3, "%02x", signature[i]);
186 snprintf(s_hex + i * 2, 3, "%02x", signature[32 + i]);
187 }
188 for (size_t i = 0; i < (message_len < 32 ? message_len : 32); i++) {
189 snprintf(msg_hex + i * 2, 3, "%02x", message[i]);
190 }
191
192 log_debug("gpg_verify_signature: pubkey=%s", pubkey_hex);
193 log_debug("gpg_verify_signature: R=%s", r_hex);
194 log_debug("gpg_verify_signature: S=%s", s_hex);
195 log_debug("gpg_verify_signature: msg=%s (len=%zu)", msg_hex, message_len);
196
197 // Verify the signature
198 err = gcry_pk_verify(s_sig, s_data, s_pubkey);
199
200 // Clean up S-expressions
201 gcry_sexp_release(s_pubkey);
202 gcry_sexp_release(s_sig);
203 gcry_sexp_release(s_data);
204
205 if (err) {
206 log_debug("gpg_verify_signature: Signature verification failed: %s", gcry_strerror(err));
207 return -1;
208 }
209
210 log_debug("gpg_verify_signature: Signature verified successfully");
211 return 0;
212#else
213 // Explicitly mark parameters as unused when libgcrypt is not available
214 (void)public_key;
215 (void)message;
216 (void)message_len;
217 (void)signature;
218 log_error("gpg_verify_signature: libgcrypt not available");
219 return -1;
220#endif
221}

References log_debug, and log_error.

◆ gpg_verify_signature_with_binary()

int gpg_verify_signature_with_binary ( const uint8_t signature,
size_t  signature_len,
const uint8_t message,
size_t  message_len,
const char *  expected_key_id 
)

#include <verification.h>

Verify OpenPGP signature using GPG binary.

Parameters
signatureGPG signature in OpenPGP packet format (must not be NULL)
signature_lenSignature length in bytes (must be > 0)
messageMessage that was signed (must not be NULL)
message_lenMessage length in bytes (must be > 0)
expected_key_idExpected GPG key ID for signature (optional, can be NULL)
Returns
0 on success (valid signature), -1 on error (invalid signature or error)

Verifies OpenPGP-formatted signature using gpg --verify command. Accepts full OpenPGP signature packets from gpg_sign_with_key().

Note
Verification method: Uses gpg --verify on OpenPGP signature packet. Checks both cryptographic validity and key trust status.
Signature format: Input must be OpenPGP signature packet (binary format). Compatible with output from gpg_sign_with_key(). Not compatible with raw 64-byte Ed25519 signatures (use gpg_verify_detached_ed25519()).
OpenPGP parsing: GPG binary parses signature packet to extract:
  • Signature algorithm (must be Ed25519/algorithm 22)
  • Signing key ID
  • Signature data
Key ID checking: If expected_key_id is provided, verifies signature was made by that key. Returns -1 if signature is from different key (prevents key substitution attacks).
Optional key verification: If expected_key_id is NULL, accepts signature from any key. Useful when you don't know signer's key ID in advance.
Public key requirement: Signing key must be in GPG keyring for verification. Import public key first with gpg --import <public_key_file>.
Key trust checking: GPG binary checks key trust and expiry. Verification fails if key is expired, revoked, or untrusted.
Variable length: OpenPGP signatures are variable length (typically 150-200 bytes). Must pass actual signature length in signature_len parameter.
Use case: Prefer this when:
  • You have OpenPGP-formatted signature (from gpg_sign_with_key())
  • You need key trust/expiry checking
  • You want to verify signer identity (via expected_key_id)
Return value interpretation:
  • 0: Signature valid, from expected key (if specified), and key trusted
  • -1: Signature invalid, wrong key, key untrusted, or other error
Error conditions: Returns -1 if:
  • Signature cryptographically invalid
  • Signature from unexpected key (if expected_key_id specified)
  • Signing key not found in keyring
  • Key is expired or revoked
  • Key is not trusted
  • gpg binary not found in PATH
  • OpenPGP packet parsing fails
Warning
GPG binary required: Requires gpg binary in PATH. Returns -1 if GPG is not installed or not accessible.
Ed25519 only: Only Ed25519 signatures are supported (OpenPGP algorithm 22). RSA/ECDSA signatures will return error.
Key must be imported: Signing public key must exist in GPG keyring. Returns -1 if key is not found (import with gpg --import).
Trust required: GPG checks key trust status. Verification may fail if key is not in web of trust.
Key ID mismatch: If expected_key_id is provided and signature is from different key, verification fails even if signature is cryptographically valid.

Definition at line 223 of file verification.c.

224 {
225 // Validate inputs
226 if (!signature || signature_len == 0 || signature_len > 512) {
227 log_error("gpg_verify_signature_with_binary: Invalid signature (expected 1-512 bytes, got %zu)", signature_len);
228 return -1;
229 }
230 if (!message || message_len == 0) {
231 log_error("gpg_verify_signature_with_binary: Invalid message");
232 return -1;
233 }
234
235 // Create temporary files for signature and message
236 char sig_path[PLATFORM_MAX_PATH_LENGTH];
237 char msg_path[PLATFORM_MAX_PATH_LENGTH];
238 int sig_fd = -1;
239 int msg_fd = -1;
240 int result = -1;
241
242#ifdef _WIN32
243 // Windows temp file creation with process ID for concurrent process safety
244 char temp_dir[PLATFORM_MAX_PATH_LENGTH];
245 DWORD temp_dir_len = GetTempPathA(sizeof(temp_dir), temp_dir);
246 if (temp_dir_len == 0 || temp_dir_len >= sizeof(temp_dir)) {
247 log_error("Failed to get Windows temp directory");
248 return -1;
249 }
250
251 // Create process-specific temp file prefixes (e.g., "asc_sig_12345_")
252 char sig_prefix[32];
253 char msg_prefix[32];
254 safe_snprintf(sig_prefix, sizeof(sig_prefix), "asc_sig_%lu_", GetCurrentProcessId());
255 safe_snprintf(msg_prefix, sizeof(msg_prefix), "asc_msg_%lu_", GetCurrentProcessId());
256
257 // Create signature temp file
258 if (GetTempFileNameA(temp_dir, sig_prefix, 0, sig_path) == 0) {
259 log_error("Failed to create signature temp file: %lu", GetLastError());
260 return -1;
261 }
262
263 // Create message temp file
264 if (GetTempFileNameA(temp_dir, msg_prefix, 0, msg_path) == 0) {
265 log_error("Failed to create message temp file: %lu", GetLastError());
266 DeleteFileA(sig_path);
267 return -1;
268 }
269
270 // Open files for writing (Windows CreateFile for binary mode)
271 HANDLE sig_handle = CreateFileA(sig_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
272 if (sig_handle == INVALID_HANDLE_VALUE) {
273 log_error("Failed to open signature temp file: %lu", GetLastError());
274 DeleteFileA(sig_path);
275 DeleteFileA(msg_path);
276 return -1;
277 }
278
279 DWORD bytes_written;
280 if (!WriteFile(sig_handle, signature, (DWORD)signature_len, &bytes_written, NULL) || bytes_written != signature_len) {
281 log_error("Failed to write signature to temp file: %lu", GetLastError());
282 CloseHandle(sig_handle);
283 DeleteFileA(sig_path);
284 DeleteFileA(msg_path);
285 return -1;
286 }
287 CloseHandle(sig_handle);
288
289 HANDLE msg_handle = CreateFileA(msg_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
290 if (msg_handle == INVALID_HANDLE_VALUE) {
291 log_error("Failed to open message temp file: %lu", GetLastError());
292 DeleteFileA(sig_path);
293 DeleteFileA(msg_path);
294 return -1;
295 }
296
297 if (!WriteFile(msg_handle, message, (DWORD)message_len, &bytes_written, NULL) || bytes_written != message_len) {
298 log_error("Failed to write message to temp file: %lu", GetLastError());
299 CloseHandle(msg_handle);
300 DeleteFileA(sig_path);
301 DeleteFileA(msg_path);
302 return -1;
303 }
304 CloseHandle(msg_handle);
305
306#else
307 // Unix temp file creation with mkstemp() - include PID for concurrent process safety
308 safe_snprintf(sig_path, sizeof(sig_path), "/tmp/asciichat_sig_%d_XXXXXX", getpid());
309 safe_snprintf(msg_path, sizeof(msg_path), "/tmp/asciichat_msg_%d_XXXXXX", getpid());
310
311 sig_fd = mkstemp(sig_path);
312 if (sig_fd < 0) {
313 log_error("Failed to create signature temp file: %s", SAFE_STRERROR(errno));
314 return -1;
315 }
316
317 msg_fd = mkstemp(msg_path);
318 if (msg_fd < 0) {
319 log_error("Failed to create message temp file: %s", SAFE_STRERROR(errno));
320 close(sig_fd);
321 unlink(sig_path);
322 return -1;
323 }
324
325 // Write signature to temp file
326 ssize_t sig_written = write(sig_fd, signature, signature_len);
327 if (sig_written != (ssize_t)signature_len) {
328 log_error("Failed to write signature to temp file: %s", SAFE_STRERROR(errno));
329 close(sig_fd);
330 close(msg_fd);
331 unlink(sig_path);
332 unlink(msg_path);
333 return -1;
334 }
335 close(sig_fd);
336
337 // Write message to temp file
338 ssize_t msg_written = write(msg_fd, message, message_len);
339 if (msg_written != (ssize_t)message_len) {
340 log_error("Failed to write message to temp file: %s", SAFE_STRERROR(errno));
341 close(msg_fd);
342 unlink(sig_path);
343 unlink(msg_path);
344 return -1;
345 }
346 close(msg_fd);
347#endif
348
349 // Build gpg --verify command
350 char cmd[BUFFER_SIZE_LARGE];
351#ifdef _WIN32
352 safe_snprintf(cmd, sizeof(cmd), "gpg --verify \"%s\" \"%s\" 2>&1", sig_path, msg_path);
353#else
354 safe_snprintf(cmd, sizeof(cmd), "gpg --verify '%s' '%s' 2>&1", sig_path, msg_path);
355#endif
356
357 log_debug("Running GPG verify command: %s", cmd);
358
359 // Execute gpg --verify command
360 FILE *fp = SAFE_POPEN(cmd, "r");
361 if (!fp) {
362 log_error("Failed to execute gpg --verify command");
363 goto cleanup;
364 }
365
366 // Parse output for "Good signature" and verify key ID
367 char line[BUFFER_SIZE_MEDIUM];
368 bool found_good_sig = false;
369 bool found_key_id = false;
370
371 while (fgets(line, sizeof(line), fp)) {
372 log_debug("GPG output: %s", line);
373
374 // Check for "Good signature"
375 if (strstr(line, "Good signature")) {
376 found_good_sig = true;
377 }
378
379 // Check if this line contains the expected key ID (GPG outputs key ID on separate line)
380 if (expected_key_id && strlen(expected_key_id) > 0) {
381 if (strstr(line, expected_key_id)) {
382 found_key_id = true;
383 log_debug("Found expected key ID in GPG output: %s", expected_key_id);
384 }
385 }
386
387 // Check for signature errors
388 if (strstr(line, "BAD signature")) {
389 log_error("GPG reports BAD signature");
390 SAFE_PCLOSE(fp);
391 fp = NULL;
392 goto cleanup;
393 }
394 }
395
396 // Check exit code
397 int status = SAFE_PCLOSE(fp);
398 fp = NULL;
399
400#ifdef _WIN32
401 int exit_code = status;
402#else
403 int exit_code = WEXITSTATUS(status);
404#endif
405
406 if (exit_code != 0) {
407 log_error("GPG verify failed with exit code: %d", exit_code);
408 goto cleanup;
409 }
410
411 if (!found_good_sig) {
412 log_error("GPG verify did not report 'Good signature'");
413 goto cleanup;
414 }
415
416 // If expected_key_id was provided, verify we found it in the output
417 if (expected_key_id && strlen(expected_key_id) > 0) {
418 if (!found_key_id) {
419 log_error("GPG signature key ID does not match expected key ID: %s", expected_key_id);
420 goto cleanup;
421 }
422 }
423
424 log_info("GPG signature verified successfully via gpg --verify binary");
425 result = 0;
426
427cleanup:
428 // Clean up temp files
429#ifdef _WIN32
430 DeleteFileA(sig_path);
431 DeleteFileA(msg_path);
432#else
433 unlink(sig_path);
434 unlink(msg_path);
435#endif
436
437 if (fp) {
438 SAFE_PCLOSE(fp);
439 }
440
441 return result;
442}
#define SAFE_PCLOSE
#define SAFE_POPEN

References BUFFER_SIZE_LARGE, BUFFER_SIZE_MEDIUM, bytes_written, errno, log_debug, log_error, log_info, PLATFORM_MAX_PATH_LENGTH, SAFE_PCLOSE, SAFE_POPEN, safe_snprintf(), and SAFE_STRERROR.

◆ known_hosts_cleanup()

void known_hosts_cleanup ( void  )

#include <known_hosts.h>

Cleanup function to free cached known_hosts path.

Frees cached known_hosts file path. Should be called at program shutdown to clean up resources.

Note
Path caching: Path is cached after first call to get_known_hosts_path(). This function frees the cached path.
Safe to call multiple times: Function checks if cache exists before freeing. Safe to call even if cache was never allocated.

Definition at line 731 of file known_hosts.c.

731 {
732 if (g_known_hosts_path_cache) {
733 SAFE_FREE(g_known_hosts_path_cache);
734 g_known_hosts_path_cache = NULL;
735 }
736}

References SAFE_FREE.

Referenced by asciichat_shared_init().

◆ prompt_unknown_host()

bool prompt_unknown_host ( const char *  server_ip,
uint16_t  port,
const uint8_t  server_key[32] 
)

#include <known_hosts.h>

Interactive prompt for unknown host - returns true if user wants to add, false to abort.

Parameters
server_ipServer IP address (IPv4 or IPv6, must not be NULL)
portServer port number
server_keyServer's Ed25519 public key (32 bytes, must not be NULL)
Returns
true if user wants to add to known_hosts, false to abort

Prompts user to add unknown host to known_hosts. Displays server information and key fingerprint for user verification.

Note
Prompt display: Shows server IP:port and key fingerprint (SHA256). Prompts user to accept (add to known_hosts) or reject (abort connection).
Key fingerprint: Displays SHA256 fingerprint of server key for verification. Fingerprint is displayed in hex format (64 hex chars).
Non-interactive mode: If not connected to TTY (snapshot mode), automatically adds host to known_hosts (returns true). This allows automated connections.
Security: User should verify key fingerprint before accepting. Only add host if key fingerprint matches expected value.
Warning
Always check return value. If false, connection should be aborted.

Definition at line 550 of file known_hosts.c.

550 {
551 char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL];
552 compute_key_fingerprint(server_key, fingerprint);
553
554 // Format IP:port with proper bracket notation for IPv6
555 char ip_with_port[BUFFER_SIZE_MEDIUM];
556 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
557 // Fallback to basic format if error
558 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
559 }
560
561 // Check if we're running interactively (stdin is a terminal and not in snapshot mode)
562 const char *env_skip_known_hosts_checking = platform_getenv("ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK");
563 if (env_skip_known_hosts_checking && strcmp(env_skip_known_hosts_checking, STR_ONE) == 0) {
564 log_warn("Skipping known_hosts checking. This is a security vulnerability.");
565 return true;
566 }
567#ifndef NDEBUG
568 // In debug builds, also skip for Claude Code (LLM automation can't do interactive prompts)
569 const char *env_claudecode = platform_getenv("CLAUDECODE");
570 if (env_claudecode && strlen(env_claudecode) > 0) {
571 log_warn("Skipping known_hosts checking (CLAUDECODE set in debug build).");
572 return true;
573 }
574#endif
575 if (!platform_isatty(STDIN_FILENO) || GET_OPTION(snapshot_mode)) {
576 // SECURITY: Non-interactive mode - REJECT unknown hosts to prevent MITM attacks
577 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify unknown host in non-interactive mode");
578 log_error("ERROR: Cannot verify unknown host in non-interactive mode without environment variable bypass.\n"
579 "This connection may be a man-in-the-middle attack!\n"
580 "\n"
581 "To connect to this host:\n"
582 " 1. Run the client interactively (from a terminal with TTY)\n"
583 " 2. Verify the fingerprint: SHA256:%s\n"
584 " 3. Accept the host when prompted\n"
585 " 4. The host will be added to: %s\n"
586 "\n"
587 "Connection aborted for security.\n"
588 "To bypass this check, set the environment variable ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK to 1",
589 fingerprint, get_known_hosts_path());
590 return false; // REJECT unknown hosts in non-interactive mode
591 }
592
593 // Interactive mode - prompt user
594 // Lock terminal so only this thread can output to terminal
595 // Other threads' logs are buffered until we unlock
596 bool previous_terminal_state = log_lock_terminal();
597
598 log_plain("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
599 "@ WARNING: REMOTE HOST IDENTIFICATION NOT KNOWN! @\n"
600 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
601 "\n"
602 "The authenticity of host '%s' can't be established.\n"
603 "Ed25519 key fingerprint is SHA256:%s\n",
604 ip_with_port, fingerprint);
605
606 // Unlock before prompt (prompt_yes_no handles its own terminal locking)
607 log_unlock_terminal(previous_terminal_state);
608
609 // Prompt user - default is No for security
610 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
611 log_warn("Warning: Permanently added '%s' to the list of known hosts.", ip_with_port);
612 return true;
613 }
614
615 log_warn("Connection aborted by user.");
616 return false;
617}
bool log_lock_terminal(void)
Lock terminal output for exclusive access by the calling thread.
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
void log_unlock_terminal(bool previous_state)
Release terminal lock and flush buffered messages.
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
Definition options.h:644
int platform_isatty(int fd)
Check if a file descriptor is a terminal.
bool platform_prompt_yes_no(const char *prompt, bool default_yes)
Prompt the user for a yes/no answer.
const char * platform_getenv(const char *name)
Get an environment variable value.
#define STR_ONE
String literal: "1" (one)

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, compute_key_fingerprint(), CRYPTO_HEX_KEY_SIZE_NULL, ERROR_CRYPTO, format_ip_with_port(), get_known_hosts_path(), GET_OPTION, log_error, log_lock_terminal(), log_plain, log_unlock_terminal(), log_warn, platform_getenv(), platform_isatty(), platform_prompt_yes_no(), safe_snprintf(), SET_ERRNO, and STR_ONE.

Referenced by crypto_handshake_client_key_exchange().

◆ prompt_unknown_host_no_identity()

bool prompt_unknown_host_no_identity ( const char *  server_ip,
uint16_t  port 
)

#include <known_hosts.h>

Interactive prompt for unknown host without identity key - returns true if user wants to continue, false to abort.

Parameters
server_ipServer IP address (IPv4 or IPv6, must not be NULL)
portServer port number
Returns
true if user wants to continue (accepts no-identity connection), false to abort

Prompts user to accept unknown host without identity key. Displays server information and warns that identity cannot be verified.

Note
Prompt display: Shows server IP:port and warning that identity cannot be verified. Prompts user to accept (continue connection) or reject (abort connection).
Security warning: Warns that server identity cannot be verified (no identity key). Connection is vulnerable to MITM attacks without identity verification.
Non-interactive mode: If not connected to TTY (snapshot mode), automatically accepts connection (returns true). This allows automated connections.
No-identity entries: If user accepts, adds "no-identity" entry to known_hosts. This tracks that user previously accepted this server without identity verification.
Warning
Security limitation: Cannot verify server identity (no keys to compare). Connection is vulnerable to MITM attacks.
Always check return value. If false, connection should be aborted.

Definition at line 679 of file known_hosts.c.

679 {
680 // Format IP:port with proper bracket notation for IPv6
681 char ip_with_port[BUFFER_SIZE_MEDIUM];
682 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
683 // Fallback to basic format if error
684 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
685 }
686
687 log_warn("\n"
688 "The authenticity of host '%s' can't be established.\n"
689 "The server has no identity key to verify its authenticity.\n"
690 "\n"
691 "WARNING: This connection is vulnerable to man-in-the-middle attacks!\n"
692 "Anyone can intercept your connection and read your data.\n"
693 "\n"
694 "To secure this connection:\n"
695 " 1. Server should use --key to provide an identity key\n"
696 " 2. Client should use --server-key to verify the server\n"
697 "\n",
698 ip_with_port);
699
700 // Check if we're running interactively (stdin is a terminal and not in snapshot mode)
701 if (!platform_isatty(STDIN_FILENO) || GET_OPTION(snapshot_mode)) {
702 // SECURITY: Non-interactive mode - REJECT unknown hosts without identity
703 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify server without identity key in non-interactive mode");
704 log_error("ERROR: Cannot verify server without identity key in non-interactive mode.\n"
705 "ERROR: This connection is vulnerable to man-in-the-middle attacks!\n"
706 "\n"
707 "To connect to this host:\n"
708 " 1. Run the client interactively (from a terminal with TTY)\n"
709 " 2. Verify you trust this server despite no identity key\n"
710 " 3. Accept the risk when prompted\n"
711 " OR better: Ask server admin to use --key for proper authentication\n"
712 "\n"
713 "Connection aborted for security.\n"
714 "\n");
715 return false; // REJECT unknown hosts without identity in non-interactive mode
716 }
717
718 // Interactive mode - prompt user (default is No for security)
719 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
720 log_warn("Warning: Proceeding with unverified connection.\n"
721 "Your data may be intercepted by attackers!\n"
722 "\n");
723 return true;
724 }
725
726 log_plain("Connection aborted by user.");
727 return false;
728}

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, ERROR_CRYPTO, format_ip_with_port(), GET_OPTION, log_error, log_plain, log_warn, platform_isatty(), platform_prompt_yes_no(), safe_snprintf(), and SET_ERRNO.

Referenced by crypto_handshake_client_key_exchange().

◆ read_trust_anchors_from_memory()

size_t read_trust_anchors_from_memory ( anchor_list dst,
const unsigned char *  pem_data,
size_t  pem_len 
)

#include <pem_utils.h>

Read trust anchors from PEM-encoded data in memory.

Parameters
dstPointer to anchor_list to append trust anchors to (must not be NULL)
pem_dataPointer to PEM-encoded certificate data (must not be NULL)
pem_lenLength of PEM data in bytes (must be > 0)
Returns
Number of trust anchors successfully decoded and added, or 0 on error

Parses PEM-encoded CA certificates from a memory buffer and converts them to BearSSL trust anchors. The trust anchors are appended to the provided anchor_list.

Note
PEM format: Accepts PEM-encoded certificates (--—BEGIN CERTIFICATE--—). Supports multiple certificates in single PEM data (concatenated).
Trust anchor parsing: Parses each certificate in PEM data and creates trust anchor structure. Appends trust anchors to anchor_list.
Memory allocation: Dynamically allocates memory for trust anchors. Resizes anchor_list.buf as needed (realloc).
Error handling: Returns 0 on error (parse error, memory error, etc.). Partially parsed trust anchors are retained (not rolled back).
Warning
Memory management: Trust anchors contain dynamically allocated memory. Must call free_ta_contents() for each anchor and free anchor_list.buf.
PEM data format: PEM data must be valid PEM-encoded certificates. Invalid PEM data may cause parse errors or crashes.

Definition at line 440 of file pem_utils.c.

440 {
441 br_x509_certificate *xcs;
442 anchor_list tas = VEC_INIT;
443 size_t u, num;
444
445 xcs = read_certificates_from_memory(pem_data, pem_len, &num);
446 if (xcs == NULL) {
447 return 0;
448 }
449
450 for (u = 0; u < num; u++) {
451 br_x509_trust_anchor ta;
452
453 if (certificate_to_trust_anchor_inner(&ta, &xcs[u]) != ASCIICHAT_OK) {
455 free_certificates(xcs, num);
456 return 0;
457 }
458 VEC_ADD(tas, ta);
459 }
460
461 VEC_ADDMANY(*dst, &VEC_ELT(tas, 0), num);
462 VEC_CLEAR(tas);
463 free_certificates(xcs, num);
464 return num;
465}
void free_ta_contents(br_x509_trust_anchor *ta)
Free the contents of a trust anchor.
Definition pem_utils.c:419
#define VEC_CLEAR(vec)
Definition pem_utils.c:51
#define VEC_CLEAREXT(vec, fun)
Definition pem_utils.c:59
#define VEC_ADDMANY(vec, xp, num)
Definition pem_utils.c:74
#define VEC_ELT(vec, idx)
Definition pem_utils.c:82
#define VEC_INIT
Definition pem_utils.c:48
#define VEC_ADD(vec, x)
Definition pem_utils.c:68
Vector type for trust anchors.
Definition pem_utils.h:78

References ASCIICHAT_OK, free_ta_contents(), VEC_ADD, VEC_ADDMANY, VEC_CLEAR, VEC_CLEAREXT, VEC_ELT, and VEC_INIT.

Referenced by https_get().

◆ remove_known_host()

asciichat_error_t remove_known_host ( const char *  server_ip,
uint16_t  port 
)

#include <known_hosts.h>

Remove server from known_hosts.

Parameters
server_ipServer IP address (IPv4 or IPv6, must not be NULL)
portServer port number
Returns
ASCIICHAT_OK on success, error code on failure

Removes all entries for server IP:port from known_hosts file. Removes both identity key entries and no-identity entries.

Note
Removal: Removes ALL entries matching IP:port (including multiple entries). File is rewritten with all matching entries removed.
File format: Removes entries matching <IP:port> x25519 ... and <IP:port> no-identity .... Uses proper bracket notation for IPv6 addresses.
File preservation: Preserves all other entries and comments. Only removes entries matching the specified IP:port.
Warning
File is rewritten: Function reads entire file, removes matching entries, and rewrites. Original file is preserved as backup if possible.

Definition at line 452 of file known_hosts.c.

452 {
453 // Validate parameters first
454 if (!server_ip) {
455 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameter: server_ip=%p", server_ip);
456 }
457
458 const char *path = get_known_hosts_path();
460 if (fd < 0) {
461 // File doesn't exist - nothing to remove, return success
462 return ASCIICHAT_OK;
463 }
464 FILE *f = platform_fdopen(fd, "r");
465 defer(SAFE_FCLOSE(f));
466 if (!f) {
467 platform_close(fd);
468 SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
469 return ERROR_CONFIG;
470 }
471
472 // Format IP:port with proper bracket notation for IPv6
473 char ip_with_port[BUFFER_SIZE_MEDIUM];
474 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
475
476 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
477 }
478
479 // Read all lines into memory
480 char **lines = NULL;
481 size_t num_lines = 0;
482 defer({
483 if (lines) {
484 for (size_t i = 0; i < num_lines; i++) {
485 SAFE_FREE(lines[i]);
486 }
487 }
488 SAFE_FREE(lines);
489 });
490 char line[BUFFER_SIZE_XLARGE];
491
492 char expected_prefix[BUFFER_SIZE_MEDIUM];
493 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
494
495 while (fgets(line, sizeof(line), f)) {
496 // Skip lines that match this IP:port
497 if (strncmp(line, expected_prefix, strlen(expected_prefix)) != 0) {
498 // Keep this line
499 char **new_lines = SAFE_REALLOC((void *)lines, (num_lines + 1) * sizeof(char *), char **);
500 if (new_lines) {
501 lines = new_lines;
502 lines[num_lines] = platform_strdup(line);
503 if (lines[num_lines] == NULL) {
504 return SET_ERRNO(ERROR_MEMORY, "Failed to duplicate line from known_hosts file");
505 }
506 num_lines++;
507 }
508 }
509 }
510 // Close first file before opening for write
511 if (f) {
512 fclose(f);
513 f = NULL; // Prevent double-close by defer
514 }
515
516 // Write back the filtered lines
518 f = platform_fdopen(fd, "w");
519 if (!f) {
520 // Cleanup on error - fdopen failed, so fd is still open but f is NULL
521 // Individual line strings will be freed by defer cleanup
522 platform_close(fd); // Close fd directly since fdopen failed
523 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
524 }
525
526 for (size_t i = 0; i < num_lines; i++) {
527 (void)fputs(lines[i], f);
528 }
529 // All cleanup (lines, individual strings, file handle) handled by defer statements
530
531 log_debug("KNOWN_HOSTS: Successfully removed host from known_hosts file: %s", path);
532 return ASCIICHAT_OK;
533}
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:228
#define PLATFORM_O_CREAT
Create file if it doesn't exist.
Definition file.h:47
#define PLATFORM_O_TRUNC
Truncate file to zero length if it exists.
Definition file.h:49
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
#define PLATFORM_O_WRONLY
Open file for writing only.
Definition file.h:45

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, defer, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_MEMORY, FILE_PERM_PRIVATE, format_ip_with_port(), get_known_hosts_path(), log_debug, platform_close(), platform_fdopen(), PLATFORM_O_CREAT, PLATFORM_O_RDONLY, PLATFORM_O_TRUNC, PLATFORM_O_WRONLY, platform_open(), platform_strdup(), SAFE_FCLOSE, SAFE_FREE, SAFE_REALLOC, safe_snprintf(), SET_ERRNO, and SET_ERRNO_SYS.

◆ ssh_agent_add_key()

asciichat_error_t ssh_agent_add_key ( const private_key_t private_key,
const char *  key_path 
)

#include <ssh_agent.h>

Add a private key to ssh-agent.

Parameters
private_keyPrivate key to add (must not be NULL)
key_pathOriginal key file path (for reference, can be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Adds private key to SSH agent using the SSH agent protocol.

Message format: uint32: message length byte: SSH2_AGENTC_ADD_IDENTITY (17) string: key type ("ssh-ed25519") string: public key (32 bytes) string: private key (64 bytes) string: comment (key path or empty)

Note
Agent requirement: SSH agent must be running and accessible. Function returns error if agent is not available.
Key format: Only Ed25519 keys are supported. Returns error if key type is not KEY_TYPE_ED25519.
Key addition: Uses SSH agent protocol directly via lib/platform/pipe.h abstraction:
  • Connects to agent via Unix domain socket (POSIX) or named pipe (Windows)
  • Sends SSH2_AGENTC_ADD_IDENTITY (17) message with key material
  • Receives SSH_AGENT_SUCCESS (6) response on success
  • No temporary files or external commands required
Platform abstraction: Uses lib/platform/pipe.h for cross-platform communication:
  • POSIX: Unix domain socket via SSH_AUTH_SOCK environment variable
  • Windows: Named pipe via SSH_AUTH_SOCK or default \\.\pipe\openssh-ssh-agent
Idempotent: SSH agent protocol is idempotent - adding same key multiple times is safe. Key is not duplicated in agent.
Key path: key_path is only used for logging/reference in agent comment field. No temporary files are created.
Warning
Agent requirement: SSH agent must be running and accessible. Returns error if agent connection fails (agent not running, wrong path, etc.).
Key format: Only Ed25519 keys are supported. Other key types will return error.

Definition at line 185 of file ssh_agent.c.

185 {
186 if (private_key == NULL) {
187 return SET_ERRNO(ERROR_INVALID_PARAM, "Cannot add key to ssh-agent: private_key is NULL");
188 }
189 if (private_key->type != KEY_TYPE_ED25519) {
190 return SET_ERRNO(ERROR_INVALID_PARAM, "Cannot add key to ssh-agent: only Ed25519 keys supported");
191 }
192
193 log_info("Adding key to ssh-agent: %s", key_path ? key_path : "(memory)");
194
195 // Open the pipe/socket for this operation (works on both Windows and Unix)
196 pipe_t pipe = ssh_agent_open_pipe();
197 if (pipe == INVALID_PIPE_VALUE) {
198 return SET_ERRNO(ERROR_CRYPTO, "Failed to connect to ssh-agent");
199 }
200
201 // Build SSH agent protocol message: SSH2_AGENTC_ADD_IDENTITY (17)
202 // Message format:
203 // uint32: message length
204 // byte: SSH2_AGENTC_ADD_IDENTITY (17)
205 // string: key type ("ssh-ed25519")
206 // string: public key (32 bytes)
207 // string: private key (64 bytes)
208 // string: comment (key path or empty)
209
210 unsigned char buf[BUFFER_SIZE_XXLARGE];
211 size_t pos = 4; // Reserve space for length prefix
212
213 // Message type: SSH2_AGENTC_ADD_IDENTITY
214 buf[pos++] = 17;
215
216 // Key type: "ssh-ed25519" (11 bytes)
217 uint32_t len = 11;
218 write_u32_be(buf + pos, len);
219 pos += 4;
220 // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - Binary protocol, intentionally not null-terminated
221 memcpy(buf + pos, "ssh-ed25519", 11);
222 pos += 11;
223
224 // Public key (32 bytes) - last 32 bytes of the 64-byte ed25519 key
225 len = 32;
226 write_u32_be(buf + pos, len);
227 pos += 4;
228 memcpy(buf + pos, private_key->key.ed25519 + 32, 32); // Public key is second half
229 pos += 32;
230
231 // Private key (64 bytes - full ed25519 key: 32-byte seed + 32-byte public)
232 len = 64;
233 write_u32_be(buf + pos, len);
234 pos += 4;
235 memcpy(buf + pos, private_key->key.ed25519, 64);
236 pos += 64;
237
238 // Comment (key path)
239 len = key_path ? strlen(key_path) : 0;
240
241 // SECURITY: Validate key path length to prevent buffer overflow
242 // Buffer is BUFFER_SIZE_XXLARGE (4096), pos is ~128 at this point, need 4 bytes for length prefix
243 size_t max_key_path_len = sizeof(buf) - pos - 4;
244 if (len > max_key_path_len) {
246 sodium_memzero(buf, sizeof(buf));
247 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "SSH key path too long: %u bytes (max %zu)", len, max_key_path_len);
248 }
249
250 write_u32_be(buf + pos, len);
251 pos += 4;
252 if (len > 0) {
253 memcpy(buf + pos, key_path, len);
254 pos += len;
255 }
256
257 // Write message length at start (excluding the 4-byte length field itself)
258 uint32_t msg_len = pos - 4;
259 write_u32_be(buf, msg_len);
260
261 // Send message to agent
262 ssize_t bytes_written = platform_pipe_write(pipe, buf, pos);
263 if (bytes_written != (ssize_t)pos) {
265 sodium_memzero(buf, sizeof(buf));
266 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to write to ssh-agent pipe");
267 }
268
269 // Read response
270 unsigned char response[BUFFER_SIZE_SMALL];
271 ssize_t bytes_read = platform_pipe_read(pipe, response, sizeof(response));
272 if (bytes_read < 5) {
274 sodium_memzero(buf, sizeof(buf));
275 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to read from ssh-agent pipe");
276 }
277
278 // Done with the pipe - close it
280 sodium_memzero(buf, sizeof(buf));
281
282 // Check response: should be SSH_AGENT_SUCCESS (6)
283 // Response format: uint32 length, byte message_type
284 uint8_t response_type = response[4];
285 if (response_type == 6) {
286 log_info("Successfully added key to ssh-agent");
287 return ASCIICHAT_OK;
288 } else if (response_type == 5) {
289 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent rejected key (SSH_AGENT_FAILURE)");
290 } else {
291 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response: %d", response_type);
292 }
293}
#define BUFFER_SIZE_XXLARGE
Extra extra large buffer size (4096 bytes)
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
@ ERROR_BUFFER_OVERFLOW
Definition error_codes.h:98
int pipe_t
Pipe handle type (POSIX: int file descriptor)
Definition pipe.h:40

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, BUFFER_SIZE_XXLARGE, bytes_written, private_key_t::ed25519, ERROR_BUFFER_OVERFLOW, ERROR_CRYPTO, ERROR_INVALID_PARAM, INVALID_PIPE_VALUE, private_key_t::key, KEY_TYPE_ED25519, log_info, platform_pipe_close(), platform_pipe_read(), platform_pipe_write(), SET_ERRNO, SET_ERRNO_SYS, and private_key_t::type.

Referenced by parse_ssh_private_key().

◆ ssh_agent_get_key()

asciichat_error_t ssh_agent_get_key ( const public_key_t public_key,
private_key_t key_out 
)

#include <ssh_agent.h>

Retrieve a private key from ssh-agent by matching public key.

Parameters
public_keyPublic key to match (must not be NULL)
key_outOutput private key structure (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Retrieves private key from SSH agent by sending SSH2_AGENTC_SIGN_REQUEST. This doesn't actually retrieve the private key material - instead it proves the key exists in the agent by attempting a signature operation.

Note
Agent requirement: SSH agent must be running and accessible. Returns error if agent is not available.
Key format: Only Ed25519 keys are supported.
Security: Private key never leaves ssh-agent. This function only verifies the key exists by matching the public key.

◆ ssh_agent_has_key()

bool ssh_agent_has_key ( const public_key_t public_key)

#include <ssh_agent.h>

Check if a public key is already in ssh-agent.

Parameters
public_keyPublic key to check (must not be NULL)
Returns
true if key is in agent, false otherwise

Checks if public key is already in SSH agent by listing agent keys using SSH agent protocol.

Note
Agent requirement: SSH agent must be running and accessible. Returns false if agent is not available.
Key listing: Uses SSH agent protocol directly via lib/platform/pipe.h abstraction:
  • Connects to agent via Unix domain socket (POSIX) or named pipe (Windows)
  • Sends SSH2_AGENTC_REQUEST_IDENTITIES (11) message
  • Receives SSH2_AGENT_IDENTITIES_ANSWER (12) response with key list
  • Parses response to find matching Ed25519 public key (32-byte comparison)
Key matching: Compares raw Ed25519 public keys (32 bytes) directly. No fingerprint computation required - direct byte comparison.
Platform abstraction: Uses lib/platform/pipe.h for cross-platform communication:
  • POSIX: Unix domain socket via SSH_AUTH_SOCK environment variable
  • Windows: Named pipe via SSH_AUTH_SOCK or default \\.\pipe\openssh-ssh-agent
Warning
Agent requirement: SSH agent must be running and accessible. Returns false if agent connection fails (agent not running, wrong path, etc.).

Definition at line 90 of file ssh_agent.c.

90 {
91 if (public_key == NULL) {
92 log_warn("NULL is not a valid public key");
93 return false;
94 }
95
96 // Use SSH agent protocol to list keys (works on both Windows and Unix)
97 pipe_t pipe = ssh_agent_open_pipe();
98 if (pipe == INVALID_PIPE_VALUE) {
99 return false;
100 }
101
102 // Build SSH2_AGENTC_REQUEST_IDENTITIES message (type 11)
103 unsigned char request[5];
104 request[0] = 0; // length: 1 (4-byte big-endian)
105 request[1] = 0;
106 request[2] = 0;
107 request[3] = 1;
108 request[4] = 11; // SSH2_AGENTC_REQUEST_IDENTITIES
109
110 // Send request
111 ssize_t bytes_written = platform_pipe_write(pipe, request, 5);
112 if (bytes_written != 5) {
114 return false;
115 }
116
117 // Read response
118 unsigned char response[BUFFER_SIZE_XXXLARGE];
119 ssize_t bytes_read = platform_pipe_read(pipe, response, sizeof(response));
120 if (bytes_read < 9) {
122 return false;
123 }
124
126
127 // Parse response: type should be SSH2_AGENT_IDENTITIES_ANSWER (12)
128 uint8_t resp_type = response[4];
129 if (resp_type != 12) {
130 return false;
131 }
132
133 // Number of keys at bytes 5-8
134 uint32_t num_keys = read_u32_be(response + 5);
135
136 // Parse keys and check if our public key matches
137 size_t pos = 9;
138 for (uint32_t i = 0; i < num_keys && pos + 4 < (size_t)bytes_read; i++) {
139 // Read key blob length
140 uint32_t blob_len = read_u32_be(response + pos);
141 pos += 4;
142
143 if (pos + blob_len > (size_t)bytes_read)
144 break;
145
146 // Parse the blob to extract the Ed25519 public key
147 size_t blob_pos = pos;
148 // Skip key type string
149 if (blob_pos + 4 > pos + blob_len) {
150 pos += blob_len;
151 continue;
152 }
153 uint32_t type_len = read_u32_be(response + blob_pos);
154 blob_pos += 4 + type_len;
155
156 // Read public key data
157 if (blob_pos + 4 > pos + blob_len) {
158 pos += blob_len;
159 continue;
160 }
161 uint32_t pubkey_len = read_u32_be(response + blob_pos);
162 blob_pos += 4;
163
164 // Compare public key (should be 32 bytes for Ed25519)
165 // Use constant-time comparison to prevent timing side channels
166 if (pubkey_len == 32 && blob_pos + 32 <= pos + blob_len) {
167 if (sodium_memcmp(response + blob_pos, public_key->key, 32) == 0) {
168 log_debug("Found matching key in ssh-agent");
169 return true;
170 }
171 }
172
173 pos += blob_len;
174
175 // Skip comment string length + comment
176 if (pos + 4 > (size_t)bytes_read)
177 break;
178 uint32_t comment_len = read_u32_be(response + pos);
179 pos += 4 + comment_len;
180 }
181
182 return false;
183}

References BUFFER_SIZE_XXXLARGE, bytes_written, INVALID_PIPE_VALUE, public_key_t::key, log_debug, log_warn, platform_pipe_close(), platform_pipe_read(), and platform_pipe_write().

Referenced by parse_ssh_private_key().

◆ ssh_agent_is_available()

bool ssh_agent_is_available ( void  )

#include <ssh_agent.h>

Check if ssh-agent is running and available.

Returns
true if ssh-agent is available, false otherwise

Checks if SSH agent is running and accessible by verifying SSH_AUTH_SOCK environment variable.

Note
Agent detection:
  • Checks SSH_AUTH_SOCK environment variable is set
  • On Unix: Verifies socket exists and is accessible (access() with W_OK)
  • On Windows: Only checks environment variable (named pipe accessibility checked at connection time)
Platform differences:
  • Unix: Uses Unix domain socket (AF_UNIX) - can verify socket exists
  • Windows: Uses named pipe - can't use access() on named pipes
Agent location:
  • Unix: SSH_AUTH_SOCK points to Unix domain socket (e.g., /tmp/ssh-XXXXXXXX/agent.XXXXXX)
  • Windows: SSH_AUTH_SOCK points to named pipe (e.g., \\.\pipe\openssh-ssh-agent)
Warning
Agent may not be running: Function checks environment variable but doesn't verify agent is actually running. Connection may fail later if agent is not running.
Windows limitation: Cannot verify named pipe accessibility without attempting connection. Function may return true even if agent is not running (connection will fail later).

Definition at line 52 of file ssh_agent.c.

52 {
53 // Check if SSH_AUTH_SOCK environment variable is set
54 const char *auth_sock = SAFE_GETENV("SSH_AUTH_SOCK");
55
56#ifdef _WIN32
57 // On Windows, if SSH_AUTH_SOCK is not set, try to open the Windows named pipe to check availability
58 if (!auth_sock || strlen(auth_sock) == 0) {
59 pipe_t pipe = ssh_agent_open_pipe();
60 if (pipe != INVALID_PIPE_VALUE) {
61 platform_pipe_close(pipe); // Close immediately after checking
62 log_debug("ssh-agent is available via Windows named pipe (SSH_AUTH_SOCK not set)");
63 return true;
64 } else {
65 log_debug("ssh-agent not available: SSH_AUTH_SOCK not set and Windows named pipe not accessible");
66 return false;
67 }
68 }
69
70 // SSH_AUTH_SOCK is set on Windows
71 log_debug("ssh-agent appears available (SSH_AUTH_SOCK=%s)", auth_sock);
72 return true;
73#else
74 // Unix: SSH_AUTH_SOCK is required
75 if (!auth_sock || strlen(auth_sock) == 0) {
76 log_debug("ssh-agent not available: SSH_AUTH_SOCK not set");
77 return false;
78 }
79
80 // Check if Unix socket exists and is accessible
81 if (access(auth_sock, W_OK) != 0) {
82 log_debug("ssh-agent not available: cannot access socket at %s", auth_sock);
83 return false;
84 }
85 log_debug("ssh-agent is available at %s", auth_sock);
86 return true;
87#endif
88}

References INVALID_PIPE_VALUE, log_debug, platform_pipe_close(), and SAFE_GETENV.

◆ ssh_agent_sign()

asciichat_error_t ssh_agent_sign ( const public_key_t public_key,
const uint8_t message,
size_t  message_len,
uint8_t  signature[64] 
)

#include <ssh_agent.h>

Sign data using SSH agent with the specified public key.

Parameters
public_keyPublic key to use for signing (must not be NULL)
messageData to sign (must not be NULL)
message_lenLength of data to sign
signatureOutput buffer for signature (must be 64 bytes for Ed25519)
Returns
ASCIICHAT_OK on success, error code on failure

Signs message data using SSH agent protocol SSH2_AGENTC_SIGN_REQUEST (message type 13).

Note
Agent requirement: SSH agent must be running and accessible, and must have the private key corresponding to public_key.
Only Ed25519 signatures are supported (64 bytes).
The public key must already be in the ssh-agent (check with ssh_agent_has_key first).

Definition at line 295 of file ssh_agent.c.

296 {
297 if (!public_key || !message || !signature) {
298 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: public_key=%p, message=%p, signature=%p", public_key,
299 message, signature);
300 }
301
302 if (public_key->type != KEY_TYPE_ED25519) {
303 return SET_ERRNO(ERROR_CRYPTO_KEY, "Only Ed25519 keys are supported for SSH agent signing");
304 }
305
306 // Connect to SSH agent
307 pipe_t pipe = ssh_agent_open_pipe();
308 if (pipe == INVALID_PIPE_VALUE) {
309 return SET_ERRNO(ERROR_CRYPTO, "Cannot connect to ssh-agent");
310 }
311
312 // Build SSH2_AGENTC_SIGN_REQUEST message (type 13)
313 // Format: uint32 length, byte type, string key_blob, string data, uint32 flags
314 // For Ed25519, key_blob is: string "ssh-ed25519", string public_key(32 bytes)
315
316 const char *key_type = "ssh-ed25519";
317 uint32_t key_type_len = (uint32_t)strlen(key_type);
318
319 // Calculate total message length
320 // 1 (type) + 4 (key_blob_len) + key_blob_size + 4 (data_len) + data_size + 4 (flags)
321 uint32_t key_blob_size = 4 + key_type_len + 4 + 32; // string(key_type) + string(pubkey)
322 uint32_t total_len = 1 + 4 + key_blob_size + 4 + message_len + 4;
323
324 uint8_t *buf = SAFE_MALLOC(total_len + 4, uint8_t *); // +4 for length prefix
325 if (!buf) {
327 return SET_ERRNO(ERROR_CRYPTO, "Out of memory for SSH agent sign request");
328 }
329
330 uint32_t offset = 0;
331
332 // Write total message length (excluding this 4-byte length field)
333 write_u32_be(buf + offset, total_len);
334 offset += 4;
335
336 // Write message type (13 = SSH2_AGENTC_SIGN_REQUEST)
337 buf[offset++] = 13;
338
339 // Write key_blob length
340 write_u32_be(buf + offset, key_blob_size);
341 offset += 4;
342
343 // Write key_blob: string(key_type)
344 write_u32_be(buf + offset, key_type_len);
345 offset += 4;
346 memcpy(buf + offset, key_type, key_type_len);
347 offset += key_type_len;
348
349 // Write key_blob: string(public_key)
350 write_u32_be(buf + offset, 32);
351 offset += 4;
352 memcpy(buf + offset, public_key->key, 32);
353 offset += 32;
354
355 // Write data to sign
356 write_u32_be(buf + offset, (uint32_t)message_len);
357 offset += 4;
358 memcpy(buf + offset, message, message_len);
359 offset += (uint32_t)message_len;
360
361 // Write flags (0 = default)
362 write_u32_be(buf + offset, 0);
363 offset += 4;
364
365 // Send request
366 ssize_t written = platform_pipe_write(pipe, buf, total_len + 4);
367 sodium_memzero(buf, total_len + 4);
368 SAFE_FREE(buf);
369
370 if (written < 0 || (size_t)written != total_len + 4) {
372 return SET_ERRNO(ERROR_CRYPTO, "Failed to write SSH agent sign request");
373 }
374
375 // Read response
377 ssize_t read_bytes = platform_pipe_read(pipe, response, sizeof(response));
379
380 if (read_bytes < 5) {
381 return SET_ERRNO(ERROR_CRYPTO, "Failed to read SSH agent sign response (read %zd bytes)", read_bytes);
382 }
383
384 // Response format: uint32 length, byte type, data...
385 // We validate length implicitly by checking read_bytes and parsing the full response
386 (void)read_u32_be(response); // Read but don't need explicit length check
387 uint8_t response_type = response[4];
388
389 // Check for SSH2_AGENT_SIGN_RESPONSE (14)
390 if (response_type != 14) {
391 if (response_type == 5) {
392 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent refused to sign (SSH_AGENT_FAILURE)");
393 }
394 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response type: %d (expected 14)", response_type);
395 }
396
397 // Parse signature blob
398 // Response format: uint32 len, byte type(14), string signature_blob
399 if (read_bytes < 9) {
400 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response too short (no signature blob length)");
401 }
402
403 uint32_t sig_blob_len = read_u32_be(response + 5);
404 uint32_t expected_total = 4 + 1 + 4 + sig_blob_len;
405
406 if ((size_t)read_bytes < expected_total) {
407 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response truncated (expected %u bytes, got %zd)", expected_total,
408 read_bytes);
409 }
410
411 // Signature blob format for Ed25519: string "ssh-ed25519", string signature(64 bytes)
412 uint32_t offset_sig = 9;
413 uint32_t sig_type_len = read_u32_be(response + offset_sig);
414 offset_sig += 4;
415
416 if (offset_sig + sig_type_len + 4 > (uint32_t)read_bytes) {
417 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature type");
418 }
419
420 // Verify signature type is "ssh-ed25519"
421 if (sig_type_len != 11 || memcmp(response + offset_sig, "ssh-ed25519", 11) != 0) {
422 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned non-Ed25519 signature");
423 }
424 offset_sig += sig_type_len;
425
426 // Read signature bytes
427 uint32_t sig_len = read_u32_be(response + offset_sig);
428 offset_sig += 4;
429
430 if (sig_len != 64) {
431 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned invalid Ed25519 signature length: %u (expected 64)", sig_len);
432 }
433
434 if (offset_sig + 64 > (uint32_t)read_bytes) {
435 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature bytes");
436 }
437
438 // Copy signature to output
439 memcpy(signature, response + offset_sig, 64);
440
441 log_debug("SSH agent successfully signed %zu bytes with Ed25519 key", message_len);
442 return ASCIICHAT_OK;
443}
@ ERROR_CRYPTO_KEY
Definition error_codes.h:89

References ASCIICHAT_OK, BUFFER_SIZE_XXLARGE, ERROR_CRYPTO, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, INVALID_PIPE_VALUE, public_key_t::key, KEY_TYPE_ED25519, log_debug, platform_pipe_close(), platform_pipe_read(), platform_pipe_write(), SAFE_FREE, SAFE_MALLOC, SET_ERRNO, and public_key_t::type.

Referenced by ed25519_sign_message().

Variable Documentation

◆ buf

br_x509_trust_anchor* anchor_list::buf

Array of trust anchors (dynamically allocated)

Definition at line 79 of file pem_utils.h.

Referenced by https_get().

◆ len

size_t anchor_list::len

Total capacity of array

Definition at line 81 of file pem_utils.h.

◆ ptr

size_t anchor_list::ptr

Current number of trust anchors

Definition at line 80 of file pem_utils.h.

Referenced by https_get().