ascii-chat 0.11.33
Video chat in your terminal
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  discovery_keys.c
 Discovery Server Public Key Trust Management Implementation.
 
file  agent.c
 GPG agent connection and communication implementation.
 
file  export.c
 GPG public key export implementation.
 
file  homedir.c
 Temporary GPG homedir management implementation.
 
file  openpgp.c
 OpenPGP (RFC 4880) packet format parser implementation.
 
file  signing.c
 GPG signing operations implementation.
 
file  verification.c
 GPG signature verification implementation.
 
file  known_hosts.c
 📜 SSH known_hosts file parser for host key verification and trust management
 
file  pem.c
 📄 PEM format encoding/decoding utilities for certificates and keys (adapted from BearSSL)
 
file  regex.c
 PCRE2-based regex patterns for cryptographic parsing.
 
file  discovery_keys.h
 Discovery Server Public Key Trust Management.
 
file  agent.h
 GPG agent connection and communication interface.
 
file  export.h
 GPG public key export interface.
 
file  gpg.h
 GPG operations - main header.
 
file  homedir.h
 Temporary GPG homedir management for isolated key operations.
 
file  openpgp.h
 OpenPGP (RFC 4880) packet format parser.
 
file  signing.h
 GPG message signing interface.
 
file  verification.h
 GPG signature verification interface.
 
file  known_hosts.h
 Known hosts management for MITM attack prevention.
 
file  pem.h
 BearSSL PEM and trust anchor utilities adapted for in-memory data.
 
file  regex.h
 PCRE2-based regex patterns for cryptographic parsing.
 

Data Structures

struct  crypto_context_t
 Cryptographic context structure. More...
 
struct  openpgp_packet_header_t
 OpenPGP packet header information. More...
 
struct  openpgp_public_key_t
 OpenPGP public key packet data. More...
 
struct  openpgp_secret_key_t
 OpenPGP secret key packet data. 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
 

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.
 

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_destroy (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   (3LL * NS_PER_SEC_INT)
 Minimum time interval between rekey requests (3 seconds in nanoseconds for testing, 60 for production)
 
#define REKEY_DEFAULT_TIME_THRESHOLD   (3600LL * NS_PER_SEC_INT)
 Default rekey time threshold (1 hour in nanoseconds)
 
#define REKEY_DEFAULT_PACKET_THRESHOLD   1000000
 Default rekey packet threshold (1 million packets)
 
#define REKEY_TEST_TIME_THRESHOLD   (30LL * NS_PER_SEC_INT)
 Test mode rekey time threshold (30 seconds in nanoseconds)
 
#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   (60LL * NS_PER_SEC_INT)
 Minimum interval between rekey requests (60 seconds in nanoseconds, 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.
 

PGP Armored Format Parsing

asciichat_error_t openpgp_parse_armored_pubkey (const char *armored_text, uint8_t ed25519_pk[32])
 Parse PGP armored key block and extract Ed25519 public key.
 
asciichat_error_t openpgp_parse_armored_seckey (const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
 Parse PGP armored secret key block and extract Ed25519 keypair.
 

OpenPGP Packet Parsing

asciichat_error_t openpgp_parse_packet_header (const uint8_t *data, size_t data_len, openpgp_packet_header_t *header)
 Parse OpenPGP packet header.
 
asciichat_error_t openpgp_parse_public_key_packet (const uint8_t *packet_body, size_t body_len, openpgp_public_key_t *pubkey)
 Parse OpenPGP Public Key Packet (tag 6)
 
asciichat_error_t openpgp_parse_secret_key_packet (const uint8_t *packet_body, size_t body_len, openpgp_secret_key_t *seckey)
 Parse OpenPGP Secret Key Packet (tag 5)
 
asciichat_error_t openpgp_extract_ed25519_from_mpi (const uint8_t *mpi, size_t mpi_len, uint8_t ed25519_pk[32])
 Extract Ed25519 public key from MPI-encoded data.
 

Base64 Decoding for PGP Armor

asciichat_error_t openpgp_base64_decode (const char *base64, size_t base64_len, uint8_t **binary_out, size_t *binary_len)
 Decode PGP armored base64 data.
 

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 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_destroy (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")
 

OpenPGP Constants

#define OPENPGP_TAG_PUBLIC_KEY   6
 OpenPGP packet tag for Public Key Packet.
 
#define OPENPGP_TAG_SECRET_KEY   5
 OpenPGP packet tag for Secret Key Packet.
 
#define OPENPGP_TAG_USER_ID   13
 OpenPGP packet tag for User ID Packet.
 
#define OPENPGP_TAG_SIGNATURE   2
 OpenPGP packet tag for Signature Packet.
 
#define OPENPGP_ALGO_EDDSA   22
 OpenPGP algorithm ID for EdDSA (Ed25519)
 
#define OPENPGP_ALGO_ECDH   18
 OpenPGP algorithm ID for ECDH (Curve25519)
 

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:

Implements parsing of OpenPGP packet format (RFC 4880) for extracting Ed25519 public keys from PGP armored key blocks.

Supported features:

Note
Limitations:
  • Only supports Ed25519 keys (algorithm 22)
  • Only parses public key packets (tag 6)
  • Does not verify signatures or checksums
  • Does not support encrypted keys
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

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

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 113 of file include/ascii-chat/crypto/crypto.h.

◆ AES256_KEY_SIZE

#define AES256_KEY_SIZE   32

#include <crypto.h>

AES-256 key size in bytes.

Definition at line 109 of file include/ascii-chat/crypto/crypto.h.

◆ AES_IV_SIZE

#define AES_IV_SIZE   16

#include <crypto.h>

AES initialization vector (IV) size in bytes.

Definition at line 111 of file include/ascii-chat/crypto/crypto.h.

◆ ANCHOR_LIST_INIT

#define ANCHOR_LIST_INIT   {NULL, 0, 0}

#include <pem.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.h.

◆ ARGON2ID_SALT_SIZE

#define ARGON2ID_SALT_SIZE   32

#include <crypto.h>

Argon2id salt size in bytes.

Definition at line 105 of file include/ascii-chat/crypto/crypto.h.

◆ AUTH_CHALLENGE_FLAGS_SIZE

#define AUTH_CHALLENGE_FLAGS_SIZE   1

#include <crypto.h>

Authentication flags size (1 byte)

Definition at line 175 of file include/ascii-chat/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 177 of file include/ascii-chat/crypto/crypto.h.

◆ AUTH_CHALLENGE_SIZE

#define AUTH_CHALLENGE_SIZE   32

#include <crypto.h>

Challenge nonce size (32 bytes)

Definition at line 159 of file include/ascii-chat/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 161 of file include/ascii-chat/crypto/crypto.h.

◆ AUTH_HMAC_SIZE

#define AUTH_HMAC_SIZE   32

#include <crypto.h>

HMAC size in authentication packets (32 bytes)

Definition at line 157 of file include/ascii-chat/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 187 of file include/ascii-chat/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 189 of file include/ascii-chat/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 165 of file include/ascii-chat/crypto/crypto.h.

◆ AUTH_SIGNATURE_SIZE

#define AUTH_SIGNATURE_SIZE   64

#include <crypto.h>

Ed25519 signature size (64 bytes)

Definition at line 163 of file include/ascii-chat/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 135 of file include/ascii-chat/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 133 of file include/ascii-chat/crypto/crypto.h.

◆ CRYPTO_ED25519_SIGNATURE_SIZE

#define CRYPTO_ED25519_SIGNATURE_SIZE   ED25519_SIGNATURE_SIZE

#include <crypto.h>

Ed25519 signature size.

Definition at line 137 of file include/ascii-chat/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 143 of file include/ascii-chat/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 382 of file include/ascii-chat/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 398 of file include/ascii-chat/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 400 of file include/ascii-chat/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 394 of file include/ascii-chat/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 396 of file include/ascii-chat/crypto/crypto.h.

◆ CRYPTO_HMAC_SIZE

#define CRYPTO_HMAC_SIZE   HMAC_SHA256_SIZE

#include <crypto.h>

HMAC size (HMAC-SHA256)

Definition at line 147 of file include/ascii-chat/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 373 of file include/ascii-chat/crypto/crypto.h.

◆ CRYPTO_MAC_SIZE

#define CRYPTO_MAC_SIZE   POLY1305_MAC_SIZE

#include <crypto.h>

MAC size (Poly1305)

Definition at line 145 of file include/ascii-chat/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 232 of file include/ascii-chat/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 230 of file include/ascii-chat/crypto/crypto.h.

◆ CRYPTO_NONCE_SIZE

#define CRYPTO_NONCE_SIZE   XSALSA20_NONCE_SIZE

#include <crypto.h>

Nonce size (XSalsa20)

Definition at line 139 of file include/ascii-chat/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 129 of file include/ascii-chat/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 127 of file include/ascii-chat/crypto/crypto.h.

◆ CRYPTO_SALT_SIZE

#define CRYPTO_SALT_SIZE   ARGON2ID_SALT_SIZE

#include <crypto.h>

Salt size (Argon2id)

Definition at line 141 of file include/ascii-chat/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 131 of file include/ascii-chat/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 95 of file include/ascii-chat/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 93 of file include/ascii-chat/crypto/crypto.h.

◆ ED25519_SIGNATURE_SIZE

#define ED25519_SIGNATURE_SIZE   64

#include <crypto.h>

Ed25519 signature size in bytes.

Definition at line 97 of file include/ascii-chat/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 214 of file include/ascii-chat/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 216 of file include/ascii-chat/crypto/crypto.h.

◆ HMAC_SHA256_SIZE

#define HMAC_SHA256_SIZE   32

#include <crypto.h>

HMAC-SHA256 output size in bytes.

Definition at line 103 of file include/ascii-chat/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 202 of file include/ascii-chat/crypto/crypto.h.

◆ MAX_COMMENT_LEN

#define MAX_COMMENT_LEN   256

#include <crypto.h>

Definition at line 435 of file include/ascii-chat/crypto/crypto.h.

◆ MAX_ENCRYPTED_PACKET_SIZE

#define MAX_ENCRYPTED_PACKET_SIZE   65536

#include <crypto.h>

Maximum encrypted packet size (64KB)

Definition at line 204 of file include/ascii-chat/crypto/crypto.h.

◆ MAX_GPG_KEYGRIP_LEN

#define MAX_GPG_KEYGRIP_LEN   64

#include <crypto.h>

Definition at line 436 of file include/ascii-chat/crypto/crypto.h.

◆ MAX_PASSWORD_LENGTH

#define MAX_PASSWORD_LENGTH   256

#include <crypto.h>

Maximum password length (256 characters)

Definition at line 81 of file include/ascii-chat/crypto/crypto.h.

◆ MIN_PASSWORD_LENGTH

#define MIN_PASSWORD_LENGTH   8

#include <crypto.h>

Minimum password length (8 characters)

Definition at line 79 of file include/ascii-chat/crypto/crypto.h.

◆ NO_IDENTITY_MARKER

#define NO_IDENTITY_MARKER   "no-identity"

#include <crypto.h>

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

Definition at line 418 of file include/ascii-chat/crypto/crypto.h.

◆ OPENPGP_ALGO_ECDH

#define OPENPGP_ALGO_ECDH   18

#include <openpgp.h>

OpenPGP algorithm ID for ECDH (Curve25519)

Definition at line 56 of file openpgp.h.

◆ OPENPGP_ALGO_EDDSA

#define OPENPGP_ALGO_EDDSA   22

#include <openpgp.h>

OpenPGP algorithm ID for EdDSA (Ed25519)

Definition at line 53 of file openpgp.h.

◆ OPENPGP_TAG_PUBLIC_KEY

#define OPENPGP_TAG_PUBLIC_KEY   6

#include <openpgp.h>

OpenPGP packet tag for Public Key Packet.

Definition at line 41 of file openpgp.h.

◆ OPENPGP_TAG_SECRET_KEY

#define OPENPGP_TAG_SECRET_KEY   5

#include <openpgp.h>

OpenPGP packet tag for Secret Key Packet.

Definition at line 44 of file openpgp.h.

◆ OPENPGP_TAG_SIGNATURE

#define OPENPGP_TAG_SIGNATURE   2

#include <openpgp.h>

OpenPGP packet tag for Signature Packet.

Definition at line 50 of file openpgp.h.

◆ OPENPGP_TAG_USER_ID

#define OPENPGP_TAG_USER_ID   13

#include <openpgp.h>

OpenPGP packet tag for User ID Packet.

Definition at line 47 of file openpgp.h.

◆ PASSWORD_BUFFER_SIZE

#define PASSWORD_BUFFER_SIZE   256

#include <crypto.h>

Password input buffer size (256 bytes)

Definition at line 218 of file include/ascii-chat/crypto/crypto.h.

◆ POLY1305_MAC_SIZE

#define POLY1305_MAC_SIZE   16

#include <crypto.h>

Poly1305 MAC size in bytes.

Definition at line 101 of file include/ascii-chat/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 1244 of file include/ascii-chat/crypto/crypto.h.

◆ REKEY_DEFAULT_TIME_THRESHOLD

#define REKEY_DEFAULT_TIME_THRESHOLD   (3600LL * NS_PER_SEC_INT)

#include <crypto.h>

Default rekey time threshold (1 hour in nanoseconds)

Definition at line 1242 of file include/ascii-chat/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 1250 of file include/ascii-chat/crypto/crypto.h.

◆ REKEY_MIN_INTERVAL

#define REKEY_MIN_INTERVAL   (3LL * NS_PER_SEC_INT)

#include <crypto.h>

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

Definition at line 1240 of file include/ascii-chat/crypto/crypto.h.

◆ REKEY_MIN_REQUEST_INTERVAL

#define REKEY_MIN_REQUEST_INTERVAL   (60LL * NS_PER_SEC_INT)

#include <crypto.h>

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

Definition at line 1252 of file include/ascii-chat/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 1248 of file include/ascii-chat/crypto/crypto.h.

◆ REKEY_TEST_TIME_THRESHOLD

#define REKEY_TEST_TIME_THRESHOLD   (30LL * NS_PER_SEC_INT)

#include <crypto.h>

Test mode rekey time threshold (30 seconds in nanoseconds)

Definition at line 1246 of file include/ascii-chat/crypto/crypto.h.

◆ SECRETBOX_KEY_SIZE

#define SECRETBOX_KEY_SIZE   32

#include <crypto.h>

Secretbox key size in bytes.

Definition at line 107 of file include/ascii-chat/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 194 of file include/ascii-chat/crypto/crypto.h.

◆ SESSION_ID_SIZE

#define SESSION_ID_SIZE   16

#include <crypto.h>

Session ID size in bytes.

Definition at line 115 of file include/ascii-chat/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 414 of file include/ascii-chat/crypto/crypto.h.

◆ SSH_KEY_HEADER_SIZE

◆ SSH_KEY_PERMISSIONS_MASK

#define SSH_KEY_PERMISSIONS_MASK   (S_IRWXG | S_IRWXO)

#include <crypto.h>

Definition at line 427 of file include/ascii-chat/crypto/crypto.h.

◆ SSH_KEY_PUBLIC_KEY_LENGTH_SIZE

#define SSH_KEY_PUBLIC_KEY_LENGTH_SIZE   4

#include <crypto.h>

Definition at line 357 of file include/ascii-chat/crypto/crypto.h.

◆ SSH_KEY_PUBLIC_KEY_SIZE

#define SSH_KEY_PUBLIC_KEY_SIZE   32

#include <crypto.h>

Definition at line 358 of file include/ascii-chat/crypto/crypto.h.

◆ SSH_KEY_RECOMMENDED_PERMISSIONS

#define SSH_KEY_RECOMMENDED_PERMISSIONS   0600

#include <crypto.h>

Definition at line 428 of file include/ascii-chat/crypto/crypto.h.

◆ SSH_KEY_TYPE_LENGTH_SIZE

#define SSH_KEY_TYPE_LENGTH_SIZE   4

#include <crypto.h>

Definition at line 355 of file include/ascii-chat/crypto/crypto.h.

◆ SSH_KEY_TYPE_STRING_SIZE

#define SSH_KEY_TYPE_STRING_SIZE   11

#include <crypto.h>

Definition at line 356 of file include/ascii-chat/crypto/crypto.h.

◆ X25519_KEY_SIZE

#define X25519_KEY_SIZE   32

#include <crypto.h>

X25519 key size in bytes.

Definition at line 91 of file include/ascii-chat/crypto/crypto.h.

◆ X25519_KEY_TYPE

#define X25519_KEY_TYPE   "x25519"

#include <crypto.h>

X25519 key type string ("x25519")

Definition at line 416 of file include/ascii-chat/crypto/crypto.h.

◆ XSALSA20_NONCE_SIZE

#define XSALSA20_NONCE_SIZE   24

#include <crypto.h>

XSalsa20 nonce size in bytes.

Definition at line 99 of file include/ascii-chat/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 220 of file include/ascii-chat/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 331 of file include/ascii-chat/crypto/crypto.h.

331 {
332 CRYPTO_OK = 0,
crypto_result_t
Cryptographic operation result codes.

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 381 of file known_hosts.c.

381 {
382 // Validate parameters first
383 if (!server_ip || !server_key) {
384 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
385 }
386
387 // Check for empty string (must be after NULL check)
388 size_t ip_len = strlen(server_ip);
389 if (ip_len == 0) {
390 return SET_ERRNO(ERROR_INVALID_PARAM, "Empty hostname/IP");
391 }
392
393 const char *path = get_known_hosts_path();
394 if (!path || path[0] == '\0') {
395 SET_ERRNO(ERROR_CONFIG, "Failed to get known hosts file path");
396 return ERROR_CONFIG;
397 }
398
399 // Create parent directories recursively (like mkdir -p)
400 size_t path_len = strlen(path);
401 if (path_len == 0) {
402 SET_ERRNO(ERROR_CONFIG, "Empty known hosts file path");
403 return ERROR_CONFIG;
404 }
405 char *dir = SAFE_MALLOC(path_len + 1, char *);
406 defer(SAFE_FREE(dir));
407 if (!dir) {
408 SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for directory path");
409 return ERROR_MEMORY;
410 }
411 memcpy(dir, path, path_len + 1);
412
413 // Find the last path separator
414 char *last_sep = strrchr(dir, PATH_DELIM);
415
416 if (last_sep) {
417 *last_sep = '\0'; // Truncate to get directory path
418 asciichat_error_t result = mkdir_recursive(dir);
419 if (result != ASCIICHAT_OK) {
420 return result; // Error already set by mkdir_recursive
421 }
422 }
423
424 // Create the file if it doesn't exist, then append to it
425 // Note: Temporarily removed log_debug to avoid potential crashes during debugging
426 // log_debug("KNOWN_HOSTS: Attempting to create/open file: %s", path);
427 // Use "a" mode for append-only (simpler and works better with chmod)
428 FILE *f = platform_fopen("file_stream", path, "a");
429 defer(SAFE_FCLOSE(f));
430 if (!f) {
431 // log_debug("KNOWN_HOSTS: platform_fopen failed: %s (errno=%d)", SAFE_STRERROR(errno), errno);
432 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to create/open known hosts file: %s", path);
433 }
434 // Set secure permissions (0600) - only owner can read/write
435 // Note: chmod may fail if file was just created by fopen, but that's okay
437 // log_debug("KNOWN_HOSTS: Successfully opened file: %s", path);
438
439 // Format IP:port with proper bracket notation for IPv6
440 char ip_with_port[BUFFER_SIZE_MEDIUM];
441 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
442 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
443 }
444
445 // Convert key to hex for storage
446 char hex[CRYPTO_HEX_KEY_SIZE_NULL] = {0}; // Initialize to zeros for safety
447 bool is_placeholder = true;
448 // Build hex string byte by byte to avoid buffer overflow issues
449 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
450 // Convert each byte to 2 hex digits directly
451 uint8_t byte = server_key[i];
452 hex[i * 2] = "0123456789abcdef"[byte >> 4]; // High nibble
453 hex[i * 2 + 1] = "0123456789abcdef"[byte & 0xf]; // Low nibble
454 if (byte != 0) {
455 is_placeholder = false;
456 }
457 }
458 hex[CRYPTO_HEX_KEY_SIZE] = '\0'; // Ensure null termination (64 hex digits + null terminator)
459
460 // Write to file and check for errors
461 int fprintf_result;
462 if (is_placeholder) {
463 // Server has no identity key - store as placeholder
464 fprintf_result = safe_fprintf(f, "%s %s 0000000000000000000000000000000000000000000000000000000000000000 %s\n",
466 } else {
467 // Server has identity key - store normally
468 fprintf_result = safe_fprintf(f, "%s %s %s %s\n", ip_with_port, X25519_KEY_TYPE, hex, ASCII_CHAT_APP_NAME);
469 }
470
471 // Check if fprintf failed
472 if (fprintf_result < 0) {
473 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to write to known_hosts file: %s", path);
474 }
475
476 // Flush to ensure data is written
477 if (fflush(f) != 0) {
478 return SET_ERRNO_SYS(ERROR_CONFIG, "CRITICAL SECURITY ERROR: Failed to flush known_hosts file: %s", path);
479 }
480
481 log_debug("KNOWN_HOSTS: Successfully added host to known_hosts file: %s", path);
482
483 return ASCIICHAT_OK;
484}
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes)
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
unsigned char uint8_t
Definition common.h:56
#define SAFE_FCLOSE(fp)
Definition common.h:386
#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:47
#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, returning the error code.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_INVALID_PARAM
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
int safe_fprintf(FILE *stream, const char *format,...)
Safe formatted output to file stream.
Definition system.c:172
#define PATH_DELIM
Platform-specific path separator character.
Definition filesystem.h:112
int platform_chmod(const char *pathname, int mode)
Change file permissions/mode.
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
#define FILE_PERM_PRIVATE
File permission: Private (owner read/write only)
Definition filesystem.h:187
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:221

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 78 of file known_hosts.c.

78 {
79 // Validate parameters first
80 if (!server_ip || !server_key) {
81 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: server_ip=%p, server_key=%p", server_ip, server_key);
82 }
83
84 // Format IP:port with proper bracket notation for IPv6
85 char ip_with_port[BUFFER_SIZE_MEDIUM];
86 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
87 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
88 }
89
90 // Add space after IP:port for prefix matching
91 char expected_prefix[BUFFER_SIZE_MEDIUM];
92 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
93
94 // Use platform abstraction to find all known_hosts files across standard locations
95 // Append semantics: search ALL files for matching entries (user + system)
96 config_file_list_t known_hosts_files = {0};
97 asciichat_error_t search_result = platform_find_config_file("known_hosts", &known_hosts_files);
98
99 if (search_result != ASCIICHAT_OK) {
100 // Platform search failed - non-fatal, treat as unknown host
101 log_debug("KNOWN_HOSTS: Failed to search for known_hosts files: %d", search_result);
102 return ASCIICHAT_OK;
103 }
104
105 // If no files found, this is an unknown host (first connection)
106 if (known_hosts_files.count == 0) {
107 return ASCIICHAT_OK;
108 }
109
110 // Search through ALL known_hosts files for matching entry (append semantics)
111 bool found_entries = false;
112 for (size_t file_idx = 0; file_idx < known_hosts_files.count; file_idx++) {
113 const char *path = known_hosts_files.files[file_idx].path;
114
115 int fd = platform_open("known_hosts_file", path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
116 if (fd < 0) {
117 log_debug("KNOWN_HOSTS: Cannot open file: %s", path);
118 continue; // Skip files that can't be opened
119 }
120
121 FILE *f = platform_fdopen("known_hosts_stream", fd, "r");
122 if (!f) {
123 platform_close(fd);
124 log_debug("KNOWN_HOSTS: Failed to fdopen: %s", path);
125 continue;
126 }
127
128 char line[BUFFER_SIZE_XLARGE];
129
130 // Search this file for matching IP:port entries
131 while (fgets(line, sizeof(line), f)) {
132 if (line[0] == '#')
133 continue; // Comment
134
135 // Use regex to parse and extract known_hosts line components
136 char *parsed_ip_port = NULL, *parsed_key_type = NULL, *parsed_hex_key = NULL, *parsed_comment = NULL;
137 if (!crypto_regex_match_known_hosts(line, &parsed_ip_port, &parsed_key_type, &parsed_hex_key, &parsed_comment)) {
138 continue; // Line doesn't match known_hosts format
139 }
140
141 // Check if IP:port matches what we're looking for (with IPv6 normalization)
142 if (!compare_ip_port_strings(parsed_ip_port, ip_with_port)) {
143 SAFE_FREE(parsed_ip_port);
144 SAFE_FREE(parsed_key_type);
145 SAFE_FREE(parsed_hex_key);
146 SAFE_FREE(parsed_comment);
147 continue; // Different IP:port
148 }
149
150 found_entries = true;
151
152 if (strcmp(parsed_key_type, NO_IDENTITY_MARKER) == 0) {
153 // No-identity entry but server has identity - continue searching
154 log_debug("SECURITY_DEBUG: Found no-identity entry, but server has identity key");
155 SAFE_FREE(parsed_ip_port);
156 SAFE_FREE(parsed_key_type);
157 SAFE_FREE(parsed_hex_key);
158 SAFE_FREE(parsed_comment);
159 continue;
160 }
161
162 // Parse identity key from hex string
163 if (!parsed_hex_key) {
164 // No hex key found - invalid entry
165 SAFE_FREE(parsed_ip_port);
166 SAFE_FREE(parsed_key_type);
167 SAFE_FREE(parsed_comment);
168 continue;
169 }
170
171 public_key_t stored_key;
172 if (parse_public_key(parsed_hex_key, &stored_key) != 0) {
173 SAFE_FREE(parsed_ip_port);
174 SAFE_FREE(parsed_key_type);
175 SAFE_FREE(parsed_hex_key);
176 SAFE_FREE(parsed_comment);
177 continue; // Invalid key format
178 }
179
180 // Check if server key is all zeros (no-identity server)
181 bool server_key_is_zero = true;
182 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
183 if (server_key[i] != 0) {
184 server_key_is_zero = false;
185 break;
186 }
187 }
188
189 // Check if stored key is all zeros
190 bool stored_key_is_zero = true;
191 for (int i = 0; i < ED25519_PUBLIC_KEY_SIZE; i++) {
192 if (stored_key.key[i] != 0) {
193 stored_key_is_zero = false;
194 break;
195 }
196 }
197
198 // Both zero = no-identity connection (weaker security)
199 if (server_key_is_zero && stored_key_is_zero) {
200 log_warn("SECURITY: Connecting to no-identity server at known IP:port");
201 SAFE_FREE(parsed_ip_port);
202 SAFE_FREE(parsed_key_type);
203 SAFE_FREE(parsed_hex_key);
204 SAFE_FREE(parsed_comment);
205 fclose(f);
206 config_file_list_destroy(&known_hosts_files);
207 return 1; // Match found
208 }
209
210 // Compare keys (constant-time)
211 if (sodium_memcmp(server_key, stored_key.key, ED25519_PUBLIC_KEY_SIZE) == 0) {
212 log_info("SECURITY: Server key matches known_hosts - connection verified");
213 SAFE_FREE(parsed_ip_port);
214 SAFE_FREE(parsed_key_type);
215 SAFE_FREE(parsed_hex_key);
216 SAFE_FREE(parsed_comment);
217 fclose(f);
218 config_file_list_destroy(&known_hosts_files);
219 return 1; // Match found!
220 }
221
222 // Key mismatch - free and continue searching
223 SAFE_FREE(parsed_ip_port);
224 SAFE_FREE(parsed_key_type);
225 SAFE_FREE(parsed_hex_key);
226 SAFE_FREE(parsed_comment);
227 }
228
229 fclose(f);
230 }
231
232 config_file_list_destroy(&known_hosts_files);
233
234 // Check if we found any entries at all
235 if (found_entries) {
236 // We found entries for this IP:port but none matched the server key
237 // This is a MITM warning!
238 log_error("SECURITY: Server key does NOT match any known_hosts entries!");
239 log_error("SECURITY: This indicates a possible man-in-the-middle attack!");
241 }
242
243 // No entries found for this IP:port - first connection
244 return ASCIICHAT_OK;
245}
#define BUFFER_SIZE_XLARGE
Extra large buffer size (2048 bytes)
@ ERROR_CRYPTO_VERIFICATION
uint8_t key[32]
Definition key_types.h:71
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:28
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
Find config file across multiple standard locations.
FILE * platform_fdopen(const char *name, int fd, const char *mode)
Convert file descriptor to stream (fdopen replacement)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int platform_open(const char *name, const char *pathname, int flags,...)
Safe file open (open replacement)
int platform_close(int fd)
Safe file close (close replacement)
void config_file_list_destroy(config_file_list_t *list)
Free config file list resources.
#define PLATFORM_O_RDONLY
Definition filesystem.h:82
int compare_ip_port_strings(const char *ip_port1, const char *ip_port2)
Compare two "IP:port" strings with IPv6 normalization.
Definition ip.c:1133
bool crypto_regex_match_known_hosts(const char *line, char **ip_port_out, char **key_type_out, char **hex_key_out, char **comment_out)
Definition regex.c:136
List of config file search results.
Definition filesystem.h:595
size_t count
Number of results found.
Definition filesystem.h:597
config_file_result_t * files
Array of results (allocated, must be freed)
Definition filesystem.h:596
char * path
Absolute path to config file (allocated, must be freed)
Definition filesystem.h:583
Public key structure.
Definition key_types.h:69

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, compare_ip_port_strings(), config_file_list_destroy(), config_file_list_t::count, crypto_regex_match_known_hosts(), ED25519_PUBLIC_KEY_SIZE, ERROR_CRYPTO_VERIFICATION, ERROR_INVALID_PARAM, FILE_PERM_PRIVATE, config_file_list_t::files, format_ip_with_port(), public_key_t::key, log_debug, log_error, log_info, log_warn, NO_IDENTITY_MARKER, parse_public_key(), config_file_result_t::path, platform_close(), platform_fdopen(), platform_find_config_file(), PLATFORM_O_RDONLY, platform_open(), SAFE_FREE, 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 251 of file known_hosts.c.

251 {
252 const char *path = get_known_hosts_path();
253 int fd = platform_open("known_hosts_file", path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
254 if (fd < 0) {
255 // File doesn't exist - this is an unknown host that needs verification
256 log_warn("Known hosts file does not exist: %s", path);
257 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
258 }
259
260 FILE *f = platform_fdopen("known_hosts_stream", fd, "r");
261 defer(SAFE_FCLOSE(f));
262 if (!f) {
263 // Failed to open file descriptor as FILE*
264 platform_close(fd);
265 log_warn("Failed to open known hosts file: %s", path);
266 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
267 }
268
269 char line[BUFFER_SIZE_XLARGE];
270 char expected_prefix[BUFFER_SIZE_MEDIUM];
271
272 // Format IP:port with proper bracket notation for IPv6
273 char ip_with_port[BUFFER_SIZE_MEDIUM];
274 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
275
276 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
277 }
278
279 // Add space after IP:port for prefix matching
280 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
281
282 while (fgets(line, sizeof(line), f)) {
283 if (line[0] == '#')
284 continue; // Comment
285
286 // Use regex to parse and extract known_hosts line components
287 char *parsed_ip_port = NULL, *parsed_key_type = NULL, *parsed_hex_key = NULL, *parsed_comment = NULL;
288 if (!crypto_regex_match_known_hosts(line, &parsed_ip_port, &parsed_key_type, &parsed_hex_key, &parsed_comment)) {
289 continue; // Line doesn't match known_hosts format
290 }
291
292 // Check if IP:port matches what we're looking for (with IPv6 normalization)
293 if (!compare_ip_port_strings(parsed_ip_port, ip_with_port)) {
294 SAFE_FREE(parsed_ip_port);
295 SAFE_FREE(parsed_key_type);
296 SAFE_FREE(parsed_hex_key);
297 SAFE_FREE(parsed_comment);
298 continue; // Different IP:port
299 }
300
301 // Found matching IP:port - check key type
302 if (strcmp(parsed_key_type, "no-identity") == 0) {
303 // This is a server without identity key that was previously accepted by the user
304 // No warnings or user confirmation needed - user already accepted this server
305 SAFE_FREE(parsed_ip_port);
306 SAFE_FREE(parsed_key_type);
307 SAFE_FREE(parsed_hex_key);
308 SAFE_FREE(parsed_comment);
309 return 1; // Known host (no-identity entry) - secure connection
310 }
311
312 // If we found a normal identity key entry, this is a mismatch
313 // Server previously had identity key but now has none
314 log_warn("Server previously had identity key but now has none - potential security issue");
315 SAFE_FREE(parsed_ip_port);
316 SAFE_FREE(parsed_key_type);
317 SAFE_FREE(parsed_hex_key);
318 SAFE_FREE(parsed_comment);
319 return ERROR_CRYPTO_VERIFICATION; // Mismatch - server changed from identity to no-identity
320 }
321
322 return ASCIICHAT_OK; // Not found = first connection
323}

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, compare_ip_port_strings(), crypto_regex_match_known_hosts(), 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_FREE, 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_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 1143 of file lib/crypto/crypto.c.

1143 {
1144 if (!hmac || !challenge_nonce || !combined_out) {
1145 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: hmac=%p, challenge_nonce=%p, combined_out=%p", hmac,
1146 challenge_nonce, combined_out);
1147 return;
1148 }
1149
1150 // Combine HMAC (32 bytes) + challenge nonce (32 bytes) = 64 bytes total
1151 memcpy(combined_out, hmac, 32);
1152 memcpy(combined_out + 32, challenge_nonce, 32);
1153}

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 899 of file lib/crypto/crypto.c.

900 {
901 if (!ctx || !nonce || !hmac_out) {
902 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_compute_auth_response: Invalid parameters (ctx=%p, nonce=%p, hmac_out=%p)",
903 ctx, nonce, hmac_out);
905 }
906
907 // Ensure shared secret is derived before computing HMAC
908 // This is critical for password HMAC which binds to the shared secret
909 if (!ctx->key_exchange_complete) {
910 SET_ERRNO(ERROR_CRYPTO, "Cannot compute auth response - key exchange not complete");
912 }
913
914 // Bind password HMAC to DH shared_secret to prevent MITM
915 // Combined data: nonce || shared_secret
916 uint8_t combined_data[64];
917 memcpy(combined_data, nonce, 32);
918 memcpy(combined_data + 32, ctx->shared_key, 32);
919
920 // Use password_key if available, otherwise use shared_key
921 const uint8_t *auth_key = ctx->has_password ? ctx->password_key : ctx->shared_key;
922
923 crypto_result_t result = crypto_compute_hmac_ex(ctx, auth_key, combined_data, 64, hmac_out);
924
925 // Securely zero sensitive data containing shared secret
926 sodium_memzero(combined_data, sizeof(combined_data));
927
928 return result;
929}
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.
@ ERROR_CRYPTO
Definition error_codes.h:96

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 828 of file lib/crypto/crypto.c.

829 {
830 if (!ctx || !key || !data || !hmac) {
832 "crypto_compute_hmac: Invalid parameters (ctx=%p, key=%p, data=%p, hmac=%p)", ctx,
833 key, data, hmac);
834 }
835
836 crypto_result_t result = init_libsodium();
837 if (result != CRYPTO_OK) {
838 return result;
839 }
840
841 crypto_auth_hmacsha256(hmac, data, 32, key);
842 return CRYPTO_OK;
843}

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 845 of file lib/crypto/crypto.c.

846 {
847 if (!ctx || !key || !data || !hmac || data_len == 0) {
850 "crypto_compute_hmac_ex: Invalid parameters (ctx=%p, key=%p, data=%p, data_len=%zu, hmac=%p)", ctx, key, data,
851 data_len, hmac);
852 }
853
854 crypto_result_t result = init_libsodium();
855 if (result != CRYPTO_OK) {
856 return result;
857 }
858
859 crypto_auth_hmacsha256(hmac, data, data_len, key);
860 return CRYPTO_OK;
861}

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 1073 of file lib/crypto/crypto.c.

1074 {
1075 if (!ctx || !password_key || !nonce || !shared_secret || !hmac_out) {
1077 "Invalid parameters: ctx=%p, password_key=%p, nonce=%p, shared_secret=%p, hmac_out=%p", ctx,
1078 password_key, nonce, shared_secret, hmac_out);
1079 }
1080
1081 // Combine nonce and shared_secret for HMAC computation
1082 // This binds the password to the DH shared secret, preventing MITM attacks
1083 uint8_t combined_data[64];
1084 memcpy(combined_data, nonce, 32);
1085 memcpy(combined_data + 32, shared_secret, 32);
1086
1087 // Compute HMAC using the password-derived key
1088 if (crypto_compute_hmac_ex(ctx, password_key, combined_data, 64, hmac_out) != 0) {
1089 // Securely zero sensitive data containing shared secret even on error
1090 sodium_memzero(combined_data, sizeof(combined_data));
1091 return SET_ERRNO(ERROR_CRYPTO, "Failed to compute password HMAC");
1092 }
1093
1094 // Securely zero sensitive data containing shared secret
1095 sodium_memzero(combined_data, sizeof(combined_data));
1096
1097 return ASCIICHAT_OK;
1098}

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 967 of file lib/crypto/crypto.c.

968 {
969 if (!ctx || !ctx->initialized || !packet_out || !packet_len_out) {
970 SET_ERRNO(
972 "crypto_create_auth_challenge: Invalid parameters (ctx=%p, initialized=%d, packet_out=%p, packet_len_out=%p)",
973 ctx, ctx ? ctx->initialized : 0, packet_out, packet_len_out);
975 }
976
977 size_t required_size = sizeof(uint32_t) + ctx->auth_challenge_size; // type + nonce
978 if (packet_size < required_size) {
979 SET_ERRNO(ERROR_BUFFER, "crypto_create_auth_challenge: Buffer too small (size=%zu, required=%zu)", packet_size,
980 required_size);
982 }
983
984 // Generate random nonce (stores result in ctx->auth_nonce)
986 if (result != CRYPTO_OK) {
987 return result;
988 }
989
990 // Pack packet: [type:4][nonce:auth_challenge_size]
991 uint32_t packet_type = CRYPTO_PACKET_AUTH_CHALLENGE;
992 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
993 SAFE_MEMCPY(packet_out + sizeof(packet_type), ctx->auth_challenge_size, ctx->auth_nonce, ctx->auth_challenge_size);
994
995 *packet_len_out = required_size;
996 return CRYPTO_OK;
997}
unsigned int uint32_t
Definition common.h:58
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:468
crypto_result_t crypto_generate_nonce(uint8_t nonce[32])
Generate random nonce for authentication.
@ ERROR_BUFFER

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 706 of file lib/crypto/crypto.c.

707 {
708 if (!ctx || !data || !packet_out || !packet_len_out) {
710 "crypto_create_encrypted_packet: Invalid parameters (ctx=%p, data=%p, packet_out=%p, packet_len_out=%p)",
711 ctx, data, packet_out, packet_len_out);
713 }
714
715 if (!crypto_is_ready(ctx)) {
716 SET_ERRNO(ERROR_CRYPTO, "crypto_create_encrypted_packet: Crypto context not ready");
718 }
719
720 // Validate data_len to prevent integer overflow in size calculations.
721 if (data_len > CRYPTO_MAX_PLAINTEXT_SIZE) {
722 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_create_encrypted_packet: data_len %zu exceeds max %d", data_len,
725 }
726
727 // Check for integer overflow: data_len + nonce_size + mac_size
728 if (data_len > SIZE_MAX - ctx->nonce_size - ctx->mac_size) {
729 SET_ERRNO(ERROR_BUFFER, "crypto_create_encrypted_packet: encrypted_size overflow");
731 }
732 size_t encrypted_size = data_len + ctx->nonce_size + ctx->mac_size;
733
734 // Check for integer overflow: header + encrypted_size
735 size_t header_size = sizeof(uint32_t) + sizeof(uint32_t);
736 if (encrypted_size > SIZE_MAX - header_size) {
737 SET_ERRNO(ERROR_BUFFER, "crypto_create_encrypted_packet: required_size overflow");
739 }
740 size_t required_size = header_size + encrypted_size; // type + len + encrypted_data
741
742 if (packet_size < required_size) {
743 SET_ERRNO(ERROR_BUFFER, "crypto_create_encrypted_packet: Buffer too small (size=%zu, required=%zu)", packet_size,
744 required_size);
746 }
747
748 // Encrypt the data
749 size_t ciphertext_len;
750 uint8_t *encrypted_data = packet_out + sizeof(uint32_t) + sizeof(uint32_t);
751 crypto_result_t result = crypto_encrypt(ctx, data, data_len, encrypted_data,
752 packet_size - sizeof(uint32_t) - sizeof(uint32_t), &ciphertext_len);
753 if (result != CRYPTO_OK) {
754 return result;
755 }
756
757 // Pack packet: [type:4][length:4][encrypted_data:var]
758 uint32_t packet_type = CRYPTO_PACKET_ENCRYPTED_DATA;
759 uint32_t data_length = (uint32_t)ciphertext_len;
760
761 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
762 SAFE_MEMCPY(packet_out + sizeof(packet_type), sizeof(data_length), &data_length, sizeof(data_length));
763
764 *packet_len_out = required_size;
765 return CRYPTO_OK;
766}
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 649 of file lib/crypto/crypto.c.

650 {
651 if (!ctx || !ctx->initialized || !packet_out || !packet_len_out) {
653 "crypto_create_public_key_packet: Invalid parameters (ctx=%p, initialized=%d, packet_out=%p, "
654 "packet_len_out=%p)",
655 ctx, ctx ? ctx->initialized : 0, packet_out, packet_len_out);
657 }
658
659 size_t required_size = sizeof(uint32_t) + ctx->public_key_size; // type + key
660 if (packet_size < required_size) {
661 SET_ERRNO(ERROR_BUFFER, "crypto_create_public_key_packet: Buffer too small (size=%zu, required=%zu)", packet_size,
662 required_size);
664 }
665
666 // Pack packet: [type:4][public_key:32]
667 uint32_t packet_type = CRYPTO_PACKET_PUBLIC_KEY;
668 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
669 // Bounds check to prevent buffer overflow
670 size_t copy_size = (ctx->public_key_size <= X25519_KEY_SIZE) ? ctx->public_key_size : X25519_KEY_SIZE;
671 SAFE_MEMCPY(packet_out + sizeof(packet_type), copy_size, ctx->public_key, copy_size);
672
673 *packet_len_out = required_size;
674 return CRYPTO_OK;
675}
#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 505 of file lib/crypto/crypto.c.

506 {
507 if (!ctx || !ctx->initialized || !ciphertext || !plaintext_out || !plaintext_len_out) {
509 "Invalid parameters: ctx=%p, initialized=%d, ciphertext=%p, plaintext_out=%p, plaintext_len_out=%p", ctx,
510 ctx ? ctx->initialized : 0, ciphertext, plaintext_out, plaintext_len_out);
512 }
513
514 if (!crypto_is_ready(ctx)) {
515 SET_ERRNO(ERROR_CRYPTO, "Crypto context not ready for decryption");
517 }
518
519 // Check minimum ciphertext size (nonce + MAC)
520 size_t min_ciphertext_size = ctx->nonce_size + ctx->mac_size;
521 if (ciphertext_len < min_ciphertext_size) {
522 SET_ERRNO(ERROR_INVALID_PARAM, "Ciphertext too small: %zu < %zu", ciphertext_len, min_ciphertext_size);
524 }
525
526 size_t plaintext_len = ciphertext_len - ctx->nonce_size - ctx->mac_size;
527 if (plaintext_out_size < plaintext_len) {
528 SET_ERRNO(ERROR_BUFFER, "Plaintext buffer too small: %zu < %zu", plaintext_out_size, plaintext_len);
530 }
531
532 // Extract nonce from beginning of ciphertext
533 const uint8_t *nonce = ciphertext;
534 const uint8_t *encrypted_data = ciphertext + ctx->nonce_size;
535
536 // Choose decryption key (prefer shared key over password key)
537 const uint8_t *decryption_key = NULL;
538 if (ctx->key_exchange_complete) {
539 decryption_key = ctx->shared_key;
540 } else if (ctx->has_password) {
541 decryption_key = ctx->password_key;
542 } else {
543 SET_ERRNO(ERROR_CRYPTO, "No decryption key available");
545 }
546
547 // Decrypt using NaCl secretbox (XSalsa20 + Poly1305)
548 if (crypto_secretbox_open_easy(plaintext_out, encrypted_data, ciphertext_len - ctx->nonce_size, nonce,
549 decryption_key) != 0) {
550 // Log decryption failure details for debugging
551 char key_hex[65], nonce_hex[49];
552 for (int i = 0; i < 32; i++) {
553 snprintf(&key_hex[i * 2], 3, "%02x", decryption_key[i]);
554 }
555 for (int i = 0; i < 24; i++) {
556 snprintf(&nonce_hex[i * 2], 3, "%02x", nonce[i]);
557 }
558 key_hex[64] = '\0';
559 nonce_hex[48] = '\0';
560 log_error("DECRYPT_FAILED: cipherlen=%zu key=%s nonce=%s key_exchange_complete=%d has_password=%d",
561 ciphertext_len - ctx->nonce_size, key_hex, nonce_hex, ctx->key_exchange_complete, ctx->has_password);
562 SET_ERRNO(ERROR_CRYPTO, "Decryption failed - invalid MAC or corrupted data");
564 }
565
566 *plaintext_len_out = plaintext_len;
567 ctx->bytes_decrypted += plaintext_len;
568
569 return CRYPTO_OK;
570}

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, log_error, 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 acip_server_receive_and_dispatch(), client_decrypt_packet(), client_dispatch_thread(), crypto_handshake_decrypt_packet(), crypto_handshake_process_rekey_complete(), crypto_process_encrypted_packet(), packet_decrypt_envelope(), and receive_packet_secure_with_timeout().

◆ 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.

◆ 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 313 of file lib/crypto/crypto.c.

313 {
314 if (!ctx || !ctx->initialized || !password) {
316 "crypto_derive_password_key: Invalid parameters (ctx=%p, initialized=%d, password=%p)", ctx,
317 ctx ? ctx->initialized : 0, password);
319 }
320
321 // Validate password length requirements
322 crypto_result_t validation_result = crypto_validate_password(password);
323 if (validation_result != CRYPTO_OK) {
324 return validation_result;
325 }
326
327 // Use deterministic salt for consistent key derivation across client/server
328 // This ensures the same password produces the same key on both sides
329 // Salt must be exactly ARGON2ID_SALT_SIZE (32) bytes
330 const char *deterministic_salt = "ascii-chat-password-salt-v1";
331 size_t salt_str_len = strlen(deterministic_salt);
332
333 // Zero-initialize the salt buffer first
334 memset(ctx->password_salt, 0, ctx->salt_size);
335
336 // Copy the salt string (will be padded with zeros to ctx->salt_size)
337 memcpy(ctx->password_salt, deterministic_salt, (salt_str_len < ctx->salt_size) ? salt_str_len : ctx->salt_size);
338
339 // Derive key using Argon2id (memory-hard, secure against GPU attacks)
340 if (crypto_pwhash(ctx->password_key, ctx->encryption_key_size, password, strlen(password), ctx->password_salt,
341 crypto_pwhash_OPSLIMIT_INTERACTIVE, // ~0.1 seconds
342 crypto_pwhash_MEMLIMIT_INTERACTIVE, // ~64MB
343 crypto_pwhash_ALG_DEFAULT) != 0) {
344 SET_ERRNO(ERROR_CRYPTO, "Password key derivation failed - possibly out of memory");
346 }
347
348 log_dev("Password key derived successfully using Argon2id with deterministic salt");
349 return CRYPTO_OK;
350}
crypto_result_t crypto_validate_password(const char *password)
Validate password length requirements.
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534

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_dev, 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_destroy()

void crypto_destroy ( 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 194 of file lib/crypto/crypto.c.

194 {
195 if (!ctx || !ctx->initialized) {
196 return;
197 }
198
199 // Securely wipe sensitive data
200 secure_memzero(ctx->private_key, sizeof(ctx->private_key));
201 secure_memzero(ctx->shared_key, sizeof(ctx->shared_key));
202 secure_memzero(ctx->password_key, sizeof(ctx->password_key));
203 secure_memzero(ctx->password_salt, sizeof(ctx->password_salt));
204
205 char encrypted_str[32], decrypted_str[32];
206 format_bytes_pretty(ctx->bytes_encrypted, encrypted_str, sizeof(encrypted_str));
207 format_bytes_pretty(ctx->bytes_decrypted, decrypted_str, sizeof(decrypted_str));
208
209 log_debug("Crypto context cleaned up (encrypted: %s, decrypted: %s)", encrypted_str, decrypted_str);
210
211 // Clear entire context
212 secure_memzero(ctx, sizeof(crypto_context_t));
213}
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Format byte count into human-readable string.
Definition util/format.c:10
Cryptographic context structure.

References crypto_context_t::bytes_decrypted, crypto_context_t::bytes_encrypted, format_bytes_pretty(), 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_destroy(), 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 431 of file lib/crypto/crypto.c.

432 {
433 if (!ctx || !ctx->initialized || !plaintext || !ciphertext_out || !ciphertext_len_out) {
435 "Invalid parameters: ctx=%p, initialized=%d, plaintext=%p, ciphertext_out=%p, ciphertext_len_out=%p", ctx,
436 ctx ? ctx->initialized : 0, plaintext, ciphertext_out, ciphertext_len_out);
438 }
439
440 if (plaintext_len == 0 || plaintext_len > CRYPTO_MAX_PLAINTEXT_SIZE) {
441 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid plaintext length: %zu (max: %d)", plaintext_len, CRYPTO_MAX_PLAINTEXT_SIZE);
443 }
444
445 if (!crypto_is_ready(ctx)) {
446 SET_ERRNO(ERROR_CRYPTO, "Crypto context not ready for encryption");
448 }
449
450 // Check output buffer size
451 size_t required_size = plaintext_len + ctx->nonce_size + ctx->mac_size;
452 if (ciphertext_out_size < required_size) {
453 SET_ERRNO(ERROR_BUFFER, "Ciphertext buffer too small: %zu < %zu", ciphertext_out_size, required_size);
455 }
456
457 // Check for nonce counter exhaustion (extremely unlikely in practice)
458 // Starting from 1, reaching UINT64_MAX would require ~292 billion years at 60 FPS.
459 // With key rotation required at 1M packets (~16 seconds), exhaustion is virtually impossible.
460 // This check is a safety fallback that should never trigger in practice.
461 if (ctx->nonce_counter == 0 || ctx->nonce_counter == UINT64_MAX) {
462 SET_ERRNO(ERROR_CRYPTO, "Nonce counter exhausted - key rotation required");
464 }
465
466 // Choose encryption key BEFORE generating nonce (prevents nonce_counter increment on error)
467 const uint8_t *encryption_key = NULL;
468 if (ctx->key_exchange_complete) {
469 encryption_key = ctx->shared_key;
470 } else if (ctx->has_password) {
471 encryption_key = ctx->password_key;
472 } else {
473 SET_ERRNO(ERROR_CRYPTO, "No encryption key available");
475 }
476
477 // Generate nonce and place at beginning of ciphertext
478 uint8_t nonce[XSALSA20_NONCE_SIZE]; // Use maximum nonce size for buffer
479 generate_nonce(ctx, nonce);
480 SAFE_MEMCPY(ciphertext_out, ctx->nonce_size, nonce, ctx->nonce_size);
481
482 // Log nonce for debugging
483 char nonce_hex[49];
484 for (int i = 0; i < 24; i++) {
485 snprintf(&nonce_hex[i * 2], 3, "%02x", nonce[i]);
486 }
487 nonce_hex[48] = '\0';
488 log_debug("ENCRYPT_NONCE: counter=%lu nonce=%s plaintext_len=%zu", ctx->nonce_counter - 1, nonce_hex, plaintext_len);
489
490 // Encrypt using NaCl secretbox (XSalsa20 + Poly1305)
491 if (crypto_secretbox_easy(ciphertext_out + ctx->nonce_size, plaintext, plaintext_len, nonce, encryption_key) != 0) {
492 SET_ERRNO(ERROR_CRYPTO, "Encryption failed");
494 }
495
496 *ciphertext_len_out = required_size;
497 ctx->bytes_encrypted += plaintext_len;
498
499 // Increment rekey packet counter for rekeying trigger detection
500 ctx->rekey_packet_count++;
501
502 return CRYPTO_OK;
503}
#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, log_debug, 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 client_encrypt_packet(), crypto_create_encrypted_packet(), crypto_handshake_encrypt_packet(), 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 1155 of file lib/crypto/crypto.c.

1155 {
1156 if (!combined_data || !hmac_out || !challenge_out) {
1157 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: combined_data=%p, hmac_out=%p, challenge_out=%p", combined_data,
1158 hmac_out, challenge_out);
1159 return;
1160 }
1161
1162 // Extract HMAC (first 32 bytes) and challenge nonce (last 32 bytes)
1163 memcpy(hmac_out, combined_data, 32);
1164 memcpy(challenge_out, combined_data + 32, 32);
1165}

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 215 of file lib/crypto/crypto.c.

215 {
216 if (!ctx) {
217 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_generate_keypair: NULL context");
219 }
220
221 log_info("DEBUG: About to call crypto_box_keypair");
222 // Generate X25519 key pair for key exchange
223 if (crypto_box_keypair(ctx->public_key, ctx->private_key) != 0) {
224 SET_ERRNO(ERROR_CRYPTO, "Failed to generate X25519 key pair");
226 }
227
228 log_debug("Generated X25519 key pair for key exchange");
229 return CRYPTO_OK;
230}

References CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_KEY_GENERATION, CRYPTO_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, log_debug, log_info, 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 813 of file lib/crypto/crypto.c.

813 {
814 if (!nonce) {
815 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_generate_nonce: NULL nonce buffer");
817 }
818
819 crypto_result_t result = init_libsodium();
820 if (result != CRYPTO_OK) {
821 return result;
822 }
823
824 randombytes_buf(nonce, 32);
825 return CRYPTO_OK;
826}

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 236 of file lib/crypto/crypto.c.

236 {
237 if (!ctx || !ctx->initialized || !public_key_out) {
239 "crypto_get_public_key: Invalid parameters (ctx=%p, initialized=%d, public_key_out=%p)", ctx,
240 ctx ? ctx->initialized : 0, public_key_out);
242 }
243
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(public_key_out, copy_size, ctx->public_key, copy_size);
247 return CRYPTO_OK;
248}

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 1367 of file lib/crypto/crypto.c.

1367 {
1368 if (!ctx || !status_buffer || buffer_size == 0) {
1369 return;
1370 }
1371
1372 time_t now = time(NULL);
1373 time_t elapsed = now - ctx->rekey_last_time;
1374 time_t remaining_time = (ctx->rekey_time_threshold > elapsed) ? (ctx->rekey_time_threshold - elapsed) : 0;
1375 uint64_t remaining_packets = (ctx->rekey_packet_threshold > ctx->rekey_packet_count)
1377 : 0;
1378
1379 safe_snprintf(status_buffer, buffer_size,
1380 "Rekey status: %s | "
1381 "Packets: %llu/%llu (%llu remaining) | "
1382 "Time: %ld/%ld sec (%ld sec remaining) | "
1383 "Rekeys: %llu | Failures: %d",
1384 ctx->rekey_in_progress ? "IN_PROGRESS" : "IDLE", (unsigned long long)ctx->rekey_packet_count,
1385 (unsigned long long)ctx->rekey_packet_threshold, (unsigned long long)remaining_packets, (long)elapsed,
1386 (long)ctx->rekey_time_threshold, (long)remaining_time, (unsigned long long)ctx->rekey_count,
1387 ctx->rekey_failure_count);
1388}
int buffer_size
Size of circular buffer.
Definition grep.c:90
unsigned long long uint64_t
Definition common.h:59

References buffer_size, 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, crypto_context_t::rekey_time_threshold, and safe_snprintf().

◆ 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 605 of file lib/crypto/crypto.c.

605 {
606 if (!ctx || !status_buffer || buffer_size == 0) {
607 return;
608 }
609
610 if (!ctx->initialized) {
611 SAFE_SNPRINTF(status_buffer, buffer_size, "Not initialized");
612 return;
613 }
614
615 SAFE_SNPRINTF(status_buffer, buffer_size,
616 "Initialized: %s, Password: %s, Key Exchange: %s, Ready: %s, "
617 "Encrypted: %" PRIu64 " bytes, Decrypted: %" PRIu64 " bytes, Nonce: %" PRIu64,
618 ctx->initialized ? "yes" : "no", ctx->has_password ? "yes" : "no",
619 ctx->key_exchange_complete ? "complete" : "incomplete", crypto_is_ready(ctx) ? "yes" : "no",
620 ctx->bytes_encrypted, ctx->bytes_decrypted, ctx->nonce_counter);
621}
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492

References buffer_size, 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 84 of file lib/crypto/crypto.c.

84 {
85 log_info("DEBUG: crypto_init START");
86 if (!ctx) {
87 log_info("DEBUG: crypto_init ctx is NULL");
88 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
90 }
91
92 // Initialize libsodium
93 log_info("DEBUG: crypto_init calling init_libsodium");
94 crypto_result_t result = init_libsodium();
95 if (result != CRYPTO_OK) {
96 return result;
97 }
98
99 // Clear context
100 secure_memzero(ctx, sizeof(crypto_context_t));
101
102 // Generate key pair for X25519 key exchange
103 result = crypto_generate_keypair(ctx);
104 if (result != CRYPTO_OK) {
105 return result;
106 }
107
108 ctx->initialized = true;
109 ctx->has_password = false;
110 ctx->key_exchange_complete = false;
111 ctx->peer_key_received = false;
112 ctx->handshake_complete = false;
113 ctx->encrypt_data = true; // Default: enable payload encryption
114 ctx->nonce_counter = 1; // Start from 1 (0 reserved for testing)
115 ctx->bytes_encrypted = 0;
116 ctx->bytes_decrypted = 0;
117
118 // Set default algorithm-specific parameters (can be overridden during handshake)
128 // signature_size and auth_public_key_size remain 0 until set by CRYPTO_PARAMETERS
129 // (must be 0 for unauthenticated format detection in crypto_handshake_client_key_exchange)
130
131 // Generate unique session ID to prevent replay attacks across connections
132 randombytes_buf(ctx->session_id, sizeof(ctx->session_id));
133
134 // Initialize rekeying state
135 ctx->rekey_packet_count = 0;
136 ctx->rekey_last_time = time(NULL); // Initialize to current time
137 ctx->rekey_last_request_time = 0; // No rekey request yet
138 ctx->rekey_in_progress = false;
139 ctx->rekey_failure_count = 0;
140 ctx->has_temp_key = false;
141 ctx->rekey_count = 0;
142
143 // SECURITY: Use production-safe rekey thresholds by default
144 // Rekey every 1 hour OR 1 million packets (whichever comes first)
145 // Only use test mode if explicitly requested via environment variable
146 if (is_test_environment()) {
149 char duration_str[32];
150 time_pretty(ctx->rekey_time_threshold, -1, duration_str, sizeof(duration_str));
151 log_dev("Crypto context initialized with X25519 key exchange (TEST MODE rekey thresholds: %llu packets, %s)",
152 (unsigned long long)ctx->rekey_packet_threshold, duration_str);
153 } else {
154 ctx->rekey_packet_threshold = REKEY_DEFAULT_PACKET_THRESHOLD; // 1 million packets
155 ctx->rekey_time_threshold = REKEY_DEFAULT_TIME_THRESHOLD; // already in nanoseconds (3600 * NS_PER_SEC_INT)
156 char duration_str[32];
157 time_pretty(ctx->rekey_time_threshold, -1, duration_str, sizeof(duration_str));
158 log_dev("Crypto context initialized with X25519 key exchange (rekey thresholds: %llu packets, %s)",
159 (unsigned long long)ctx->rekey_packet_threshold, duration_str);
160 }
161 return CRYPTO_OK;
162}
#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 nanoseconds)
#define SECRETBOX_KEY_SIZE
Secretbox key size in bytes.
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 ARGON2ID_SALT_SIZE
Argon2id salt size in bytes.
#define REKEY_TEST_TIME_THRESHOLD
Test mode rekey time threshold (30 seconds in nanoseconds)
#define REKEY_DEFAULT_PACKET_THRESHOLD
Default rekey packet threshold (1 million packets)
#define POLY1305_MAC_SIZE
Poly1305 MAC size in bytes.
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424

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, crypto_context_t::encrypt_data, crypto_context_t::encryption_key_size, ERROR_INVALID_PARAM, 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_dev, 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, time_pretty(), 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 164 of file lib/crypto/crypto.c.

164 {
165 if (!ctx || !password) {
166 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, password=%p", ctx, password);
168 }
169
170 if (strlen(password) == 0) {
171 SET_ERRNO(ERROR_INVALID_PARAM, "Password cannot be empty");
173 }
174
175 // First initialize basic crypto
176 crypto_result_t result = crypto_init(ctx);
177 if (result != CRYPTO_OK) {
178 return result;
179 }
180
181 // Derive password key
182 result = crypto_derive_password_key(ctx, password);
183 if (result != CRYPTO_OK) {
184 crypto_destroy(ctx);
185 return result;
186 }
187
188 ctx->has_password = true;
189
190 log_dev("Crypto context initialized with password-based encryption");
191 return CRYPTO_OK;
192}
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.
void crypto_destroy(crypto_context_t *ctx)
Cleanup crypto context with secure memory wiping.

References crypto_derive_password_key(), crypto_destroy(), CRYPTO_ERROR_INVALID_PARAMS, crypto_init(), CRYPTO_OK, ERROR_INVALID_PARAM, crypto_context_t::has_password, log_dev, 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 277 of file lib/crypto/crypto.c.

277 {
278 if (!ctx || !ctx->initialized) {
279 return false;
280 }
281
282 // Ready if either key exchange is complete OR password is set
283 return ctx->key_exchange_complete || ctx->has_password;
284}

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 999 of file lib/crypto/crypto.c.

999 {
1000 if (!ctx || !ctx->initialized || !packet) {
1002 "crypto_process_auth_challenge: Invalid parameters (ctx=%p, initialized=%d, packet=%p)", ctx,
1003 ctx ? ctx->initialized : 0, packet);
1005 }
1006
1007 size_t expected_size = sizeof(uint32_t) + ctx->auth_challenge_size; // type + nonce
1008 if (packet_len != expected_size) {
1009 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_challenge: Invalid packet size (expected=%zu, got=%zu)",
1010 expected_size, packet_len);
1012 }
1013
1014 // Unpack packet: [type:4][nonce:auth_challenge_size]
1015 uint32_t packet_type;
1016 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
1017
1018 if (packet_type != CRYPTO_PACKET_AUTH_CHALLENGE) {
1019 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_challenge: Invalid packet type (expected=%u, got=%u)",
1020 CRYPTO_PACKET_AUTH_CHALLENGE, packet_type);
1022 }
1023
1024 // Store the nonce for HMAC computation (use buffer size for memcpy size, actual size from context)
1025 SAFE_MEMCPY(ctx->auth_nonce, sizeof(ctx->auth_nonce), packet + sizeof(packet_type), ctx->auth_challenge_size);
1026
1027 log_debug("Auth challenge received and processed");
1028 return CRYPTO_OK;
1029}

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 1031 of file lib/crypto/crypto.c.

1031 {
1032 if (!ctx || !ctx->initialized || !packet) {
1034 "crypto_process_auth_response: Invalid context or packet (ctx=%p, initialized=%d, packet=%p)", ctx,
1035 ctx ? ctx->initialized : 0, packet);
1037 }
1038
1039 size_t expected_size = sizeof(uint32_t) + 32; // type + hmac
1040 if (packet_len != expected_size) {
1041 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_response: Invalid packet size (expected=%zu, got=%zu)",
1042 expected_size, packet_len);
1044 }
1045
1046 // Unpack packet: [type:4][hmac:32]
1047 uint32_t packet_type;
1048 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
1049
1050 if (packet_type != CRYPTO_PACKET_AUTH_RESPONSE) {
1051 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_auth_response: Invalid packet type (expected=0x%x, got=0x%x)",
1052 CRYPTO_PACKET_AUTH_RESPONSE, packet_type);
1054 }
1055
1056 const uint8_t *received_hmac = packet + sizeof(packet_type);
1057
1058 // Verify HMAC using shared secret
1059 if (!crypto_verify_hmac(ctx->shared_key, ctx->auth_nonce, received_hmac)) {
1060 SET_ERRNO(ERROR_CRYPTO, "crypto_process_auth_response: HMAC verification failed");
1062 }
1063
1064 ctx->handshake_complete = true;
1065 log_debug("Authentication successful - handshake complete");
1066 return CRYPTO_OK;
1067}
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 768 of file lib/crypto/crypto.c.

769 {
770 if (!ctx || !packet || !data_out || !data_len_out) {
772 "crypto_process_encrypted_packet: Invalid parameters (ctx=%p, packet=%p, data_out=%p, data_len_out=%p)",
773 ctx, packet, data_out, data_len_out);
775 }
776
777 if (!crypto_is_ready(ctx)) {
778 SET_ERRNO(ERROR_CRYPTO, "crypto_process_encrypted_packet: Crypto context not ready");
780 }
781
782 if (packet_len < sizeof(uint32_t) + sizeof(uint32_t)) {
783 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_encrypted_packet: Packet too small (size=%zu)", packet_len);
785 }
786
787 // Unpack packet: [type:4][length:4][encrypted_data:var]
788 uint32_t packet_type;
789 uint32_t data_length;
790 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
791 SAFE_MEMCPY(&data_length, sizeof(data_length), packet + sizeof(packet_type), sizeof(data_length));
792
793 if (packet_type != CRYPTO_PACKET_ENCRYPTED_DATA) {
794 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_encrypted_packet: Invalid packet type (expected=%u, got=%u)",
795 CRYPTO_PACKET_ENCRYPTED_DATA, packet_type);
797 }
798
799 if (packet_len != sizeof(uint32_t) + sizeof(uint32_t) + data_length) {
800 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_encrypted_packet: Packet length mismatch (expected=%zu, got=%zu)",
801 sizeof(uint32_t) + sizeof(uint32_t) + data_length, packet_len);
803 }
804
805 const uint8_t *encrypted_data = packet + sizeof(uint32_t) + sizeof(uint32_t);
806 return crypto_decrypt(ctx, encrypted_data, data_length, data_out, data_size, data_len_out);
807}
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 677 of file lib/crypto/crypto.c.

677 {
678 if (!ctx || !ctx->initialized || !packet) {
680 "crypto_process_public_key_packet: Invalid parameters (ctx=%p, initialized=%d, packet=%p)", ctx,
681 ctx ? ctx->initialized : 0, packet);
683 }
684
685 size_t expected_size = sizeof(uint32_t) + ctx->public_key_size;
686 if (packet_len != expected_size) {
687 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_public_key_packet: Invalid packet size (expected=%zu, got=%zu)",
688 expected_size, packet_len);
690 }
691
692 // Unpack packet: [type:4][public_key:32]
693 uint32_t packet_type;
694 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
695
696 if (packet_type != CRYPTO_PACKET_PUBLIC_KEY) {
697 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_process_public_key_packet: Invalid packet type (expected=%u, got=%u)",
698 CRYPTO_PACKET_PUBLIC_KEY, packet_type);
700 }
701
702 const uint8_t *peer_public_key = packet + sizeof(packet_type);
703 return crypto_set_peer_public_key(ctx, peer_public_key);
704}
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 630 of file lib/crypto/crypto.c.

630 {
631 if (!buffer || len == 0) {
632 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_random_bytes: Invalid parameters (buffer=%p, len=%zu)", buffer, len);
634 }
635
636 crypto_result_t result = init_libsodium();
637 if (result != CRYPTO_OK) {
638 return result;
639 }
640
641 randombytes_buf(buffer, len);
642 return CRYPTO_OK;
643}

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 1345 of file lib/crypto/crypto.c.

1345 {
1346 if (!ctx) {
1347 return;
1348 }
1349
1350 log_warn("Aborting rekey (attempt %d failed), keeping old encryption key", ctx->rekey_failure_count + 1);
1351
1352 // Wipe temporary keys securely
1353 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1354 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1355 secure_memzero(ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1356
1357 // Reset rekey state
1358 ctx->has_temp_key = false;
1359 ctx->rekey_in_progress = false;
1360
1361 // Increment failure counter for exponential backoff
1362 ctx->rekey_failure_count++;
1363
1364 // Continue using old key - no disruption to connection
1365}

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 1301 of file lib/crypto/crypto.c.

1301 {
1302 if (!ctx || !ctx->initialized) {
1303 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_commit: Invalid context");
1305 }
1306
1307 if (!ctx->rekey_in_progress || !ctx->has_temp_key) {
1308 SET_ERRNO(ERROR_CRYPTO, "No rekey in progress to commit");
1310 }
1311
1312 // Wipe old shared secret securely
1313 secure_memzero(ctx->shared_key, sizeof(ctx->shared_key));
1314
1315 // Switch to new shared secret
1316 SAFE_MEMCPY(ctx->shared_key, sizeof(ctx->shared_key), ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1317
1318 // Reset nonce counter to 1 (fresh start with new key)
1319 ctx->nonce_counter = 1;
1320
1321 // Generate new session ID to prevent cross-session replay
1322 randombytes_buf(ctx->session_id, sizeof(ctx->session_id));
1323
1324 // Reset rekey tracking
1325 ctx->rekey_packet_count = 0;
1326 ctx->rekey_last_time = time(NULL);
1327 ctx->rekey_count++;
1328
1329 // Clear rekey state
1330 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1331 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1332 secure_memzero(ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1333 ctx->has_temp_key = false;
1334 ctx->rekey_in_progress = false;
1335
1336 // Reset failure counter on successful rekey
1337 ctx->rekey_failure_count = 0;
1338
1339 log_debug("Rekey committed successfully (rekey #%llu, nonce reset to 1, new session_id generated)",
1340 (unsigned long long)ctx->rekey_count);
1341
1342 return CRYPTO_OK;
1343}

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_debug, 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 1205 of file lib/crypto/crypto.c.

1205 {
1206 if (!ctx || !ctx->initialized) {
1207 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_init: Invalid context");
1209 }
1210
1211 if (ctx->rekey_in_progress) {
1212 SET_ERRNO(ERROR_CRYPTO, "Rekey already in progress");
1214 }
1215
1216 // Rate limiting: check minimum interval since last rekey
1217 time_t now = time(NULL);
1218 time_t since_last_rekey = now - ctx->rekey_last_time;
1219 time_t min_interval_seconds = (time_t)(REKEY_MIN_INTERVAL / NS_PER_SEC_INT);
1220 if (since_last_rekey < min_interval_seconds) {
1221 char elapsed_str[32], min_str[32];
1222 time_pretty((uint64_t)(since_last_rekey * 1e9), -1, elapsed_str, sizeof(elapsed_str));
1223 time_pretty((uint64_t)(min_interval_seconds * 1e9), -1, min_str, sizeof(min_str));
1224 log_warn("Rekey rate limited: %s since last rekey (minimum: %s)", elapsed_str, min_str);
1226 }
1227
1228 // Check if too many consecutive failures
1230 log_error("Too many consecutive rekey failures (%d), giving up", ctx->rekey_failure_count);
1232 }
1233
1234 // Generate new ephemeral X25519 keypair for rekeying
1235 if (crypto_box_keypair(ctx->temp_public_key, ctx->temp_private_key) != 0) {
1236 SET_ERRNO(ERROR_CRYPTO, "Failed to generate rekey ephemeral keypair");
1238 }
1239
1240 ctx->rekey_in_progress = true;
1241 ctx->has_temp_key = true;
1242
1243 log_debug("Rekey initiated (packets: %llu, time elapsed: %ld sec, attempt %d)",
1244 (unsigned long long)ctx->rekey_packet_count, (long)since_last_rekey, ctx->rekey_failure_count + 1);
1245
1246 return CRYPTO_OK;
1247}
#define REKEY_MIN_INTERVAL
Minimum time interval between rekey requests (3 seconds in nanoseconds for testing,...
#define REKEY_MAX_FAILURE_COUNT
Maximum consecutive rekey failures before giving up.
#define NS_PER_SEC_INT
Definition time.h:157

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, crypto_context_t::has_temp_key, crypto_context_t::initialized, log_debug, log_error, log_warn, NS_PER_SEC_INT, 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, crypto_context_t::temp_public_key, and time_pretty().

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 1249 of file lib/crypto/crypto.c.

1249 {
1250 if (!ctx || !ctx->initialized || !peer_new_public_key) {
1251 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_process_request: Invalid parameters");
1253 }
1254
1255 // Generate our own new ephemeral keypair (responder side)
1256 if (crypto_box_keypair(ctx->temp_public_key, ctx->temp_private_key) != 0) {
1257 SET_ERRNO(ERROR_CRYPTO, "Failed to generate rekey ephemeral keypair");
1259 }
1260
1261 // Compute new shared secret: DH(our_new_private_key, peer_new_public_key)
1262 if (crypto_scalarmult(ctx->temp_shared_key, ctx->temp_private_key, peer_new_public_key) != 0) {
1263 SET_ERRNO(ERROR_CRYPTO, "Failed to compute rekey shared secret");
1264 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1265 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1267 }
1268
1269 ctx->rekey_in_progress = true;
1270 ctx->has_temp_key = true;
1271
1272 log_debug("Rekey request processed (responder side), new shared secret computed");
1273
1274 return CRYPTO_OK;
1275}

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 1277 of file lib/crypto/crypto.c.

1277 {
1278 if (!ctx || !ctx->initialized || !peer_new_public_key) {
1279 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_rekey_process_response: Invalid parameters");
1281 }
1282
1283 if (!ctx->rekey_in_progress || !ctx->has_temp_key) {
1284 SET_ERRNO(ERROR_CRYPTO, "No rekey in progress");
1286 }
1287
1288 // Compute new shared secret: DH(our_new_private_key, peer_new_public_key)
1289 // Use crypto_scalarmult to produce raw 32-byte shared secret for crypto_secretbox
1290 if (crypto_scalarmult(ctx->temp_shared_key, ctx->temp_private_key, peer_new_public_key) != 0) {
1291 SET_ERRNO(ERROR_CRYPTO, "Failed to compute rekey shared secret");
1292 crypto_rekey_abort(ctx);
1294 }
1295
1296 log_debug("Rekey response processed (initiator side), new shared secret computed");
1297
1298 return CRYPTO_OK;
1299}
void crypto_rekey_abort(crypto_context_t *ctx)
Abort rekeying and fallback to old keys.

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 597 of file lib/crypto/crypto.c.

597 {
598 int idx = -result; // CRYPTO_OK=0, errors are negative
599 if (idx >= 0 && (size_t)idx < CRYPTO_RESULT_STRING_COUNT) {
600 return g_crypto_result_strings[idx];
601 }
602 return "Unknown error";
603}
#define CRYPTO_RESULT_STRING_COUNT

References CRYPTO_RESULT_STRING_COUNT.

Referenced by acip_server_receive_and_dispatch(), client_crypto_init(), client_dispatch_thread(), 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(), packet_decrypt_envelope(), receive_packet_secure_with_timeout(), 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 623 of file lib/crypto/crypto.c.

623 {
624 if (!lhs || !rhs) {
625 return false;
626 }
627 return sodium_memcmp(lhs, rhs, len) == 0;
628}

◆ 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 250 of file lib/crypto/crypto.c.

250 {
251 if (!ctx || !ctx->initialized || !peer_public_key) {
253 "crypto_set_peer_public_key: Invalid parameters (ctx=%p, initialized=%d, peer_public_key=%p)", ctx,
254 ctx ? ctx->initialized : 0, peer_public_key);
256 }
257
258 // Store peer's public key
259 // Bounds check to prevent buffer overflow
260 size_t copy_size = (ctx->public_key_size <= X25519_KEY_SIZE) ? ctx->public_key_size : X25519_KEY_SIZE;
261 SAFE_MEMCPY(ctx->peer_public_key, copy_size, peer_public_key, copy_size);
262 ctx->peer_key_received = true;
263
264 // Compute shared secret using X25519 and crypto_scalarmult (compatible with secretbox)
265 // This produces a raw 32-byte shared secret suitable for crypto_secretbox
266 if (crypto_scalarmult(ctx->shared_key, ctx->private_key, peer_public_key) != 0) {
267 SET_ERRNO(ERROR_CRYPTO, "Failed to compute shared secret from peer public key");
269 }
270
271 ctx->key_exchange_complete = true;
272
273 log_debug("Key exchange complete");
274 return CRYPTO_OK;
275}

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 1171 of file lib/crypto/crypto.c.

1171 {
1172 if (!ctx || !ctx->initialized) {
1173 return false;
1174 }
1175
1176 // Don't trigger rekey if one is already in progress
1177 if (ctx->rekey_in_progress) {
1178 return false;
1179 }
1180
1181 // Don't trigger rekey if handshake isn't complete yet
1182 if (!ctx->handshake_complete) {
1183 return false;
1184 }
1185
1186 // Check packet count threshold
1187 if (ctx->rekey_packet_count >= ctx->rekey_packet_threshold) {
1188 log_debug("Rekey triggered: packet count (%llu) >= threshold (%llu)", (unsigned long long)ctx->rekey_packet_count,
1189 (unsigned long long)ctx->rekey_packet_threshold);
1190 return true;
1191 }
1192
1193 // Check time threshold
1194 time_t now = time(NULL);
1195 time_t elapsed = now - ctx->rekey_last_time;
1196 if (elapsed >= ctx->rekey_time_threshold) {
1197 log_debug("Rekey triggered: time elapsed (%ld sec) >= threshold (%ld sec)", (long)elapsed,
1198 (long)ctx->rekey_time_threshold);
1199 return true;
1200 }
1201
1202 return false;
1203}

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 1119 of file lib/crypto/crypto.c.

1120 {
1121 if (!private_key || !ephemeral_key || !signature_out) {
1122 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: private_key=%p, ephemeral_key=%p, signature_out=%p",
1123 private_key, ephemeral_key, signature_out);
1124 }
1125
1126 if (ephemeral_key_size == 0) {
1127 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ephemeral key size: %zu", ephemeral_key_size);
1128 return ERROR_INVALID_PARAM;
1129 }
1130
1131 // Sign the ephemeral key with our Ed25519 private key
1132 if (private_key->type == KEY_TYPE_ED25519) {
1133 if (crypto_sign_detached(signature_out, NULL, ephemeral_key, ephemeral_key_size, private_key->key.ed25519) != 0) {
1134 return SET_ERRNO(ERROR_CRYPTO, "Failed to sign ephemeral key");
1135 }
1136 } else {
1137 return SET_ERRNO(ERROR_CRYPTO, "Unsupported private key type for signing");
1138 }
1139
1140 return ASCIICHAT_OK;
1141}
key_type_t type
Definition key_types.h:92
uint8_t ed25519[64]
Definition key_types.h:94
union private_key_t::@14 key
@ KEY_TYPE_ED25519
Definition key_types.h:52

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 290 of file lib/crypto/crypto.c.

290 {
291 if (!password) {
292 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_validate_password: Password is NULL");
294 }
295
296 size_t password_len = strlen(password);
297
298 if (password_len < MIN_PASSWORD_LENGTH) {
299 SET_ERRNO(ERROR_INVALID_PARAM, "Password too short (minimum %d characters, got %zu)", MIN_PASSWORD_LENGTH,
300 password_len);
302 }
303
304 if (password_len > MAX_PASSWORD_LENGTH) {
305 SET_ERRNO(ERROR_INVALID_PARAM, "Password too long (maximum %d characters, got %zu)", MAX_PASSWORD_LENGTH,
306 password_len);
308 }
309
310 return CRYPTO_OK;
311}
#define MIN_PASSWORD_LENGTH
Minimum password length (8 characters)
#define MAX_PASSWORD_LENGTH
Maximum password length (256 characters)

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 931 of file lib/crypto/crypto.c.

932 {
933 if (!ctx || !nonce || !expected_hmac) {
935 "crypto_verify_auth_response: Invalid parameters (ctx=%p, nonce=%p, expected_hmac=%p)", ctx, nonce,
936 expected_hmac);
937 return false;
938 }
939
940 // Ensure shared secret is derived before verifying HMAC
941 // This is critical for password HMAC verification which binds to the shared secret
942 if (!ctx->key_exchange_complete) {
943 SET_ERRNO(ERROR_CRYPTO, "Cannot verify auth response - key exchange not complete");
944 return false;
945 }
946
947 // Bind password HMAC to DH shared_secret to prevent MITM
948 // Combined data: nonce || shared_secret
949 uint8_t combined_data[64];
950 memcpy(combined_data, nonce, 32);
951 memcpy(combined_data + 32, ctx->shared_key, 32);
952
953 // Use password_key if available, otherwise use shared_key
954 const uint8_t *auth_key = ctx->has_password ? ctx->password_key : ctx->shared_key;
955
956 log_debug("Verifying auth response: has_password=%d, key_exchange_complete=%d, using_password_key=%d",
957 ctx->has_password, ctx->key_exchange_complete, (auth_key == ctx->password_key));
958
959 bool result = crypto_verify_hmac_ex(auth_key, combined_data, 64, expected_hmac);
960
961 // Securely zero sensitive data containing shared secret
962 sodium_memzero(combined_data, sizeof(combined_data));
963
964 return result;
965}
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.

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 863 of file lib/crypto/crypto.c.

863 {
864 if (!key || !data || !expected_hmac) {
865 SET_ERRNO(ERROR_INVALID_PARAM, "crypto_verify_hmac: Invalid parameters (key=%p, data=%p, expected_hmac=%p)", key,
866 data, expected_hmac);
867 return false;
868 }
869
870 uint8_t computed_hmac[32];
871 if (crypto_auth_hmacsha256(computed_hmac, data, 32, key) != 0) {
872 return false;
873 }
874
875 return sodium_memcmp(computed_hmac, expected_hmac, 32) == 0;
876}

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 878 of file lib/crypto/crypto.c.

879 {
880 if (!key || !data || !expected_hmac || data_len == 0) {
882 "crypto_verify_hmac_ex: Invalid parameters (key=%p, data=%p, data_len=%zu, expected_hmac=%p)", key, data,
883 data_len, expected_hmac);
884 return false;
885 }
886
887 uint8_t computed_hmac[32];
888 if (crypto_auth_hmacsha256(computed_hmac, data, data_len, key) != 0) {
889 return false;
890 }
891
892 return sodium_memcmp(computed_hmac, expected_hmac, 32) == 0;
893}

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 352 of file lib/crypto/crypto.c.

352 {
353 if (!ctx || !ctx->initialized || !ctx->has_password || !password) {
354 return false;
355 }
356
357 uint8_t test_key[SECRETBOX_KEY_SIZE]; // Use maximum size for buffer
358
359 // Use the same deterministic salt for verification
360 // Salt must be exactly ARGON2ID_SALT_SIZE (32) bytes
361 const char *deterministic_salt = "ascii-chat-password-salt-v1";
362 uint8_t salt[ARGON2ID_SALT_SIZE]; // Use maximum size for buffer
363 size_t salt_str_len = strlen(deterministic_salt);
364
365 // Zero-initialize the salt buffer first
366 memset(salt, 0, ARGON2ID_SALT_SIZE);
367
368 // Copy the salt string (will be padded with zeros to ctx->salt_size)
369 memcpy(salt, deterministic_salt, (salt_str_len < ctx->salt_size) ? salt_str_len : ctx->salt_size);
370
371 // Derive key with same salt
372 if (crypto_pwhash(test_key, ctx->encryption_key_size, password, strlen(password), salt,
373 crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE,
374 crypto_pwhash_ALG_DEFAULT) != 0) {
375 secure_memzero(test_key, sizeof(test_key));
376 return false;
377 }
378
379 // Constant-time comparison
380 bool match = (sodium_memcmp(test_key, ctx->password_key, ctx->encryption_key_size) == 0);
381
382 secure_memzero(test_key, sizeof(test_key));
383 return match;
384}

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 1100 of file lib/crypto/crypto.c.

1101 {
1102 if (!peer_public_key || !ephemeral_key || !signature) {
1103 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: peer_public_key=%p, ephemeral_key=%p, signature=%p",
1104 peer_public_key, ephemeral_key, signature);
1105 }
1106
1107 if (ephemeral_key_size == 0) {
1108 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ephemeral key size: %zu", ephemeral_key_size);
1109 }
1110
1111 // Verify the signature using Ed25519
1112 if (crypto_sign_verify_detached(signature, ephemeral_key, ephemeral_key_size, peer_public_key) != 0) {
1113 return SET_ERRNO(ERROR_CRYPTO, "Peer signature verification failed");
1114 }
1115
1116 return ASCIICHAT_OK;
1117}

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 656 of file known_hosts.c.

657 {
658 char expected_fp[CRYPTO_HEX_KEY_SIZE_NULL], received_fp[CRYPTO_HEX_KEY_SIZE_NULL];
659 compute_key_fingerprint(expected_key, expected_fp);
660 compute_key_fingerprint(received_key, received_fp);
661
662 const char *known_hosts_path = get_known_hosts_path();
663
664 // Format IP:port with proper bracket notation for IPv6
665 char ip_with_port[BUFFER_SIZE_MEDIUM];
666 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
667 // Fallback to basic format if error
668 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
669 }
670
671 char escaped_ip_with_port[128];
672 escape_ascii(ip_with_port, "[]", escaped_ip_with_port, 128);
673 log_warn("\n"
674 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
675 "@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n"
676 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
677 "\n"
678 "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"
679 "Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n"
680 "It is also possible that the host key has just been changed.\n"
681 "\n"
682 "The fingerprint for the Ed25519 key sent by the remote host is:\n"
683 "SHA256:%s\n"
684 "\n"
685 "Expected fingerprint:\n"
686 "SHA256:%s\n"
687 "\n"
688 "Please contact your system administrator.\n"
689 "\n"
690 "Add correct host key in %s to get rid of this message.\n"
691 "Offending key for IP address %s was found at:\n"
692 "%s\n"
693 "\n"
694 "To update the key, run:\n"
695 " # Linux/macOS:\n"
696 " sed -i '' '/%s /d' ~/.ascii-chat/known_hosts\n"
697 " # or run this instead:\n"
698 " cat ~/.ascii-chat/known_hosts | grep -v '%s ' > /tmp/x; cp /tmp/x ~/.ascii-chat/known_hosts\n"
699 " # Windows PowerShell:\n"
700 " (Get-Content ~/.ascii-chat/known_hosts) | Where-Object { $_ -notmatch '^%s ' } | Set-Content "
701 "~/.ascii-chat/known_hosts\n"
702 " # Or manually edit ~/.ascii-chat/known_hosts to remove lines starting with '%s '\n"
703 "\n"
704 "Host key verification failed.\n"
705 "\n",
706 received_fp, expected_fp, known_hosts_path, ip_with_port, known_hosts_path, ip_with_port,
707 escaped_ip_with_port, ip_with_port, ip_with_port);
708
709 return false;
710}
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 util/string.c:20
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.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 428 of file pem.c.

428 {
429 if (!ta) {
430 return;
431 }
432 xfree(ta->dn.data);
433 switch (ta->pkey.key_type) {
434 case BR_KEYTYPE_RSA:
435 xfree((void *)ta->pkey.key.rsa.n);
436 xfree((void *)ta->pkey.key.rsa.e);
437 break;
438 case BR_KEYTYPE_EC:
439 xfree((void *)ta->pkey.key.ec.q);
440 break;
441 default:
442 SET_ERRNO(ERROR_CRYPTO, "Unknown public key type in CA");
443 break;
444 }
445}

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_destroy().
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 47 of file known_hosts.c.

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

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

Referenced by add_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 96 of file agent.c.

96 {
97 char agent_path[PLATFORM_MAX_PATH_LENGTH];
98 if (platform_get_gpg_agent_socket(agent_path, sizeof(agent_path)) != 0) {
99 log_error("Failed to get GPG agent path");
100 return -1;
101 }
102
103 log_debug("Connecting to GPG agent at: %s", agent_path);
104
105 // Use platform abstraction for pipe/socket connection
106 pipe_t pipe = platform_pipe_connect(agent_path);
107 if (!platform_pipe_is_valid(pipe)) {
108 log_error("Failed to connect to GPG agent");
109 return -1;
110 }
111
112 // Read initial greeting
113 char response[GPG_AGENT_MAX_RESPONSE];
114 if (read_agent_line(pipe, response, sizeof(response)) != 0) {
115 log_error("Failed to read GPG agent greeting");
117 return -1;
118 }
119
120 if (!is_ok_response(response)) {
121 log_error("Unexpected GPG agent greeting: %s", response);
123 return -1;
124 }
125
126 log_debug("Connected to GPG agent successfully");
127
128 // Set loopback pinentry mode to avoid interactive prompts
129 // This allows GPG agent to work in non-interactive environments
130 if (send_agent_command(pipe, "OPTION pinentry-mode=loopback") != 0) {
131 log_warn("Failed to set loopback pinentry mode (continuing anyway)");
132 } else {
133 // Read response for OPTION command
134 if (read_agent_line(pipe, response, sizeof(response)) != 0) {
135 log_warn("Failed to read OPTION command response (continuing anyway)");
136 } else if (is_ok_response(response)) {
137 log_debug("Loopback pinentry mode enabled");
138 } else {
139 log_warn("Failed to enable loopback pinentry mode: %s (continuing anyway)", response);
140 }
141 }
142
143 return (int)(intptr_t)pipe;
144}
#define GPG_AGENT_MAX_RESPONSE
Definition agent.c:30
pipe_t platform_pipe_connect(const char *path)
Connect to an agent via named pipe (Windows) or Unix socket (POSIX)
int platform_get_gpg_agent_socket(char *path_out, size_t path_size)
int pipe_t
Pipe handle type (POSIX: int file descriptor)
Definition pipe.h:40
int platform_pipe_close(pipe_t pipe)
Close a pipe connection.
bool platform_pipe_is_valid(pipe_t pipe)
Check if a pipe handle is valid.
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69

References GPG_AGENT_MAX_RESPONSE, log_debug, log_error, log_warn, platform_get_gpg_agent_socket(), PLATFORM_MAX_PATH_LENGTH, platform_pipe_close(), platform_pipe_connect(), and platform_pipe_is_valid().

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 146 of file agent.c.

146 {
147 if (handle_as_int >= 0) {
148 pipe_t pipe = (pipe_t)(intptr_t)handle_as_int;
149 send_agent_command(pipe, "BYE");
151 }
152}

References platform_pipe_close().

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 343 of file agent.c.

343 {
344 int sock = gpg_agent_connect();
345 if (sock < 0) {
346 return false;
347 }
349 return true;
350}
int gpg_agent_connect(void)
Connect to gpg-agent.
Definition agent.c:96
void gpg_agent_disconnect(int handle_as_int)
Disconnect from gpg-agent.
Definition agent.c:146

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 154 of file agent.c.

155 {
156 if (handle_as_int < 0 || !keygrip || !message || !signature_out || !signature_len_out) {
157 log_error("Invalid arguments to gpg_agent_sign");
158 return -1;
159 }
160
161 pipe_t handle = (pipe_t)(intptr_t)handle_as_int;
162 char response[GPG_AGENT_MAX_RESPONSE];
163
164 // 1. Set the key to use (SIGKEY command)
165 char sigkey_cmd[128];
166 safe_snprintf(sigkey_cmd, sizeof(sigkey_cmd), "SIGKEY %s", keygrip);
167 if (send_agent_command(handle, sigkey_cmd) != 0) {
168 log_error("Failed to send SIGKEY command");
169 return -1;
170 }
171
172 if (read_agent_line(handle, response, sizeof(response)) != 0) {
173 log_error("Failed to read SIGKEY response");
174 return -1;
175 }
176
177 if (!is_ok_response(response)) {
178 log_error("SIGKEY failed: %s", response);
179 return -1;
180 }
181
182 // 2. For EdDSA/Ed25519, GPG agent requires SETHASH with a hash algorithm
183 // GPG agent doesn't support --inquire for SETHASH - the command syntax is:
184 // SETHASH (--hash=<name>)|(<algonumber>) <hexstring>
185 // For Ed25519, we hash the message with SHA512 (algo 10) first
186
187 // Hash the message with SHA512 using libsodium
188 uint8_t hash[crypto_hash_sha512_BYTES];
189 crypto_hash_sha512(hash, message, message_len);
190
191 // Build SETHASH command with SHA512 hash (algo 10)
192 // Format: "SETHASH 10 <128 hex chars for 64-byte SHA512 hash>"
193 char sethash_cmd[256];
194 int offset = safe_snprintf(sethash_cmd, sizeof(sethash_cmd), "SETHASH 10 ");
195 for (size_t i = 0; i < crypto_hash_sha512_BYTES; i++) {
196 offset += safe_snprintf(sethash_cmd + offset, sizeof(sethash_cmd) - (size_t)offset, "%02X", hash[i]);
197 }
198
199 log_debug("Sending SETHASH command with SHA512 hash");
200 if (send_agent_command(handle, sethash_cmd) != 0) {
201 log_error("Failed to send SETHASH command");
202 return -1;
203 }
204
205 // Read SETHASH response
206 if (read_agent_line(handle, response, sizeof(response)) != 0) {
207 log_error("Failed to read SETHASH response");
208 return -1;
209 }
210
211 if (!is_ok_response(response)) {
212 log_error("SETHASH failed: %s", response);
213 return -1;
214 }
215
216 // 3. Request signature using PKSIGN
217 if (send_agent_command(handle, "PKSIGN") != 0) {
218 log_error("Failed to send PKSIGN command");
219 return -1;
220 }
221
222 // Read response - skip status/error lines and wait for data line (D ...)
223 // GPG agent sends informational ERR lines that are not fatal (e.g., "Not implemented")
224 // Keep reading until we get the actual signature data
225 bool found_data = false;
226 for (int attempts = 0; attempts < 20; attempts++) {
227 if (read_agent_line(handle, response, sizeof(response)) != 0) {
228 log_error("Failed to read PKSIGN response");
229 return -1;
230 }
231
232 log_debug("PKSIGN response line %d: %s", attempts + 1, response);
233
234 // Skip status lines (S INQUIRE_MAXLEN, etc)
235 if (response[0] == 'S' && response[1] == ' ') {
236 log_debug("Skipping PKSIGN status line: %s", response);
237 continue;
238 }
239
240 // Skip informational ERR lines (GPG agent sends these even on success)
241 // Common ERR codes: 67109141 (IPC cancelled), 67108933 (Not implemented)
242 if (strncmp(response, "ERR", 3) == 0) {
243 log_debug("Skipping PKSIGN error line (informational): %s", response);
244 continue;
245 }
246
247 // Check if it's a data line (D followed by space)
248 if (response[0] == 'D' && response[1] == ' ') {
249 log_debug("Found signature data line");
250 found_data = true;
251 break;
252 }
253
254 // Check for OK (success without data would be unexpected)
255 if (strncmp(response, "OK", 2) == 0) {
256 log_warn("PKSIGN returned OK without data line");
257 continue; // Keep trying in case D line follows
258 }
259
260 // Check if GPG agent is sending another INQUIRE (shouldn't happen)
261 if (strncmp(response, "INQUIRE", 7) == 0) {
262 log_error("Unexpected INQUIRE after PKSIGN: %s", response);
263 return -1;
264 }
265
266 // Unknown response type
267 log_warn("Unexpected PKSIGN response (attempt %d): %s", attempts + 1, response);
268 }
269
270 if (!found_data) {
271 log_error("Expected D line from PKSIGN after %d attempts", 20);
272 return -1;
273 }
274
275 // Parse S-expression signature from GPG agent
276 // GPG agent returns: D <percent-encoded-sexp>
277 // Example: D (7:sig-val(5:eddsa(1:r32:%<hex>)(1:s32:%<hex>)))
278 // The signature is 64 bytes total: R (32) + S (32)
279
280 // DEBUG: Print first 200 chars of response to see format
281 char debug_buf[201];
282 size_t response_len = strlen(response);
283 size_t debug_len = response_len < 200 ? response_len : 200;
284 memcpy(debug_buf, response, debug_len);
285 debug_buf[debug_len] = '\0';
286 log_debug("GPG agent D line (first 200 bytes): %s", debug_buf);
287
288 const char *data = response + 2; // Skip "D "
289
290 // The response format from GPG agent for EdDSA is percent-encoded
291 // We need to decode it to get the raw binary signature
292 // For now, let's try the simple approach: find the raw data
293
294 // Look for the pattern that indicates where R starts: "(1:r32:"
295 const char *r_marker = strstr(data, "(1:r32:");
296 if (!r_marker) {
297 log_error("Could not find r value marker in S-expression");
298 return -1;
299 }
300
301 // Skip the marker to get to the actual R data
302 const char *r_data = r_marker + 7; // strlen("(1:r32:")
303
304 // Look for the pattern that indicates where S starts: "(1:s32:"
305 const char *s_marker = strstr(r_data + 32, "(1:s32:");
306 if (!s_marker) {
307 log_error("Could not find s value marker in S-expression");
308 return -1;
309 }
310
311 // Skip the marker to get to the actual S data
312 const char *s_data = s_marker + 7; // strlen("(1:s32:")
313
314 // Copy the raw binary data
315 memcpy(signature_out, r_data, 32);
316 memcpy(signature_out + 32, s_data, 32);
317
318 *signature_len_out = 64;
319
320 // DEBUG: Print signature in hex
321 char sig_hex[129];
322 for (int i = 0; i < 64; i++) {
323 safe_snprintf(sig_hex + i * 2, 3, "%02x", (unsigned char)signature_out[i]);
324 }
325 sig_hex[128] = '\0';
326 log_debug("Extracted signature (64 bytes): %s", sig_hex);
327
328 // Read final OK
329 if (read_agent_line(handle, response, sizeof(response)) != 0) {
330 log_error("Failed to read final PKSIGN response");
331 return -1;
332 }
333
334 if (!is_ok_response(response)) {
335 log_error("PKSIGN final response not OK: %s", response);
336 return -1;
337 }
338
339 log_debug("Successfully signed message with GPG agent");
340 return 0;
341}

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 248 of file export.c.

248 {
249 if (!key_id || !public_key_out) {
250 log_error("Invalid arguments to gpg_get_public_key");
251 return -1;
252 }
253
254 // SECURITY: Validate key_id to prevent command injection
255 // GPG key IDs should be hexadecimal (0-9, a-f, A-F)
256 if (!validate_shell_safe(key_id, NULL)) {
257 log_error("Invalid GPG key ID format - contains unsafe characters: %s", key_id);
258 return -1;
259 }
260
261 // Additional validation: ensure key_id is hex alphanumeric
262 for (size_t i = 0; key_id[i] != '\0'; i++) {
263 if (!isxdigit((unsigned char)key_id[i])) {
264 log_error("Invalid GPG key ID format - must be hexadecimal: %s", key_id);
265 return -1;
266 }
267 }
268
269 // Escape key_id for safe use in shell command (single quotes)
270 char escaped_key_id[BUFFER_SIZE_MEDIUM];
271 if (!escape_shell_single_quotes(key_id, escaped_key_id, sizeof(escaped_key_id))) {
272 log_error("Failed to escape GPG key ID for shell command");
273 return -1;
274 }
275
276 // Use gpg to list the key and get the keygrip
277 char cmd[BUFFER_SIZE_LARGE];
278 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-keygrip --with-colons 0x%s " PLATFORM_SHELL_NULL_REDIRECT,
279 escaped_key_id);
280
281 FILE *fp = NULL;
282 if (platform_popen("gpg_export", cmd, "r", &fp) != ASCIICHAT_OK || !fp) {
283 log_error("Failed to run gpg command - GPG may not be installed");
284#ifdef _WIN32
285 log_error("To install GPG on Windows, download Gpg4win from:");
286 log_error(" https://www.gpg4win.org/download.html");
287#elif defined(__APPLE__)
288 log_error("To install GPG on macOS, use Homebrew:");
289 log_error(" brew install gnupg");
290#else
291 log_error("To install GPG on Linux:");
292 log_error(" Debian/Ubuntu: sudo apt-get install gnupg");
293 log_error(" Fedora/RHEL: sudo dnf install gnupg2");
294 log_error(" Arch Linux: sudo pacman -S gnupg");
295 log_error(" Alpine Linux: sudo apk add gnupg");
296#endif
297 return -1;
298 }
299
300 char line[BUFFER_SIZE_XLARGE];
301 char found_keygrip[128] = {0};
302 bool found_key = false;
303
304 // Parse gpg output
305 // Format: pub:..., grp:::::::::<keygrip>:
306 while (fgets(line, sizeof(line), fp)) {
307 if (strncmp(line, "pub:", 4) == 0) {
308 // Found the public key line
309 found_key = true;
310 } else if (found_key && strncmp(line, "grp:", 4) == 0) {
311 // Extract keygrip using PCRE2 regex
312 // Format: grp:::::::::D52FF935FBA59609EE65E1685287828242A1EA1A:
313 char *keygrip_extracted = NULL;
314
315 if (crypto_regex_extract_gpg_keygrip(line, &keygrip_extracted)) {
316 // Successfully extracted keygrip
317 SAFE_STRNCPY(found_keygrip, keygrip_extracted, sizeof(found_keygrip));
318 if (keygrip_out) {
319 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
320 }
321 SAFE_FREE(keygrip_extracted);
322 } else {
323 // Fallback to manual parsing if regex fails
324 const char *grp_start = line + 4;
325 int colon_count = 0;
326 while (*grp_start && colon_count < 8) {
327 if (*grp_start == ':') {
328 colon_count++;
329 }
330 grp_start++;
331 }
332
333 if (colon_count == 8) {
334 const char *grp_end = strchr(grp_start, ':');
335 if (grp_end) {
336 size_t grp_len = grp_end - grp_start;
337 if (grp_len < sizeof(found_keygrip)) {
338 memcpy(found_keygrip, grp_start, grp_len);
339 found_keygrip[grp_len] = '\0';
340
341 if (keygrip_out) {
342 SAFE_STRNCPY(keygrip_out, found_keygrip, 41);
343 }
344 }
345 }
346 }
347 }
348 break;
349 }
350 }
351
352 platform_pclose(&fp);
353
354 if (!found_key || strlen(found_keygrip) == 0) {
355 log_error("Could not find GPG key with ID: %s", key_id);
356 return -1;
357 }
358
359 log_debug("Found keygrip for key %s: %s", key_id, found_keygrip);
360
361 // Try to use GPG agent API to read the public key directly via READKEY command
362 int agent_sock = gpg_agent_connect();
363 if (agent_sock < 0) {
364 log_debug("GPG agent not available, falling back to gpg --export for public key extraction");
365 // Fallback: Use gpg --export to get the public key
366 int export_result = gpg_export_public_key(key_id, public_key_out);
367 if (export_result == 0) {
368 log_debug("Successfully extracted public key using fallback method");
369 } else {
370 log_error("Fallback public key extraction failed for key ID: %s", key_id);
371 }
372 return export_result;
373 }
374
375 // Send READKEY command with keygrip to get the public key S-expression
376 char readkey_cmd[BUFFER_SIZE_SMALL];
377 safe_snprintf(readkey_cmd, sizeof(readkey_cmd), "READKEY %s\n", found_keygrip);
378
379 // Cast agent_sock back to pipe_t (gpg_agent_connect returns pipe_t cast to int for portability)
380 pipe_t agent_pipe = (pipe_t)(intptr_t)agent_sock;
381 ssize_t bytes_written = platform_pipe_write(agent_pipe, (const unsigned char *)readkey_cmd, strlen(readkey_cmd));
382 if (bytes_written != (ssize_t)strlen(readkey_cmd)) {
383 log_error("Failed to send READKEY command to GPG agent");
384 gpg_agent_disconnect(agent_sock);
385 return -1;
386 }
387
388 // Read the response (public key S-expression)
389 char response[BUFFER_SIZE_XXXLARGE];
390 memset(response, 0, sizeof(response));
391 ssize_t bytes_read = platform_pipe_read(agent_pipe, (unsigned char *)response, sizeof(response) - 1);
392
393 gpg_agent_disconnect(agent_sock);
394
395 if (bytes_read <= 0) {
396 log_error("Failed to read READKEY response from GPG agent");
397 return -1;
398 }
399
400 // Parse the S-expression to extract Ed25519 public key (q value)
401 // GPG agent returns binary S-expressions in format: (1:q<length>:<binary-data>)
402 // Example: (1:q33:<33-bytes>) where first byte is 0x40 (Ed25519 prefix), then 32-byte key
403 const char *q_marker = strstr(response, "(1:q");
404 if (!q_marker) {
405 log_warn("Failed to find public key (1:q) in GPG agent READKEY response, trying gpg --export fallback");
406 log_debug("Response was: %.*s", (int)(bytes_read < 200 ? bytes_read : 200), response);
407 gpg_agent_disconnect(agent_sock);
408
409 // Fallback: Use gpg --export for public-only keys
410 int export_result = gpg_export_public_key(key_id, public_key_out);
411 if (export_result == 0) {
412 log_debug("Successfully extracted public key using gpg --export fallback");
413 } else {
414 log_error("Fallback public key extraction failed for key ID: %s", key_id);
415 }
416 return export_result;
417 }
418
419 // Skip "(1:q" to get to the length field
420 const char *len_start = q_marker + 4;
421
422 // Parse the length (e.g., "33:")
423 char *colon = strchr(len_start, ':');
424 if (!colon) {
425 log_error("Malformed S-expression: missing colon after length");
426 return -1;
427 }
428
429 size_t key_len = strtoul(len_start, NULL, 10);
430 if (key_len != 33) {
431 log_error("Unexpected Ed25519 public key length: %zu bytes (expected 33)", key_len);
432 return -1;
433 }
434
435 // Skip the colon to get to the binary data
436 const unsigned char *binary_start = (const unsigned char *)(colon + 1);
437
438 // Ed25519 public keys in GPG format have a 0x40 prefix byte, then 32 bytes of actual key
439 if (binary_start[0] != 0x40) {
440 log_error("Invalid Ed25519 public key prefix: 0x%02x (expected 0x40)", binary_start[0]);
441 return -1;
442 }
443
444 // Copy the 32-byte public key (skip the 0x40 prefix)
445 memcpy(public_key_out, binary_start + 1, 32);
446
447 log_debug("Extracted Ed25519 public key from GPG agent via READKEY command");
448 return 0;
449}
#define BUFFER_SIZE_XXXLARGE
Extra extra extra large buffer size (8192 bytes)
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes)
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
asciichat_error_t platform_popen(const char *name, const char *command, const char *mode, FILE **out_stream)
Execute a command and return a file stream for reading/writing.
ssize_t platform_pipe_read(pipe_t pipe, void *buf, size_t len)
Read data from a pipe.
#define PLATFORM_SHELL_NULL_REDIRECT
Shell null device/error redirect for platform.
ssize_t platform_pipe_write(pipe_t pipe, const void *buf, size_t len)
Write data to a pipe.
asciichat_error_t platform_pclose(FILE **stream_ptr)
Close a process stream opened with platform_popen()
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)
bool validate_shell_safe(const char *str, const char *allowed_chars)
Validate that a string contains only safe characters for shell commands.
Definition util/string.c:62
atomic_t bytes_written
Definition mmap.c:42
bool crypto_regex_extract_gpg_keygrip(const char *line, char **keygrip_out)
Definition regex.c:262

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_SMALL, BUFFER_SIZE_XLARGE, BUFFER_SIZE_XXXLARGE, bytes_written, crypto_regex_extract_gpg_keygrip(), escape_shell_single_quotes(), gpg_agent_connect(), gpg_agent_disconnect(), log_debug, log_error, log_warn, platform_pclose(), platform_pipe_read(), platform_pipe_write(), platform_popen(), PLATFORM_SHELL_NULL_REDIRECT, SAFE_FREE, 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 140 of file signing.c.

141 {
142 log_debug("gpg_sign_detached_ed25519: Signing with key ID %s (fallback mode)", key_id);
143
144 // Get OpenPGP signature packet from gpg --detach-sign
145 uint8_t openpgp_signature[512];
146 size_t openpgp_len = 0;
147
148 int result = gpg_sign_with_key(key_id, message, message_len, openpgp_signature, &openpgp_len);
149 if (result != 0) {
150 log_error("GPG detached signing failed for key %s", key_id);
151 return -1;
152 }
153
154 log_debug("gpg_sign_with_key returned %zu bytes", openpgp_len);
155
156 if (openpgp_len < 10) {
157 log_error("GPG signature too short: %zu bytes", openpgp_len);
158 return -1;
159 }
160
161 log_debug("Parsing OpenPGP signature packet (%zu bytes) to extract Ed25519 signature", openpgp_len);
162
163 // Parse OpenPGP signature packet format
164 // Reference: RFC 4880 Section 5.2 (Signature Packet)
165 // Format: [header][version][type][algo][hash-algo][...][signature-data]
166 size_t offset = 0;
167
168 // Parse packet header
169 uint8_t tag = openpgp_signature[offset++];
170 size_t packet_len = 0;
171
172 if ((tag & 0x40) == 0) {
173 // Old format packet
174 uint8_t length_type = tag & 0x03;
175 if (length_type == 0) {
176 packet_len = openpgp_signature[offset++];
177 } else if (length_type == 1) {
178 packet_len = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
179 offset += 2;
180 } else if (length_type == 2) {
181 packet_len = (openpgp_signature[offset] << 24) | (openpgp_signature[offset + 1] << 16) |
182 (openpgp_signature[offset + 2] << 8) | openpgp_signature[offset + 3];
183 offset += 4;
184 } else {
185 log_error("Unsupported old-format packet length type: %d", length_type);
186 return -1;
187 }
188 } else {
189 // New format packet
190 uint8_t length_byte = openpgp_signature[offset++];
191 if (length_byte < 192) {
192 packet_len = length_byte;
193 } else if (length_byte < 224) {
194 packet_len = ((length_byte - 192) << 8) + openpgp_signature[offset++] + 192;
195 } else if (length_byte == 255) {
196 packet_len = (openpgp_signature[offset] << 24) | (openpgp_signature[offset + 1] << 16) |
197 (openpgp_signature[offset + 2] << 8) | openpgp_signature[offset + 3];
198 offset += 4;
199 } else {
200 log_error("Unsupported new-format packet length encoding: %d", length_byte);
201 return -1;
202 }
203 }
204
205 if (offset + packet_len > openpgp_len) {
206 log_error("Packet length exceeds signature size: %zu + %zu > %zu", offset, packet_len, openpgp_len);
207 return -1;
208 }
209
210 log_debug("Signature packet: offset=%zu, length=%zu", offset, packet_len);
211
212 // Parse signature packet body
213 // Skip: version (1), sig_type (1), pub_algo (1), hash_algo (1)
214 if (offset + 4 > openpgp_len) {
215 log_error("Signature packet too short for header");
216 return -1;
217 }
218
219 uint8_t version = openpgp_signature[offset++];
220 uint8_t sig_type = openpgp_signature[offset++];
221 uint8_t pub_algo = openpgp_signature[offset++];
222 uint8_t hash_algo = openpgp_signature[offset++];
223
224 log_debug("Signature: version=%d, type=%d, algo=%d, hash=%d", version, sig_type, pub_algo, hash_algo);
225
226 // Verify algorithm is Ed25519 (22 = EdDSA)
227 if (pub_algo != 22) {
228 log_error("Expected EdDSA algorithm (22), got %d", pub_algo);
229 return -1;
230 }
231
232 // For v4 signatures: skip hashed subpackets
233 if (version == 4) {
234 if (offset + 2 > openpgp_len) {
235 log_error("Cannot read hashed subpacket length");
236 return -1;
237 }
238 uint16_t hashed_len = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
239 offset += 2;
240 offset += hashed_len; // Skip hashed subpackets
241
242 if (offset + 2 > openpgp_len) {
243 log_error("Cannot read unhashed subpacket length");
244 return -1;
245 }
246 uint16_t unhashed_len = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
247 offset += 2;
248 offset += unhashed_len; // Skip unhashed subpackets
249
250 // Skip left 16 bits of signed hash value
251 if (offset + 2 > openpgp_len) {
252 log_error("Cannot read hash left bits");
253 return -1;
254 }
255 offset += 2;
256 }
257
258 // Now we're at the signature data (MPI format for Ed25519)
259 // Ed25519 signature is: r (32 bytes) || s (32 bytes) = 64 bytes total
260 // In OpenPGP, each MPI is encoded as: [2-byte bit count][data]
261
262 if (offset + 2 > openpgp_len) {
263 log_error("Cannot read MPI bit count for R");
264 return -1;
265 }
266
267 uint16_t r_bits = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
268 offset += 2;
269 size_t r_bytes = (r_bits + 7) / 8;
270
271 log_debug("R: %d bits (%zu bytes)", r_bits, r_bytes);
272
273 if (r_bytes != 32) {
274 log_error("Expected 32-byte R value, got %zu bytes", r_bytes);
275 return -1;
276 }
277
278 if (offset + r_bytes > openpgp_len) {
279 log_error("R value exceeds packet size");
280 return -1;
281 }
282
283 memcpy(signature_out, &openpgp_signature[offset], 32);
284 offset += r_bytes;
285
286 // Read S value
287 if (offset + 2 > openpgp_len) {
288 log_error("Cannot read MPI bit count for S");
289 return -1;
290 }
291
292 uint16_t s_bits = (openpgp_signature[offset] << 8) | openpgp_signature[offset + 1];
293 offset += 2;
294 size_t s_bytes = (s_bits + 7) / 8;
295
296 log_debug("S: %d bits (%zu bytes)", s_bits, s_bytes);
297
298 if (s_bytes != 32) {
299 log_error("Expected 32-byte S value, got %zu bytes", s_bytes);
300 return -1;
301 }
302
303 if (offset + s_bytes > openpgp_len) {
304 log_error("S value exceeds packet size");
305 return -1;
306 }
307
308 memcpy(signature_out + 32, &openpgp_signature[offset], 32);
309
310 log_debug("Successfully extracted 64-byte Ed25519 signature from OpenPGP packet");
311
312 // Debug: Log signature components
313 char hex_r[65], hex_s[65];
314 for (int i = 0; i < 32; i++) {
315 safe_snprintf(hex_r + i * 2, 3, "%02x", signature_out[i]);
316 safe_snprintf(hex_s + i * 2, 3, "%02x", signature_out[i + 32]);
317 }
318 hex_r[64] = hex_s[64] = '\0';
319 log_debug("Signature R (first 32 bytes): %s", hex_r);
320 log_debug("Signature S (last 32 bytes): %s", hex_s);
321
322 return 0;
323}
unsigned short uint16_t
Definition common.h:57
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:41

References gpg_sign_with_key(), log_debug, log_error, 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 41 of file signing.c.

42 {
43 if (!key_id || !message || message_len == 0 || !signature_out || !signature_len_out) {
44 log_error("Invalid parameters to gpg_sign_with_key");
45 return -1;
46 }
47
48 char msg_path[PLATFORM_MAX_PATH_LENGTH];
49 char sig_path[PLATFORM_MAX_PATH_LENGTH];
50 int msg_fd = -1;
51 int sig_fd = -1;
52 int result = -1;
53
54 // Create temp files using platform abstraction
55 if (platform_create_temp_file(msg_path, sizeof(msg_path), "asciichat_msg", &msg_fd) != 0) {
56 log_error("Failed to create temp message file");
57 return -1;
58 }
59
60 if (platform_create_temp_file(sig_path, sizeof(sig_path), "asciichat_sig", &sig_fd) != 0) {
61 log_error("Failed to create temp signature file");
63 return -1;
64 }
65
66 // Close signature file descriptor (will be created by gpg)
67 if (sig_fd >= 0) {
68 close(sig_fd);
69 }
70 platform_delete_temp_file(sig_path); // Remove it so gpg can create it fresh
71
72 // Write message to temp file
73 ssize_t written = write(msg_fd, message, message_len);
74 close(msg_fd);
75 msg_fd = -1;
76
77 if (written != (ssize_t)message_len) {
78 log_error("Failed to write message to temp file");
79 goto cleanup;
80 }
81
82 // Escape key ID for shell command (prevent injection)
83 char escaped_key_id[64];
84 if (!escape_path_for_shell(key_id, escaped_key_id, sizeof(escaped_key_id))) {
85 log_error("Failed to escape GPG key ID for shell command");
86 goto cleanup;
87 }
88
89 // Call gpg --detach-sign
90 // Use gpg with --detach-sign and --output for safe subprocess execution
91 const char *argv[] = {"gpg", "--local-user", escaped_key_id, "--detach-sign", "--output", sig_path, msg_path, NULL};
92
93 // Note: Don't log the full command as it may contain sensitive key material
94 log_debug("Signing with GPG key 0x%s", escaped_key_id);
95 int status = 0;
96 LOG_IO("gpg", { status = platform_execute_subprocess("gpg", argv, NULL, 0); });
97 if (status != 0) {
98 log_error("GPG signing failed (exit code %d)", status);
99 goto cleanup;
100 }
101
102 // Read signature file
103 FILE *sig_fp = platform_fopen("file_stream", sig_path, "rb");
104 if (!sig_fp) {
105 log_error("Failed to open signature file: %s", SAFE_STRERROR(errno));
106 goto cleanup;
107 }
108
109 fseek(sig_fp, 0, SEEK_END);
110 long sig_size = ftell(sig_fp);
111 fseek(sig_fp, 0, SEEK_SET);
112
113 if (sig_size <= 0 || sig_size > 512) {
114 log_error("Invalid signature size: %ld bytes", sig_size);
115 fclose(sig_fp);
116 goto cleanup;
117 }
118
119 size_t bytes_read = fread(signature_out, 1, sig_size, sig_fp);
120 fclose(sig_fp);
121
122 if (bytes_read != (size_t)sig_size) {
123 log_error("Failed to read signature file");
124 goto cleanup;
125 }
126
127 *signature_len_out = sig_size;
128 log_debug("GPG signature created successfully (%zu bytes)", *signature_len_out);
129 result = 0;
130
131cleanup:
132 if (msg_fd >= 0) {
133 close(msg_fd);
134 }
137 return result;
138}
#define SAFE_STRERROR(errnum)
Definition common.h:465
int platform_create_temp_file(char *path_out, size_t path_size, const char *prefix, int *fd)
Create a temporary file with a given prefix.
int platform_execute_subprocess(const char *executable, const char **argv, char *output_buffer, size_t output_size)
Execute a subprocess and optionally capture its output.
int errno
int platform_delete_temp_file(const char *path)
Delete a temporary file.
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)
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81

References errno, escape_path_for_shell(), log_debug, log_error, LOG_IO, platform_create_temp_file(), platform_delete_temp_file(), platform_execute_subprocess(), platform_fopen(), PLATFORM_MAX_PATH_LENGTH, 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 31 of file verification.c.

32 {
33 // Note: We don't use the raw signature parameter directly.
34 // Instead, we regenerate the OpenPGP signature using GPG (Ed25519 is deterministic).
35 (void)signature;
36
37 log_debug("gpg_verify_detached_ed25519: Verifying signature with key ID %s using gpg --verify", key_id);
38
39 // To verify with GPG, we need to:
40 // 1. Reconstruct the OpenPGP signature packet from the raw R||S signature
41 // 2. Write message and signature to temp files
42 // 3. Call gpg --verify
43
44 // First, reconstruct OpenPGP signature by signing the same message
45 // Since Ed25519 is deterministic, we should get the same OpenPGP packet
46 uint8_t openpgp_signature[512];
47 size_t openpgp_len = 0;
48
49 int sign_result = gpg_sign_with_key(key_id, message, message_len, openpgp_signature, &openpgp_len);
50 if (sign_result != 0) {
51 log_error("Failed to create reference signature for verification");
52 return -1;
53 }
54
55 // Now verify using gpg --verify
56 char msg_path[PLATFORM_MAX_PATH_LENGTH];
57 char sig_path[PLATFORM_MAX_PATH_LENGTH];
58 int msg_fd = -1;
59 int sig_fd = -1;
60
61 if (platform_create_temp_file(msg_path, sizeof(msg_path), "gpg_verify_msg", &msg_fd) != 0) {
62 log_error("Failed to create temporary message file");
63 return -1;
64 }
65
66 if (platform_create_temp_file(sig_path, sizeof(sig_path), "gpg_verify_sig", &sig_fd) != 0) {
68 log_error("Failed to create temporary signature file");
69 return -1;
70 }
71
72 // Write message
73 if (write(msg_fd, message, message_len) != (ssize_t)message_len) {
74 log_error("Failed to write message to temp file");
75 close(msg_fd);
76 close(sig_fd);
79 return -1;
80 }
81 close(msg_fd);
82
83 // Write OpenPGP signature
84 if (write(sig_fd, openpgp_signature, openpgp_len) != (ssize_t)openpgp_len) {
85 log_error("Failed to write signature to temp file");
86 close(sig_fd);
89 return -1;
90 }
91 close(sig_fd);
92
93 // Call gpg --verify
94 char cmd[1024];
95 safe_snprintf(cmd, sizeof(cmd), "gpg --verify '%s' '%s' 2>&1", sig_path, msg_path);
96 log_debug("Running: %s", cmd);
97
98 FILE *fp;
99 platform_popen("gpg_verify", cmd, "r", &fp);
100 if (!fp) {
101 log_error("Failed to run gpg --verify");
102 platform_unlink(msg_path);
103 platform_unlink(sig_path);
104 return -1;
105 }
106
107 char output[4096] = {0};
108 size_t output_len = fread(output, 1, sizeof(output) - 1, fp);
109 int exit_code = platform_pclose(&fp);
110
111 // Cleanup temp files
114
115 if (exit_code == 0) {
116 log_debug("GPG signature verification PASSED");
117 return 0;
118 } else {
119 log_error("GPG signature verification FAILED (exit code %d)", exit_code);
120 if (output_len > 0) {
121 log_debug("GPG output: %s", output);
122 }
123 return -1;
124 }
125}
int platform_unlink(const char *pathname)
Delete/unlink file.

References gpg_sign_with_key(), log_debug, log_error, platform_create_temp_file(), platform_delete_temp_file(), PLATFORM_MAX_PATH_LENGTH, platform_pclose(), platform_popen(), platform_unlink(), and safe_snprintf().

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 127 of file verification.c.

128 {
129#ifdef HAVE_LIBGCRYPT
130 gcry_error_t err;
131 gcry_sexp_t s_pubkey = NULL;
132 gcry_sexp_t s_sig = NULL;
133 gcry_sexp_t s_data = NULL;
134
135 // Initialize libgcrypt if not already done
136 if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
137 gcry_check_version(NULL);
138 gcry_control(GCRYCTL_DISABLE_SECMEM, 0);
139 gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
140 }
141
142 // Build public key S-expression: (public-key (ecc (curve Ed25519) (flags eddsa) (q %b)))
143 // Must include (flags eddsa) to match libgcrypt's Ed25519 test suite.
144 // See libgcrypt/tests/t-ed25519.c line 246-251.
145 err = gcry_sexp_build(&s_pubkey, NULL, "(public-key (ecc (curve Ed25519) (flags eddsa) (q %b)))", 32, public_key);
146 if (err) {
147 log_error("gpg_verify_signature: Failed to build public key S-expression: %s", gcry_strerror(err));
148 return -1;
149 }
150
151 // Build signature S-expression: (sig-val (eddsa (r %b) (s %b)))
152 // Signature is 64 bytes: first 32 bytes are R, last 32 bytes are S
153 err = gcry_sexp_build(&s_sig, NULL, "(sig-val (eddsa (r %b) (s %b)))", 32, signature, 32, signature + 32);
154 if (err) {
155 log_error("gpg_verify_signature: Failed to build signature S-expression: %s", gcry_strerror(err));
156 gcry_sexp_release(s_pubkey);
157 return -1;
158 }
159
160 // Build data S-expression with raw message
161 // According to libgcrypt's test suite (t-ed25519.c line 273),
162 // Ed25519 data should be: (data (value %b)) with no flags.
163 // The (flags eddsa) belongs in the key S-expression above, not in the data.
164 // GPG agent's internal format is different - this is the correct libgcrypt API usage.
165 err = gcry_sexp_build(&s_data, NULL, "(data (value %b))", message_len, message);
166 if (err) {
167 log_error("gpg_verify_signature: Failed to build data S-expression: %s", gcry_strerror(err));
168 gcry_sexp_release(s_pubkey);
169 gcry_sexp_release(s_sig);
170 return -1;
171 }
172
173 // Debug logging
174 char pubkey_hex[65];
175 char r_hex[65];
176 char s_hex[65];
177 char msg_hex[128];
178
179 for (int i = 0; i < 32; i++) {
180 safe_snprintf(pubkey_hex + i * 2, 3, "%02x", public_key[i]);
181 safe_snprintf(r_hex + i * 2, 3, "%02x", signature[i]);
182 safe_snprintf(s_hex + i * 2, 3, "%02x", signature[32 + i]);
183 }
184 for (size_t i = 0; i < (message_len < 32 ? message_len : 32); i++) {
185 safe_snprintf(msg_hex + i * 2, 3, "%02x", message[i]);
186 }
187
188 log_debug("gpg_verify_signature: pubkey=%s", pubkey_hex);
189 log_debug("gpg_verify_signature: R=%s", r_hex);
190 log_debug("gpg_verify_signature: S=%s", s_hex);
191 log_debug("gpg_verify_signature: msg=%s (len=%zu)", msg_hex, message_len);
192
193 // Verify the signature
194 err = gcry_pk_verify(s_sig, s_data, s_pubkey);
195
196 // Clean up S-expressions
197 gcry_sexp_release(s_pubkey);
198 gcry_sexp_release(s_sig);
199 gcry_sexp_release(s_data);
200
201 if (err) {
202 log_debug("gpg_verify_signature: Signature verification failed: %s", gcry_strerror(err));
203 return -1;
204 }
205
206 log_debug("gpg_verify_signature: Signature verified successfully");
207 return 0;
208#else
209 // Explicitly mark parameters as unused when libgcrypt is not available
210 (void)public_key;
211 (void)message;
212 (void)message_len;
213 (void)signature;
214 log_error("gpg_verify_signature: libgcrypt not available");
215 return -1;
216#endif
217}

References log_debug, log_error, and safe_snprintf().

◆ 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 219 of file verification.c.

220 {
221 // Validate inputs
222 if (!signature || signature_len == 0 || signature_len > 512) {
223 log_error("gpg_verify_signature_with_binary: Invalid signature (expected 1-512 bytes, got %zu)", signature_len);
224 return -1;
225 }
226 if (!message || message_len == 0) {
227 log_error("gpg_verify_signature_with_binary: Invalid message");
228 return -1;
229 }
230
231 // Create temporary files for signature and message
232 char sig_path[PLATFORM_MAX_PATH_LENGTH];
233 char msg_path[PLATFORM_MAX_PATH_LENGTH];
234 int sig_fd = -1;
235 int msg_fd = -1;
236 int result = -1;
237
238 // Create temp files using platform abstraction
239 if (platform_create_temp_file(sig_path, sizeof(sig_path), "asciichat_sig", &sig_fd) != 0) {
240 log_error("Failed to create signature temp file");
241 return -1;
242 }
243
244 if (platform_create_temp_file(msg_path, sizeof(msg_path), "asciichat_msg", &msg_fd) != 0) {
245 log_error("Failed to create message temp file");
247 return -1;
248 }
249
250#ifdef _WIN32
251 // Windows: Write to already-created files using CreateFileA
252 HANDLE sig_handle = CreateFileA(sig_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
253 if (sig_handle == INVALID_HANDLE_VALUE) {
254 log_error("Failed to open signature temp file: %lu", GetLastError());
257 return -1;
258 }
259
260 DWORD bytes_written;
261 if (!WriteFile(sig_handle, signature, (DWORD)signature_len, &bytes_written, NULL) || bytes_written != signature_len) {
262 log_error("Failed to write signature to temp file: %lu", GetLastError());
263 CloseHandle(sig_handle);
266 return -1;
267 }
268 CloseHandle(sig_handle);
269
270 HANDLE msg_handle = CreateFileA(msg_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
271 if (msg_handle == INVALID_HANDLE_VALUE) {
272 log_error("Failed to open message temp file: %lu", GetLastError());
275 return -1;
276 }
277
278 if (!WriteFile(msg_handle, message, (DWORD)message_len, &bytes_written, NULL) || bytes_written != message_len) {
279 log_error("Failed to write message to temp file: %lu", GetLastError());
280 CloseHandle(msg_handle);
283 return -1;
284 }
285 CloseHandle(msg_handle);
286
287#else
288 // Unix: Write to open file descriptors returned by platform_create_temp_file
289 ssize_t sig_written = write(sig_fd, signature, signature_len);
290 if (sig_written != (ssize_t)signature_len) {
291 log_error("Failed to write signature to temp file: %s", SAFE_STRERROR(errno));
292 close(sig_fd);
293 close(msg_fd);
296 return -1;
297 }
298 close(sig_fd);
299
300 ssize_t msg_written = write(msg_fd, message, message_len);
301 if (msg_written != (ssize_t)message_len) {
302 log_error("Failed to write message to temp file: %s", SAFE_STRERROR(errno));
303 close(msg_fd);
306 return -1;
307 }
308 close(msg_fd);
309#endif
310
311 // Build gpg --verify command
312 char cmd[BUFFER_SIZE_LARGE];
313#ifdef _WIN32
314 safe_snprintf(cmd, sizeof(cmd), "gpg --verify \"%s\" \"%s\" 2>&1", sig_path, msg_path);
315#else
316 safe_snprintf(cmd, sizeof(cmd), "gpg --verify '%s' '%s' 2>&1", sig_path, msg_path);
317#endif
318
319 log_debug("Running GPG verify command: %s", cmd);
320
321 // Execute gpg --verify command
322 FILE *fp;
323 platform_popen("gpg_verify", cmd, "r", &fp);
324 if (!fp) {
325 log_error("Failed to execute gpg --verify command");
326 goto cleanup;
327 }
328
329 // Parse output for "Good signature" and verify key ID
330 char line[BUFFER_SIZE_MEDIUM];
331 bool found_good_sig = false;
332 bool found_key_id = false;
333
334 while (fgets(line, sizeof(line), fp)) {
335 log_debug("GPG output: %s", line);
336
337 // Check for "Good signature"
338 if (strstr(line, "Good signature")) {
339 found_good_sig = true;
340 }
341
342 // Check if this line contains the expected key ID (GPG outputs key ID on separate line)
343 if (expected_key_id && strlen(expected_key_id) > 0) {
344 if (strstr(line, expected_key_id)) {
345 found_key_id = true;
346 log_debug("Found expected key ID in GPG output: %s", expected_key_id);
347 }
348 }
349
350 // Check for signature errors
351 if (strstr(line, "BAD signature")) {
352 log_error("GPG reports BAD signature");
353 platform_pclose(&fp);
354 fp = NULL;
355 goto cleanup;
356 }
357 }
358
359 // Check exit code
360 int status = platform_pclose(&fp);
361 fp = NULL;
362
363#ifdef _WIN32
364 int exit_code = status;
365#else
366 int exit_code = WEXITSTATUS(status);
367#endif
368
369 if (exit_code != 0) {
370 log_error("GPG verify failed with exit code: %d", exit_code);
371 goto cleanup;
372 }
373
374 if (!found_good_sig) {
375 log_error("GPG verify did not report 'Good signature'");
376 goto cleanup;
377 }
378
379 // If expected_key_id was provided, verify we found it in the output
380 if (expected_key_id && strlen(expected_key_id) > 0) {
381 if (!found_key_id) {
382 log_error("GPG signature key ID does not match expected key ID: %s", expected_key_id);
383 goto cleanup;
384 }
385 }
386
387 log_debug("GPG signature verified successfully via gpg --verify binary");
388 result = 0;
389
390cleanup:
391 // Clean up temp files
394
395 if (fp) {
396 platform_pclose(&fp);
397 }
398
399 return result;
400}

References BUFFER_SIZE_LARGE, BUFFER_SIZE_MEDIUM, bytes_written, errno, log_debug, log_error, platform_create_temp_file(), platform_delete_temp_file(), PLATFORM_MAX_PATH_LENGTH, platform_pclose(), platform_popen(), safe_snprintf(), and SAFE_STRERROR.

◆ known_hosts_destroy()

void known_hosts_destroy ( 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 829 of file known_hosts.c.

829 {
830 if (g_known_hosts_path_cache) {
831 SAFE_FREE(g_known_hosts_path_cache);
832 g_known_hosts_path_cache = NULL;
833 }
834}

References SAFE_FREE.

Referenced by asciichat_shared_destroy().

◆ openpgp_base64_decode()

asciichat_error_t openpgp_base64_decode ( const char *  base64,
size_t  base64_len,
uint8_t **  binary_out,
size_t *  binary_len 
)

#include <openpgp.h>

Decode PGP armored base64 data.

Parameters
base64Base64-encoded string
base64_lenLength of base64 string
binary_outOutput buffer for decoded binary data (allocated by function)
binary_lenOutput parameter for decoded data length
Returns
ASCIICHAT_OK on success, error code on failure

Decodes base64 data from PGP armored format:

  • Removes whitespace (newlines, spaces, tabs)
  • Decodes using libsodium's base64 decoder
  • Allocates output buffer (caller must free)
Note
Caller must free *binary_out using SAFE_FREE()

Definition at line 32 of file openpgp.c.

33 {
34 if (!base64 || !binary_out || !binary_len) {
35 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for base64 decode");
36 }
37
38 // Remove whitespace from base64 input (PGP armor has newlines)
39 char *clean_base64 = SAFE_MALLOC(base64_len + 1, char *);
40 char *clean_ptr = clean_base64;
41 for (size_t i = 0; i < base64_len; i++) {
42 if (base64[i] != '\n' && base64[i] != '\r' && base64[i] != ' ' && base64[i] != '\t') {
43 *clean_ptr++ = base64[i];
44 }
45 }
46 *clean_ptr = '\0';
47 size_t clean_len = (size_t)(clean_ptr - clean_base64);
48
49 // Allocate max possible output size
50 *binary_out = SAFE_MALLOC(clean_len, uint8_t *);
51
52 const char *end;
53 int result = sodium_base642bin(*binary_out, clean_len, clean_base64, clean_len, NULL, binary_len, &end,
54 sodium_base64_VARIANT_ORIGINAL);
55
56 SAFE_FREE(clean_base64);
57
58 if (result != 0) {
59 SAFE_FREE(*binary_out);
60 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode base64 PGP armored data");
61 }
62
63 return ASCIICHAT_OK;
64}
@ ERROR_CRYPTO_KEY
Definition error_codes.h:97

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, SAFE_FREE, SAFE_MALLOC, and SET_ERRNO.

Referenced by openpgp_parse_armored_pubkey(), and openpgp_parse_armored_seckey().

◆ openpgp_extract_ed25519_from_mpi()

asciichat_error_t openpgp_extract_ed25519_from_mpi ( const uint8_t *  mpi,
size_t  mpi_len,
uint8_t  ed25519_pk[32] 
)

#include <openpgp.h>

Extract Ed25519 public key from MPI-encoded data.

Parameters
mpiMPI-encoded public key data
mpi_lenLength of MPI data
ed25519_pkOutput buffer for Ed25519 public key (32 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Extracts Ed25519 public key from OpenPGP MPI (Multi-Precision Integer) format:

  • 2 bytes: bit count (should be ~263 bits for Ed25519 with prefix)
  • 1 byte: 0x40 prefix byte (Ed25519 marker)
  • 32 bytes: Ed25519 public key

RFC 4880 Section 3.2: Multiprecision Integers

Definition at line 167 of file openpgp.c.

167 {
168 if (!mpi || !ed25519_pk || mpi_len < 35) {
169 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for MPI extraction (need at least 35 bytes)");
170 }
171
172 // MPI format:
173 // - 2 bytes: bit count (big-endian)
174 // - 1 byte: 0x40 prefix (Ed25519 marker)
175 // - 32 bytes: Ed25519 public key
176
177 uint16_t bit_count = ((uint16_t)mpi[0] << 8) | mpi[1];
178 log_debug("MPI bit count: %u", bit_count);
179
180 // Ed25519 with 0x40 prefix is typically 263 bits (0x0107)
181 if (bit_count < 256 || bit_count > 270) {
182 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unexpected MPI bit count for Ed25519: %u (expected ~263)", bit_count);
183 }
184
185 // Check for 0x40 prefix byte
186 if (mpi[2] != 0x40) {
187 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing 0x40 prefix byte in Ed25519 MPI (found 0x%02x)", mpi[2]);
188 }
189
190 // Extract 32-byte Ed25519 public key
191 memcpy(ed25519_pk, mpi + 3, 32);
192
193 return ASCIICHAT_OK;
194}

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.

◆ openpgp_parse_armored_pubkey()

asciichat_error_t openpgp_parse_armored_pubkey ( const char *  armored_text,
uint8_t  ed25519_pk[32] 
)

#include <openpgp.h>

Parse PGP armored key block and extract Ed25519 public key.

Parameters
armored_textPGP armored text (--—BEGIN PGP PUBLIC KEY BLOCK--—)
ed25519_pkOutput buffer for Ed25519 public key (32 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Parses a complete PGP armored key block:

  1. Extracts base64 data between BEGIN/END markers
  2. Decodes base64 to binary OpenPGP packets
  3. Parses packet headers to find public key packet (tag 6)
  4. Extracts Ed25519 public key from packet body
Note
Only supports Ed25519 keys (algorithm 22)
Ignores signatures, user IDs, and other packet types
Does not verify checksums or signatures

Example armored format:

-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaWxCORYJKwYBBAHaRw8BAQdAOaykIMyaQi8CBTNiF9o/Nbm6L5DwR9h1maS3
yqG5PFO0MmFzY2lpLWNoYXQgRGlzY292ZXJ5IFNlcnZpY2UgPGFjZHNAYXNjaWkt
...
=+ncm
-----END PGP PUBLIC KEY BLOCK-----

Definition at line 290 of file openpgp.c.

290 {
291 if (!armored_text || !ed25519_pk) {
292 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for armored pubkey parsing");
293 }
294
295 // Find the BEGIN marker
296 const char *begin = strstr(armored_text, "-----BEGIN PGP PUBLIC KEY BLOCK-----");
297 if (!begin) {
298 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PUBLIC KEY BLOCK BEGIN marker");
299 }
300
301 // Skip to the end of the BEGIN line
302 const char *base64_start = strchr(begin, '\n');
303 if (!base64_start) {
304 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid PGP armored format: no newline after BEGIN marker");
305 }
306 base64_start++; // Skip the newline
307
308 // Find the END marker
309 const char *end = strstr(base64_start, "-----END PGP PUBLIC KEY BLOCK-----");
310 if (!end) {
311 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PUBLIC KEY BLOCK END marker");
312 }
313
314 // Find the checksum line (starts with '=') and exclude it
315 const char *base64_end = end;
316 const char *checksum = base64_end;
317 while (checksum > base64_start && *checksum != '=') {
318 checksum--;
319 }
320 if (*checksum == '=') {
321 // Move back to before the checksum line
322 while (checksum > base64_start && (checksum[-1] == '\n' || checksum[-1] == '\r')) {
323 checksum--;
324 }
325 base64_end = checksum;
326 }
327
328 size_t base64_len = (size_t)(base64_end - base64_start);
329
330 log_debug("Extracting base64 data from PGP armor (%zu bytes)", base64_len);
331
332 // Decode base64 to binary OpenPGP packets
333 uint8_t *binary_data;
334 size_t binary_len;
335 asciichat_error_t decode_result = openpgp_base64_decode(base64_start, base64_len, &binary_data, &binary_len);
336 if (decode_result != ASCIICHAT_OK) {
337 return decode_result;
338 }
339
340 log_debug("Decoded %zu bytes of OpenPGP packet data", binary_len);
341
342 // Parse OpenPGP packets to find the public key packet (tag 6)
343 size_t offset = 0;
344 bool found_pubkey = false;
345
346 while (offset < binary_len) {
348 asciichat_error_t header_result = openpgp_parse_packet_header(binary_data + offset, binary_len - offset, &header);
349 if (header_result != ASCIICHAT_OK) {
350 SAFE_FREE(binary_data);
351 return header_result;
352 }
353
354 log_debug("Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
355
356 // Check if this is a public key packet (tag 6)
357 if (header.tag == OPENPGP_TAG_PUBLIC_KEY) {
359 asciichat_error_t parse_result =
360 openpgp_parse_public_key_packet(binary_data + offset + header.header_len, header.length, &pubkey);
361
362 if (parse_result == ASCIICHAT_OK) {
363 memcpy(ed25519_pk, pubkey.pubkey, 32);
364 found_pubkey = true;
365 log_debug("Extracted Ed25519 public key from OpenPGP armored block");
366 break;
367 } else {
368 // Not an Ed25519 key, try next packet
369 log_debug("Skipping non-Ed25519 public key packet");
370 }
371 }
372
373 // Move to next packet
374 offset += header.header_len + header.length;
375 }
376
377 SAFE_FREE(binary_data);
378
379 if (!found_pubkey) {
380 return SET_ERRNO(ERROR_CRYPTO_KEY, "No Ed25519 public key found in PGP armored block");
381 }
382
383 return ASCIICHAT_OK;
384}
asciichat_error_t openpgp_base64_decode(const char *base64, size_t base64_len, uint8_t **binary_out, size_t *binary_len)
Decode PGP armored base64 data.
Definition openpgp.c:32
asciichat_error_t openpgp_parse_packet_header(const uint8_t *data, size_t data_len, openpgp_packet_header_t *header)
Parse OpenPGP packet header.
Definition openpgp.c:70
asciichat_error_t openpgp_parse_public_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_public_key_t *pubkey)
Parse OpenPGP Public Key Packet (tag 6)
Definition openpgp.c:200
#define OPENPGP_TAG_PUBLIC_KEY
OpenPGP packet tag for Public Key Packet.
Definition openpgp.h:41
OpenPGP packet header information.
Definition openpgp.h:71
size_t header_len
Header length (bytes consumed by header)
Definition openpgp.h:74
uint8_t tag
Packet tag (type identifier)
Definition openpgp.h:72
size_t length
Packet body length.
Definition openpgp.h:73
OpenPGP public key packet data.
Definition openpgp.h:91
uint8_t pubkey[32]
Ed25519 public key (32 bytes)
Definition openpgp.h:95

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, openpgp_packet_header_t::header_len, openpgp_packet_header_t::length, log_debug, openpgp_base64_decode(), openpgp_parse_packet_header(), openpgp_parse_public_key_packet(), OPENPGP_TAG_PUBLIC_KEY, openpgp_public_key_t::pubkey, SAFE_FREE, SET_ERRNO, and openpgp_packet_header_t::tag.

Referenced by parse_gpg_key_binary().

◆ openpgp_parse_armored_seckey()

asciichat_error_t openpgp_parse_armored_seckey ( const char *  armored_text,
uint8_t  ed25519_pk[32],
uint8_t  ed25519_sk[32] 
)

#include <openpgp.h>

Parse PGP armored secret key block and extract Ed25519 keypair.

Parameters
armored_textPGP armored text (--—BEGIN PGP PRIVATE KEY BLOCK--—)
ed25519_pkOutput buffer for Ed25519 public key (32 bytes)
ed25519_skOutput buffer for Ed25519 secret key (32 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Parses a complete PGP armored secret key block:

  1. Extracts base64 data between BEGIN/END markers
  2. Decodes base64 to binary OpenPGP packets
  3. Parses packet headers to find secret key packet (tag 5)
  4. Extracts Ed25519 public and secret keys from packet body
Note
Only supports Ed25519 keys (algorithm 22)
Only supports unencrypted secret keys (S2K usage = 0)
Ignores signatures, user IDs, and other packet types
Does not verify checksums or signatures

Example armored format:

-----BEGIN PGP PRIVATE KEY BLOCK-----
lIYEaWxCORYJKwYBBAHaRw8BAQdAOaykIMyaQi8CBTNiF9o/Nbm6L5DwR9h1maS3
yqG5PFMAAQDm8...
=abcd
-----END PGP PRIVATE KEY BLOCK-----

Definition at line 729 of file openpgp.c.

730 {
731 if (!armored_text || !ed25519_pk || !ed25519_sk) {
732 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for armored seckey parsing");
733 }
734
735 // Find the BEGIN marker (try both "PRIVATE KEY" and "SECRET KEY" formats)
736 const char *begin = strstr(armored_text, "-----BEGIN PGP PRIVATE KEY BLOCK-----");
737 if (!begin) {
738 begin = strstr(armored_text, "-----BEGIN PGP SECRET KEY BLOCK-----");
739 }
740 if (!begin) {
741 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK BEGIN marker");
742 }
743
744 // Skip to the end of the BEGIN line
745 const char *base64_start = strchr(begin, '\n');
746 if (!base64_start) {
747 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid PGP armored format: no newline after BEGIN marker");
748 }
749 base64_start++; // Skip the newline
750
751 // Find the END marker (try both formats)
752 const char *end = strstr(base64_start, "-----END PGP PRIVATE KEY BLOCK-----");
753 if (!end) {
754 end = strstr(base64_start, "-----END PGP SECRET KEY BLOCK-----");
755 }
756 if (!end) {
757 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing PGP PRIVATE/SECRET KEY BLOCK END marker");
758 }
759
760 // Find the checksum line (starts with '=') and exclude it
761 const char *base64_end = end;
762 const char *checksum = base64_end;
763 while (checksum > base64_start && *checksum != '=') {
764 checksum--;
765 }
766 if (*checksum == '=') {
767 // Move back to before the checksum line
768 while (checksum > base64_start && (checksum[-1] == '\n' || checksum[-1] == '\r')) {
769 checksum--;
770 }
771 base64_end = checksum;
772 }
773
774 size_t base64_len = (size_t)(base64_end - base64_start);
775
776 log_debug("Extracting base64 data from PGP secret key armor (%zu bytes)", base64_len);
777
778 // Decode base64 to binary OpenPGP packets
779 uint8_t *binary_data;
780 size_t binary_len;
781 asciichat_error_t decode_result = openpgp_base64_decode(base64_start, base64_len, &binary_data, &binary_len);
782 if (decode_result != ASCIICHAT_OK) {
783 return decode_result;
784 }
785
786 log_debug("Decoded %zu bytes of OpenPGP secret key packet data", binary_len);
787
788 // Parse OpenPGP packets to find the secret key packet (tag 5)
789 size_t offset = 0;
790 bool found_seckey = false;
791
792 while (offset < binary_len) {
794 asciichat_error_t header_result = openpgp_parse_packet_header(binary_data + offset, binary_len - offset, &header);
795 if (header_result != ASCIICHAT_OK) {
796 SAFE_FREE(binary_data);
797 return header_result;
798 }
799
800 log_debug("Packet at offset %zu: tag=%u, length=%zu", offset, header.tag, header.length);
801
802 // Check if this is a secret key packet (tag 5)
803 if (header.tag == OPENPGP_TAG_SECRET_KEY) {
805 asciichat_error_t parse_result =
806 openpgp_parse_secret_key_packet(binary_data + offset + header.header_len, header.length, &seckey);
807
808 if (parse_result == ASCIICHAT_OK) {
809 // Check if key is encrypted
810 if (seckey.is_encrypted) {
811 SAFE_FREE(binary_data);
812 log_debug("Detected encrypted GPG key, attempting to decrypt with passphrase");
813
814 // Decrypt the key using gpg binary
815 char *decrypted_text = NULL;
816 asciichat_error_t decrypt_result = openpgp_decrypt_with_gpg(armored_text, &decrypted_text);
817 if (decrypt_result != ASCIICHAT_OK) {
818 return decrypt_result;
819 }
820
821 // Recursively parse the decrypted key
822 asciichat_error_t recursive_result = openpgp_parse_armored_seckey(decrypted_text, ed25519_pk, ed25519_sk);
823 SAFE_FREE(decrypted_text);
824 return recursive_result;
825 }
826
827 // Unencrypted key - extract directly
828 memcpy(ed25519_pk, seckey.pubkey, 32);
829 memcpy(ed25519_sk, seckey.seckey, 32);
830 found_seckey = true;
831 log_debug("Extracted Ed25519 keypair from OpenPGP armored secret key block");
832 break;
833 } else {
834 // Not an Ed25519 key, try next packet
835 log_debug("Skipping non-Ed25519 secret key packet");
836 }
837 }
838
839 // Move to next packet
840 offset += header.header_len + header.length;
841 }
842
843 SAFE_FREE(binary_data);
844
845 if (!found_seckey) {
846 return SET_ERRNO(ERROR_CRYPTO_KEY, "No Ed25519 secret key found in PGP armored block");
847 }
848
849 return ASCIICHAT_OK;
850}
#define OPENPGP_TAG_SECRET_KEY
OpenPGP packet tag for Secret Key Packet.
Definition openpgp.h:44
asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
Parse PGP armored secret key block and extract Ed25519 keypair.
Definition openpgp.c:729
asciichat_error_t openpgp_parse_secret_key_packet(const uint8_t *packet_body, size_t body_len, openpgp_secret_key_t *seckey)
Parse OpenPGP Secret Key Packet (tag 5)
Definition openpgp.c:390
OpenPGP secret key packet data.
Definition openpgp.h:112
uint8_t seckey[32]
Ed25519 secret key (32 bytes)
Definition openpgp.h:117
uint8_t pubkey[32]
Ed25519 public key (32 bytes)
Definition openpgp.h:116
bool is_encrypted
True if secret key material is encrypted.
Definition openpgp.h:119

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, openpgp_packet_header_t::header_len, openpgp_secret_key_t::is_encrypted, openpgp_packet_header_t::length, log_debug, openpgp_base64_decode(), openpgp_parse_armored_seckey(), openpgp_parse_packet_header(), openpgp_parse_secret_key_packet(), OPENPGP_TAG_SECRET_KEY, openpgp_secret_key_t::pubkey, SAFE_FREE, openpgp_secret_key_t::seckey, SET_ERRNO, and openpgp_packet_header_t::tag.

Referenced by openpgp_parse_armored_seckey(), and parse_private_key().

◆ openpgp_parse_packet_header()

asciichat_error_t openpgp_parse_packet_header ( const uint8_t *  data,
size_t  data_len,
openpgp_packet_header_t *  header 
)

#include <openpgp.h>

Parse OpenPGP packet header.

Parameters
dataPacket data (starts with packet header byte)
data_lenLength of packet data
headerOutput parameter for parsed header
Returns
ASCIICHAT_OK on success, error code on failure

Parses OpenPGP packet header (old or new format):

  • Old format: bit 7 = 1, bit 6 = 0, bits 5-2 = tag, bits 1-0 = length type
  • New format: bit 7 = 1, bit 6 = 1, bits 5-0 = tag

RFC 4880 Section 4.2: Packet Headers

Definition at line 70 of file openpgp.c.

70 {
71 if (!data || !header || data_len == 0) {
72 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for packet header parsing");
73 }
74
75 memset(header, 0, sizeof(openpgp_packet_header_t));
76
77 uint8_t ctb = data[0]; // Cipher Type Byte
78
79 // Check if bit 7 is set (all packets must have bit 7 = 1)
80 if ((ctb & 0x80) == 0) {
81 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid OpenPGP packet: bit 7 not set in CTB");
82 }
83
84 // Check if new format (bit 6 = 1) or old format (bit 6 = 0)
85 if (ctb & 0x40) {
86 // New format: bits 5-0 = tag
87 header->new_format = true;
88 header->tag = ctb & 0x3F;
89
90 if (data_len < 2) {
91 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for new format packet header");
92 }
93
94 uint8_t len_byte = data[1];
95
96 if (len_byte < 192) {
97 // One-octet length
98 header->length = len_byte;
99 header->header_len = 2;
100 } else if (len_byte < 224) {
101 // Two-octet length
102 if (data_len < 3) {
103 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for two-octet length");
104 }
105 header->length = ((len_byte - 192) << 8) + data[2] + 192;
106 header->header_len = 3;
107 } else if (len_byte == 255) {
108 // Five-octet length
109 if (data_len < 6) {
110 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for five-octet length");
111 }
112 header->length = ((size_t)data[2] << 24) | ((size_t)data[3] << 16) | ((size_t)data[4] << 8) | data[5];
113 header->header_len = 6;
114 } else {
115 // Partial body length (not supported for our use case)
116 return SET_ERRNO(ERROR_CRYPTO_KEY, "Partial body length not supported");
117 }
118 } else {
119 // Old format: bits 5-2 = tag, bits 1-0 = length type
120 header->new_format = false;
121 header->tag = (ctb >> 2) & 0x0F;
122 uint8_t length_type = ctb & 0x03;
123
124 switch (length_type) {
125 case 0: // One-octet length
126 if (data_len < 2) {
127 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for one-octet length");
128 }
129 header->length = data[1];
130 header->header_len = 2;
131 break;
132
133 case 1: // Two-octet length
134 if (data_len < 3) {
135 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for two-octet length");
136 }
137 header->length = ((size_t)data[1] << 8) | data[2];
138 header->header_len = 3;
139 break;
140
141 case 2: // Four-octet length
142 if (data_len < 5) {
143 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for four-octet length");
144 }
145 header->length = ((size_t)data[1] << 24) | ((size_t)data[2] << 16) | ((size_t)data[3] << 8) | data[4];
146 header->header_len = 5;
147 break;
148
149 case 3: // Indeterminate length (not supported)
150 return SET_ERRNO(ERROR_CRYPTO_KEY, "Indeterminate length not supported");
151
152 default:
153 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid length type: %u", length_type);
154 }
155 }
156
157 log_debug("OpenPGP packet: tag=%u, length=%zu, header_len=%zu, new_format=%d", header->tag, header->length,
158 header->header_len, header->new_format);
159
160 return ASCIICHAT_OK;
161}
bool new_format
True if new format, false if old format.
Definition openpgp.h:75

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, openpgp_packet_header_t::header_len, openpgp_packet_header_t::length, log_debug, openpgp_packet_header_t::new_format, SET_ERRNO, and openpgp_packet_header_t::tag.

Referenced by openpgp_parse_armored_pubkey(), and openpgp_parse_armored_seckey().

◆ openpgp_parse_public_key_packet()

asciichat_error_t openpgp_parse_public_key_packet ( const uint8_t *  packet_body,
size_t  body_len,
openpgp_public_key_t *  pubkey 
)

#include <openpgp.h>

Parse OpenPGP Public Key Packet (tag 6)

Parameters
packet_bodyPacket body data (after header)
body_lenLength of packet body
pubkeyOutput parameter for parsed public key
Returns
ASCIICHAT_OK on success, error code on failure

Parses Public Key Packet (tag 6) body:

  • Version (1 byte, must be 4)
  • Creation time (4 bytes, Unix timestamp)
  • Algorithm (1 byte, 22 = EdDSA)
  • Public key material (MPI format for Ed25519)

RFC 4880 Section 5.5.2: Public-Key Packet Formats

Note
Only supports version 4 packets
Only supports EdDSA (algorithm 22)

Definition at line 200 of file openpgp.c.

201 {
202 if (!packet_body || !pubkey || body_len < 6) {
203 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public key packet parsing");
204 }
205
206 memset(pubkey, 0, sizeof(openpgp_public_key_t));
207
208 size_t offset = 0;
209
210 // Version (1 byte, must be 4)
211 pubkey->version = packet_body[offset++];
212 if (pubkey->version != 4) {
213 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported OpenPGP public key version: %u (only version 4 supported)",
214 pubkey->version);
215 }
216
217 // Creation time (4 bytes, big-endian Unix timestamp)
218 pubkey->created = ((uint32_t)packet_body[offset] << 24) | ((uint32_t)packet_body[offset + 1] << 16) |
219 ((uint32_t)packet_body[offset + 2] << 8) | packet_body[offset + 3];
220 offset += 4;
221
222 // Algorithm (1 byte)
223 pubkey->algorithm = packet_body[offset++];
224
225 log_debug("Public key packet: version=%u, created=%u, algorithm=%u", pubkey->version, pubkey->created,
226 pubkey->algorithm);
227
228 // Only support EdDSA (algorithm 22)
229 if (pubkey->algorithm != OPENPGP_ALGO_EDDSA) {
230 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported public key algorithm: %u (only EdDSA/22 supported)",
231 pubkey->algorithm);
232 }
233
234 // EdDSA (Ed25519) keys have a special encoding:
235 // - OID for the curve (variable length)
236 // - 0x40 prefix byte
237 // - 32 bytes of Ed25519 public key
238 //
239 // We search for the 0x40 prefix and extract the following 32 bytes
240
241 // Search for 0x40 prefix byte in the remaining packet data
242 bool found_prefix = false;
243 size_t key_offset = 0;
244
245 for (size_t i = offset; i < body_len - 32; i++) {
246 if (packet_body[i] == 0x40) {
247 key_offset = i + 1; // Point to first byte after 0x40
248 found_prefix = true;
249 log_debug("Found Ed25519 key prefix 0x40 at offset %zu", i);
250 break;
251 }
252 }
253
254 if (!found_prefix) {
255 return SET_ERRNO(ERROR_CRYPTO_KEY, "Ed25519 public key prefix (0x40) not found in packet");
256 }
257
258 if (key_offset + 32 > body_len) {
259 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 public key (need 32 bytes after 0x40 prefix)");
260 }
261
262 // Extract the 32-byte Ed25519 public key
263 memcpy(pubkey->pubkey, packet_body + key_offset, 32);
264
265 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", pubkey->pubkey[0],
266 pubkey->pubkey[1], pubkey->pubkey[2], pubkey->pubkey[3], pubkey->pubkey[4], pubkey->pubkey[5],
267 pubkey->pubkey[6], pubkey->pubkey[7]);
268
269 // Calculate Key ID (last 8 bytes of SHA-1 fingerprint)
270 // For now, we'll skip fingerprint calculation and just extract from packet if available
271 // The keyid is typically at a fixed offset for Ed25519 keys
272 // We'll compute it properly by hashing the public key material
273
274 // For version 4 keys, fingerprint = SHA-1(0x99 || length || packet_body)
275 // Key ID = last 8 bytes of fingerprint
276 // For simplicity, we'll set keyid to 0 for now (not critical for our use case)
277 pubkey->keyid = 0;
278
279 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", pubkey->pubkey[0],
280 pubkey->pubkey[1], pubkey->pubkey[2], pubkey->pubkey[3], pubkey->pubkey[4], pubkey->pubkey[5],
281 pubkey->pubkey[6], pubkey->pubkey[7]);
282
283 return ASCIICHAT_OK;
284}
#define OPENPGP_ALGO_EDDSA
OpenPGP algorithm ID for EdDSA (Ed25519)
Definition openpgp.h:53
uint64_t keyid
OpenPGP Key ID (last 8 bytes of fingerprint)
Definition openpgp.h:96
uint32_t created
Creation timestamp (Unix epoch)
Definition openpgp.h:93
uint8_t algorithm
Public key algorithm (22 = EdDSA)
Definition openpgp.h:94
uint8_t version
Packet version (should be 4)
Definition openpgp.h:92

References openpgp_public_key_t::algorithm, ASCIICHAT_OK, openpgp_public_key_t::created, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, openpgp_public_key_t::keyid, log_debug, OPENPGP_ALGO_EDDSA, openpgp_public_key_t::pubkey, SET_ERRNO, and openpgp_public_key_t::version.

Referenced by openpgp_parse_armored_pubkey().

◆ openpgp_parse_secret_key_packet()

asciichat_error_t openpgp_parse_secret_key_packet ( const uint8_t *  packet_body,
size_t  body_len,
openpgp_secret_key_t *  seckey 
)

#include <openpgp.h>

Parse OpenPGP Secret Key Packet (tag 5)

Parameters
packet_bodyPacket body data (after header)
body_lenLength of packet body
seckeyOutput parameter for parsed secret key
Returns
ASCIICHAT_OK on success, error code on failure

Parses Secret Key Packet (tag 5) body:

  • Version (1 byte, must be 4)
  • Creation time (4 bytes, Unix timestamp)
  • Algorithm (1 byte, 22 = EdDSA)
  • Public key material (MPI format for Ed25519)
  • S2K usage (1 byte, must be 0 for unencrypted)
  • Secret key material (32 bytes for Ed25519)

RFC 4880 Section 5.5.3: Secret-Key Packet Formats

Note
Only supports version 4 packets
Only supports EdDSA (algorithm 22)
Only supports unencrypted secret keys (S2K usage = 0)

Definition at line 390 of file openpgp.c.

391 {
392 if (!packet_body || !seckey || body_len < 6) {
393 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for secret key packet parsing");
394 }
395
396 memset(seckey, 0, sizeof(openpgp_secret_key_t));
397
398 size_t offset = 0;
399
400 // Parse public key portion (same as public key packet)
401 // Version (1 byte, must be 4)
402 seckey->version = packet_body[offset++];
403 if (seckey->version != 4) {
404 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported OpenPGP secret key version: %u (only version 4 supported)",
405 seckey->version);
406 }
407
408 // Creation time (4 bytes, big-endian Unix timestamp)
409 seckey->created = ((uint32_t)packet_body[offset] << 24) | ((uint32_t)packet_body[offset + 1] << 16) |
410 ((uint32_t)packet_body[offset + 2] << 8) | packet_body[offset + 3];
411 offset += 4;
412
413 // Algorithm (1 byte)
414 seckey->algorithm = packet_body[offset++];
415
416 log_debug("Secret key packet: version=%u, created=%u, algorithm=%u", seckey->version, seckey->created,
417 seckey->algorithm);
418
419 // Only support EdDSA (algorithm 22)
420 if (seckey->algorithm != OPENPGP_ALGO_EDDSA) {
421 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported secret key algorithm: %u (only EdDSA/22 supported)",
422 seckey->algorithm);
423 }
424
425 // EdDSA public key: OID + 0x40 prefix + 32 bytes of Ed25519 public key
426 // Search for 0x40 prefix byte
427 bool found_prefix = false;
428 size_t pubkey_offset = 0;
429
430 for (size_t i = offset; i < body_len - 32; i++) {
431 if (packet_body[i] == 0x40) {
432 pubkey_offset = i + 1;
433 found_prefix = true;
434 log_debug("Found Ed25519 public key prefix 0x40 at offset %zu", i);
435 break;
436 }
437 }
438
439 if (!found_prefix) {
440 return SET_ERRNO(ERROR_CRYPTO_KEY, "Ed25519 public key prefix (0x40) not found in secret key packet");
441 }
442
443 if (pubkey_offset + 32 > body_len) {
444 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 public key (need 32 bytes after 0x40 prefix)");
445 }
446
447 // Extract the 32-byte Ed25519 public key
448 memcpy(seckey->pubkey, packet_body + pubkey_offset, 32);
449
450 log_debug("Extracted Ed25519 public key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", seckey->pubkey[0],
451 seckey->pubkey[1], seckey->pubkey[2], seckey->pubkey[3], seckey->pubkey[4], seckey->pubkey[5],
452 seckey->pubkey[6], seckey->pubkey[7]);
453
454 // Move offset past public key material
455 offset = pubkey_offset + 32;
456
457 // S2K usage byte (1 byte)
458 // 0x00 = secret key is not encrypted
459 // 0xFE or 0xFF = secret key is encrypted with S2K
460 if (offset >= body_len) {
461 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing S2K usage byte in secret key packet");
462 }
463
464 uint8_t s2k_usage = packet_body[offset++];
465 log_debug("S2K usage byte: 0x%02x", s2k_usage);
466
467 if (s2k_usage != 0x00) {
468 seckey->is_encrypted = true;
469 log_debug("Detected encrypted secret key (S2K usage = 0x%02x)", s2k_usage);
470 // Don't parse encrypted key material here - caller will need to decrypt with gpg
471 return ASCIICHAT_OK;
472 }
473
474 seckey->is_encrypted = false;
475
476 // For unencrypted keys (S2K usage = 0x00), secret key material follows directly
477 // For Ed25519: 32 bytes of secret key
478 if (offset + 32 > body_len) {
479 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 secret key (need 32 bytes)");
480 }
481
482 // Extract the 32-byte Ed25519 secret key
483 memcpy(seckey->seckey, packet_body + offset, 32);
484
485 log_debug("Extracted Ed25519 secret key (first 8 bytes): %02x%02x%02x%02x%02x%02x%02x%02x", seckey->seckey[0],
486 seckey->seckey[1], seckey->seckey[2], seckey->seckey[3], seckey->seckey[4], seckey->seckey[5],
487 seckey->seckey[6], seckey->seckey[7]);
488
489 return ASCIICHAT_OK;
490}
uint32_t created
Creation timestamp (Unix epoch)
Definition openpgp.h:114
uint8_t algorithm
Public key algorithm (22 = EdDSA)
Definition openpgp.h:115
uint8_t version
Packet version (should be 4)
Definition openpgp.h:113

References openpgp_secret_key_t::algorithm, ASCIICHAT_OK, openpgp_secret_key_t::created, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, openpgp_secret_key_t::is_encrypted, log_debug, OPENPGP_ALGO_EDDSA, openpgp_secret_key_t::pubkey, openpgp_secret_key_t::seckey, SET_ERRNO, and openpgp_secret_key_t::version.

Referenced by openpgp_parse_armored_seckey().

◆ 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 585 of file known_hosts.c.

585 {
586 char fingerprint[CRYPTO_HEX_KEY_SIZE_NULL];
587 compute_key_fingerprint(server_key, fingerprint);
588
589 // Format IP:port with proper bracket notation for IPv6
590 char ip_with_port[BUFFER_SIZE_MEDIUM];
591 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
592 // Fallback to basic format if error
593 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
594 }
595
596 // Check if we're running interactively (stdin is a terminal and not in snapshot mode)
597 const char *env_skip_known_hosts_checking = platform_getenv("ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK");
598 if (env_skip_known_hosts_checking && strcmp(env_skip_known_hosts_checking, STR_ONE) == 0) {
599 log_warn("Skipping known_hosts checking. This is a security vulnerability.");
600 return true;
601 }
602#ifndef NDEBUG
603 // In debug builds, also skip for Claude Code (LLM automation can't do interactive prompts)
604 const char *env_claudecode = platform_getenv("CLAUDECODE");
605 if (env_claudecode && strlen(env_claudecode) > 0) {
606 log_warn("Skipping known_hosts checking (CLAUDECODE set in debug build).");
607 return true;
608 }
609#endif
611 // SECURITY: Non-interactive mode - REJECT unknown hosts to prevent MITM attacks
612 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify unknown host in non-interactive mode");
613 log_error("ERROR: Cannot verify unknown host in non-interactive mode without environment variable bypass.\n"
614 "This connection may be a man-in-the-middle attack!\n"
615 "\n"
616 "To connect to this host:\n"
617 " 1. Run the client interactively (from a terminal with TTY)\n"
618 " 2. Verify the fingerprint: SHA256:%s\n"
619 " 3. Accept the host when prompted\n"
620 " 4. The host will be added to: %s\n"
621 "\n"
622 "Connection aborted for security.\n"
623 "To bypass this check, set the environment variable ASCII_CHAT_INSECURE_NO_HOST_IDENTITY_CHECK to 1",
624 fingerprint, get_known_hosts_path());
625 return false; // REJECT unknown hosts in non-interactive mode
626 }
627
628 // Interactive mode - prompt user
629 // Lock terminal so only this thread can output to terminal
630 // Other threads' logs are buffered until we unlock
631 bool previous_terminal_state = log_lock_terminal();
632
633 log_plain("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
634 "@ WARNING: REMOTE HOST IDENTIFICATION NOT KNOWN! @\n"
635 "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n"
636 "\n"
637 "The authenticity of host '%s' can't be established.\n"
638 "Ed25519 key fingerprint is SHA256:%s\n",
639 ip_with_port, fingerprint);
640
641 // Unlock before prompt (prompt_yes_no handles its own terminal locking)
642 log_unlock_terminal(previous_terminal_state);
643
644 // Prompt user - default is No for security
645 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
646 log_warn("Warning: Permanently added '%s' to the list of known hosts.", ip_with_port);
647 return true;
648 }
649
650 log_warn("Connection aborted by user.");
651 return false;
652}
bool log_lock_terminal(void)
Lock terminal output for exclusive access by the calling thread.
Definition log/log.c:787
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:618
void log_unlock_terminal(bool previous_state)
Release terminal lock and flush buffered messages.
Definition log/log.c:796
bool platform_prompt_yes_no(const char *question, bool default_yes)
Prompt the user for a yes/no answer.
Definition util.c:83
const char * platform_getenv(const char *name)
Get an environment variable value.
Definition wasm/system.c:39
#define STR_ONE
String literal: "1" (one)
bool terminal_can_prompt_user(void)
Determine if interactive user prompts are appropriate.

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, compute_key_fingerprint(), CRYPTO_HEX_KEY_SIZE_NULL, ERROR_CRYPTO, format_ip_with_port(), get_known_hosts_path(), log_error, log_lock_terminal(), log_plain, log_unlock_terminal(), log_warn, platform_getenv(), platform_prompt_yes_no(), safe_snprintf(), SET_ERRNO, STR_ONE, and terminal_can_prompt_user().

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 767 of file known_hosts.c.

767 {
768 // Format IP:port with proper bracket notation for IPv6
769 char ip_with_port[BUFFER_SIZE_MEDIUM];
770 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
771 // Fallback to basic format if error
772 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
773 }
774
775 // LOCALHOST OPTIMIZATION: Skip authentication prompt for localhost addresses
776 // Localhost is inherently trusted (same machine, no network exposure)
777 // This matches SSH behavior (ssh-keyscan doesn't prompt for localhost)
778 if (is_localhost_address(server_ip)) {
779 log_debug(
780 "SECURITY: Localhost address %s detected - skipping unknown host prompt (localhost is inherently trusted)",
781 ip_with_port);
782 return true; // Silently trust localhost
783 }
784
785 // Check if running in automated/non-interactive environment
786 // In debug builds, Claude Code automation can't interact with prompts
787 if (is_automated_mode()) {
788 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify server without identity key in non-interactive/automated mode");
789 log_error("ERROR: Cannot verify server without identity key in non-interactive/automated mode.\n"
790 "ERROR: This connection is vulnerable to man-in-the-middle attacks!\n"
791 "\n"
792 "To connect to this host:\n"
793 " 1. Run the client interactively (from a terminal with TTY)\n"
794 " 2. Verify you trust this server despite no identity key\n"
795 " 3. Accept the risk when prompted\n"
796 " OR better: Ask server admin to use --key for proper authentication\n"
797 "\n"
798 "Connection aborted for security.\n"
799 "\n");
800 return false;
801 }
802
803 log_warn("\n"
804 "The authenticity of host '%s' can't be established.\n"
805 "The server has no identity key to verify its authenticity.\n"
806 "\n"
807 "WARNING: This connection is vulnerable to man-in-the-middle attacks!\n"
808 "Anyone can intercept your connection and read your data.\n"
809 "\n"
810 "To secure this connection:\n"
811 " 1. Server should use --key to provide an identity key\n"
812 " 2. Client should use --server-key to verify the server\n"
813 "\n",
814 ip_with_port);
815
816 // Interactive mode - prompt user (default is No for security)
817 if (platform_prompt_yes_no("Are you sure you want to continue connecting", false)) {
818 log_warn("Warning: Proceeding with unverified connection.\n"
819 "Your data may be intercepted by attackers!\n"
820 "\n");
821 return true;
822 }
823
824 log_plain("Connection aborted by user.");
825 return false;
826}

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, ERROR_CRYPTO, format_ip_with_port(), log_debug, log_error, log_plain, log_warn, 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.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 449 of file pem.c.

449 {
450 br_x509_certificate *xcs;
451 anchor_list tas = VEC_INIT;
452 size_t u, num;
453
454 xcs = read_certificates_from_memory(pem_data, pem_len, &num);
455 if (xcs == NULL) {
456 return 0;
457 }
458
459 for (u = 0; u < num; u++) {
460 br_x509_trust_anchor ta;
461
462 if (certificate_to_trust_anchor_inner(&ta, &xcs[u]) != ASCIICHAT_OK) {
464 free_certificates(xcs, num);
465 return 0;
466 }
467 VEC_ADD(tas, ta);
468 }
469
470 VEC_ADDMANY(*dst, &VEC_ELT(tas, 0), num);
471 VEC_CLEAR(tas);
472 free_certificates(xcs, num);
473 return num;
474}
void free_ta_contents(br_x509_trust_anchor *ta)
Free the contents of a trust anchor.
Definition pem.c:428
#define VEC_CLEAR(vec)
Definition pem.c:51
#define VEC_CLEAREXT(vec, fun)
Definition pem.c:59
#define VEC_ADDMANY(vec, xp, num)
Definition pem.c:74
#define VEC_ELT(vec, idx)
Definition pem.c:82
#define VEC_INIT
Definition pem.c:48
#define VEC_ADD(vec, x)
Definition pem.c:68
Vector type for trust anchors.
Definition pem.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 486 of file known_hosts.c.

486 {
487 // Validate parameters first
488 if (!server_ip) {
489 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameter: server_ip=%p", server_ip);
490 }
491
492 const char *path = get_known_hosts_path();
493 int fd = platform_open("known_hosts_file", path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
494 if (fd < 0) {
495 // File doesn't exist - nothing to remove, return success
496 return ASCIICHAT_OK;
497 }
498 FILE *f = platform_fdopen("known_hosts_stream", fd, "r");
499 defer(SAFE_FCLOSE(f));
500 if (!f) {
501 platform_close(fd);
502 SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
503 return ERROR_CONFIG;
504 }
505
506 // Format IP:port with proper bracket notation for IPv6
507 char ip_with_port[BUFFER_SIZE_MEDIUM];
508 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
509
510 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
511 }
512
513 // Read all lines into memory
514 char **lines = NULL;
515 size_t num_lines = 0;
516 defer({
517 if (lines) {
518 for (size_t i = 0; i < num_lines; i++) {
519 SAFE_FREE(lines[i]);
520 }
521 }
522 SAFE_FREE(lines);
523 });
524 char line[BUFFER_SIZE_XLARGE];
525
526 char expected_prefix[BUFFER_SIZE_MEDIUM];
527 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
528
529 while (fgets(line, sizeof(line), f)) {
530 // Skip lines that match this IP:port
531 if (strncmp(line, expected_prefix, strlen(expected_prefix)) != 0) {
532 // Keep this line
533 char **new_lines = SAFE_REALLOC((void *)lines, (num_lines + 1) * sizeof(char *), char **);
534 if (new_lines) {
535 lines = new_lines;
536 lines[num_lines] = platform_strdup(line);
537 if (lines[num_lines] == NULL) {
538 return SET_ERRNO(ERROR_MEMORY, "Failed to duplicate line from known_hosts file");
539 }
540 num_lines++;
541 }
542 }
543 }
544 // Close first file before opening for write
545 if (f) {
546 fclose(f);
547 f = NULL; // Prevent double-close by defer
548 }
549
550 // Write back the filtered lines
551 fd = platform_open("known_hosts_file_write", path, PLATFORM_O_WRONLY | PLATFORM_O_CREAT | PLATFORM_O_TRUNC,
553 f = platform_fdopen("known_hosts_write_stream", fd, "w");
554 if (!f) {
555 // Cleanup on error - fdopen failed, so fd is still open but f is NULL
556 // Individual line strings will be freed by defer cleanup
557 platform_close(fd); // Close fd directly since fdopen failed
558 return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open known hosts file: %s", path);
559 }
560
561 for (size_t i = 0; i < num_lines; i++) {
562 (void)fputs(lines[i], f);
563 }
564 // All cleanup (lines, individual strings, file handle) handled by defer statements
565
566 log_debug("KNOWN_HOSTS: Successfully removed host from known_hosts file: %s", path);
567 return ASCIICHAT_OK;
568}
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:284
#define PLATFORM_O_CREAT
Definition filesystem.h:85
#define PLATFORM_O_TRUNC
Definition filesystem.h:87
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
#define PLATFORM_O_WRONLY
Definition filesystem.h:83

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 145 of file ssh_agent.c.

145 {
146 if (private_key == NULL) {
147 return SET_ERRNO(ERROR_INVALID_PARAM, "Cannot add key to ssh-agent: private_key is NULL");
148 }
149 if (private_key->type != KEY_TYPE_ED25519) {
150 return SET_ERRNO(ERROR_INVALID_PARAM, "Cannot add key to ssh-agent: only Ed25519 keys supported");
151 }
152
153 log_debug("Adding key to ssh-agent: %s", key_path ? key_path : "(memory)");
154
155 // Open the pipe/socket for this operation (works on both Windows and Unix)
156 pipe_t pipe = ssh_agent_open_pipe();
157 if (pipe == INVALID_PIPE_VALUE) {
158 return SET_ERRNO(ERROR_CRYPTO, "Failed to connect to ssh-agent");
159 }
160
161 // Build SSH agent protocol message: SSH2_AGENTC_ADD_IDENTITY (17)
162 // Message format:
163 // uint32: message length
164 // byte: SSH2_AGENTC_ADD_IDENTITY (17)
165 // string: key type ("ssh-ed25519")
166 // string: public key (32 bytes)
167 // string: private key (64 bytes)
168 // string: comment (key path or empty)
169
170 unsigned char buf[BUFFER_SIZE_XXLARGE];
171 size_t pos = 4; // Reserve space for length prefix
172
173 // Message type: SSH2_AGENTC_ADD_IDENTITY
174 buf[pos++] = 17;
175
176 // Key type: "ssh-ed25519" (11 bytes)
177 uint32_t len = 11;
178 write_u32_be(buf + pos, len);
179 pos += 4;
180 // Binary protocol: intentionally not null-terminated
181 // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary data
182 memcpy(buf + pos, "ssh-ed25519", 11);
183 pos += 11;
184
185 // Public key (32 bytes) - last 32 bytes of the 64-byte ed25519 key
186 len = 32;
187 write_u32_be(buf + pos, len);
188 pos += 4;
189 memcpy(buf + pos, private_key->key.ed25519 + 32, 32); // Public key is second half
190 pos += 32;
191
192 // Private key (64 bytes - full ed25519 key: 32-byte seed + 32-byte public)
193 len = 64;
194 write_u32_be(buf + pos, len);
195 pos += 4;
196 memcpy(buf + pos, private_key->key.ed25519, 64);
197 pos += 64;
198
199 // Comment (key path)
200 len = key_path ? strlen(key_path) : 0;
201
202 // SECURITY: Validate key path length to prevent buffer overflow
203 // Buffer is BUFFER_SIZE_XXLARGE (4096), pos is ~128 at this point, need 4 bytes for length prefix
204 size_t max_key_path_len = sizeof(buf) - pos - 4;
205 if (len > max_key_path_len) {
207 sodium_memzero(buf, sizeof(buf));
208 return SET_ERRNO(ERROR_BUFFER_OVERFLOW, "SSH key path too long: %u bytes (max %zu)", len, max_key_path_len);
209 }
210
211 write_u32_be(buf + pos, len);
212 pos += 4;
213 if (len > 0) {
214 memcpy(buf + pos, key_path, len);
215 pos += len;
216 }
217
218 // Write message length at start (excluding the 4-byte length field itself)
219 uint32_t msg_len = pos - 4;
220 write_u32_be(buf, msg_len);
221
222 // Send message to agent
223 ssize_t bytes_written = platform_pipe_write(pipe, buf, pos);
224 if (bytes_written != (ssize_t)pos) {
226 sodium_memzero(buf, sizeof(buf));
227 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to write to ssh-agent pipe");
228 }
229
230 // Read response
231 unsigned char response[BUFFER_SIZE_SMALL];
232 ssize_t bytes_read = platform_pipe_read(pipe, response, sizeof(response));
233 if (bytes_read < 5) {
235 sodium_memzero(buf, sizeof(buf));
236 return SET_ERRNO_SYS(ERROR_CRYPTO, "Failed to read from ssh-agent pipe");
237 }
238
239 // Done with the pipe - close it
241 sodium_memzero(buf, sizeof(buf));
242
243 // Check response: should be SSH_AGENT_SUCCESS (6)
244 // Response format: uint32 length, byte message_type
245 uint8_t response_type = response[4];
246 if (response_type == 6) {
247 log_debug("Successfully added key to ssh-agent");
248 return ASCIICHAT_OK;
249 } else if (response_type == 5) {
250 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent rejected key (SSH_AGENT_FAILURE)");
251 } else {
252 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response: %d", response_type);
253 }
254}
#define BUFFER_SIZE_XXLARGE
Extra extra large buffer size (4096 bytes)
@ ERROR_BUFFER_OVERFLOW
#define INVALID_PIPE_VALUE
Invalid pipe value (POSIX: -1)
Definition pipe.h:42

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_debug, 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 50 of file ssh_agent.c.

50 {
51 if (public_key == NULL) {
52 log_warn("NULL is not a valid public key");
53 return false;
54 }
55
56 // Use SSH agent protocol to list keys (works on both Windows and Unix)
57 pipe_t pipe = ssh_agent_open_pipe();
58 if (pipe == INVALID_PIPE_VALUE) {
59 return false;
60 }
61
62 // Build SSH2_AGENTC_REQUEST_IDENTITIES message (type 11)
63 unsigned char request[5];
64 request[0] = 0; // length: 1 (4-byte big-endian)
65 request[1] = 0;
66 request[2] = 0;
67 request[3] = 1;
68 request[4] = 11; // SSH2_AGENTC_REQUEST_IDENTITIES
69
70 // Send request
71 ssize_t bytes_written = platform_pipe_write(pipe, request, 5);
72 if (bytes_written != 5) {
74 return false;
75 }
76
77 // Read response
78 unsigned char response[BUFFER_SIZE_XXXLARGE];
79 ssize_t bytes_read = platform_pipe_read(pipe, response, sizeof(response));
80 if (bytes_read < 9) {
82 return false;
83 }
84
86
87 // Parse response: type should be SSH2_AGENT_IDENTITIES_ANSWER (12)
88 uint8_t resp_type = response[4];
89 if (resp_type != 12) {
90 return false;
91 }
92
93 // Number of keys at bytes 5-8
94 uint32_t num_keys = read_u32_be(response + 5);
95
96 // Parse keys and check if our public key matches
97 size_t pos = 9;
98 for (uint32_t i = 0; i < num_keys && pos + 4 < (size_t)bytes_read; i++) {
99 // Read key blob length
100 uint32_t blob_len = read_u32_be(response + pos);
101 pos += 4;
102
103 if (pos + blob_len > (size_t)bytes_read)
104 break;
105
106 // Parse the blob to extract the Ed25519 public key
107 size_t blob_pos = pos;
108 // Skip key type string
109 if (blob_pos + 4 > pos + blob_len) {
110 pos += blob_len;
111 continue;
112 }
113 uint32_t type_len = read_u32_be(response + blob_pos);
114 blob_pos += 4 + type_len;
115
116 // Read public key data
117 if (blob_pos + 4 > pos + blob_len) {
118 pos += blob_len;
119 continue;
120 }
121 uint32_t pubkey_len = read_u32_be(response + blob_pos);
122 blob_pos += 4;
123
124 // Compare public key (should be 32 bytes for Ed25519)
125 // Use constant-time comparison to prevent timing side channels
126 if (pubkey_len == 32 && blob_pos + 32 <= pos + blob_len) {
127 if (sodium_memcmp(response + blob_pos, public_key->key, 32) == 0) {
128 log_debug("Found matching key in ssh-agent");
129 return true;
130 }
131 }
132
133 pos += blob_len;
134
135 // Skip comment string length + comment
136 if (pos + 4 > (size_t)bytes_read)
137 break;
138 uint32_t comment_len = read_u32_be(response + pos);
139 pos += 4 + comment_len;
140 }
141
142 return false;
143}

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 38 of file ssh_agent.c.

38 {
39 /* Try to open the SSH agent connection */
40 pipe_t pipe = ssh_agent_open_pipe();
41 if (pipe != INVALID_PIPE_VALUE) {
43 log_dev("ssh-agent is available");
44 return true;
45 }
46 log_dev("ssh-agent not available");
47 return false;
48}

References INVALID_PIPE_VALUE, log_dev, and platform_pipe_close().

◆ 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 256 of file ssh_agent.c.

257 {
258 if (!public_key || !message || !signature) {
259 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: public_key=%p, message=%p, signature=%p", public_key,
260 message, signature);
261 }
262
263 if (public_key->type != KEY_TYPE_ED25519) {
264 return SET_ERRNO(ERROR_CRYPTO_KEY, "Only Ed25519 keys are supported for SSH agent signing");
265 }
266
267 // SSH agent protocol limits message size (typical OpenSSH agent limit is around 1MB)
268 // Large messages can cause agent memory exhaustion or timeout
269 if (message_len > 1024 * 1024) {
270 return SET_ERRNO(ERROR_CRYPTO, "Message too large for SSH agent (max 1MB)");
271 }
272
273 // Connect to SSH agent
274 pipe_t pipe = ssh_agent_open_pipe();
275 if (pipe == INVALID_PIPE_VALUE) {
276 return SET_ERRNO(ERROR_CRYPTO, "Cannot connect to ssh-agent");
277 }
278
279 // Build SSH2_AGENTC_SIGN_REQUEST message (type 13)
280 // Format: uint32 length, byte type, string key_blob, string data, uint32 flags
281 // For Ed25519, key_blob is: string "ssh-ed25519", string public_key(32 bytes)
282
283 const char *key_type = "ssh-ed25519";
284 uint32_t key_type_len = (uint32_t)strlen(key_type);
285
286 // Calculate total message length
287 // 1 (type) + 4 (key_blob_len) + key_blob_size + 4 (data_len) + data_size + 4 (flags)
288 uint32_t key_blob_size = 4 + key_type_len + 4 + 32; // string(key_type) + string(pubkey)
289 uint32_t total_len = 1 + 4 + key_blob_size + 4 + message_len + 4;
290
291 uint8_t *buf = SAFE_MALLOC(total_len + 4, uint8_t *); // +4 for length prefix
292 if (!buf) {
294 return SET_ERRNO(ERROR_CRYPTO, "Out of memory for SSH agent sign request");
295 }
296
297 uint32_t offset = 0;
298
299 // Write total message length (excluding this 4-byte length field)
300 write_u32_be(buf + offset, total_len);
301 offset += 4;
302
303 // Write message type (13 = SSH2_AGENTC_SIGN_REQUEST)
304 buf[offset++] = 13;
305
306 // Write key_blob length
307 write_u32_be(buf + offset, key_blob_size);
308 offset += 4;
309
310 // Write key_blob: string(key_type)
311 write_u32_be(buf + offset, key_type_len);
312 offset += 4;
313 // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary data
314 memcpy(buf + offset, key_type, key_type_len);
315 offset += key_type_len;
316
317 // Write key_blob: string(public_key)
318 write_u32_be(buf + offset, 32);
319 offset += 4;
320 memcpy(buf + offset, public_key->key, 32);
321 offset += 32;
322
323 // Write data to sign
324 write_u32_be(buf + offset, (uint32_t)message_len);
325 offset += 4;
326 memcpy(buf + offset, message, message_len);
327 offset += (uint32_t)message_len;
328
329 // Write flags (0 = default)
330 write_u32_be(buf + offset, 0);
331 offset += 4;
332
333 // Send request
334 ssize_t written = platform_pipe_write(pipe, buf, total_len + 4);
335 sodium_memzero(buf, total_len + 4);
336 SAFE_FREE(buf);
337
338 if (written < 0 || (size_t)written != total_len + 4) {
340 return SET_ERRNO(ERROR_CRYPTO, "Failed to write SSH agent sign request");
341 }
342
343 // Read response
345 ssize_t read_bytes = platform_pipe_read(pipe, response, sizeof(response));
347
348 if (read_bytes < 5) {
349 return SET_ERRNO(ERROR_CRYPTO, "Failed to read SSH agent sign response (read %zd bytes)", read_bytes);
350 }
351
352 // Response format: uint32 length, byte type, data...
353 // We validate length implicitly by checking read_bytes and parsing the full response
354 (void)read_u32_be(response); // Read but don't need explicit length check
355 uint8_t response_type = response[4];
356
357 // Check for SSH2_AGENT_SIGN_RESPONSE (14)
358 if (response_type != 14) {
359 if (response_type == 5) {
360 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent refused to sign (SSH_AGENT_FAILURE)");
361 }
362 return SET_ERRNO(ERROR_CRYPTO, "ssh-agent returned unexpected response type: %d (expected 14)", response_type);
363 }
364
365 // Parse signature blob
366 // Response format: uint32 len, byte type(14), string signature_blob
367 if (read_bytes < 9) {
368 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response too short (no signature blob length)");
369 }
370
371 uint32_t sig_blob_len = read_u32_be(response + 5);
372 uint32_t expected_total = 4 + 1 + 4 + sig_blob_len;
373
374 if ((size_t)read_bytes < expected_total) {
375 return SET_ERRNO(ERROR_CRYPTO, "SSH agent response truncated (expected %u bytes, got %zd)", expected_total,
376 read_bytes);
377 }
378
379 // Signature blob format for Ed25519: string "ssh-ed25519", string signature(64 bytes)
380 uint32_t offset_sig = 9;
381 uint32_t sig_type_len = read_u32_be(response + offset_sig);
382 offset_sig += 4;
383
384 if (offset_sig + sig_type_len + 4 > (uint32_t)read_bytes) {
385 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature type");
386 }
387
388 // Verify signature type is "ssh-ed25519"
389 if (sig_type_len != 11 || memcmp(response + offset_sig, "ssh-ed25519", 11) != 0) {
390 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned non-Ed25519 signature");
391 }
392 offset_sig += sig_type_len;
393
394 // Read signature bytes
395 uint32_t sig_len = read_u32_be(response + offset_sig);
396 offset_sig += 4;
397
398 if (sig_len != 64) {
399 return SET_ERRNO(ERROR_CRYPTO, "SSH agent returned invalid Ed25519 signature length: %u (expected 64)", sig_len);
400 }
401
402 if (offset_sig + 64 > (uint32_t)read_bytes) {
403 return SET_ERRNO(ERROR_CRYPTO, "SSH agent signature blob truncated at signature bytes");
404 }
405
406 // Copy signature to output
407 memcpy(signature, response + offset_sig, 64);
408
409 log_debug("SSH agent successfully signed %zu bytes with Ed25519 key", message_len);
410 return ASCIICHAT_OK;
411}
key_type_t type
Definition key_types.h:70

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.h.

Referenced by https_get().

◆ len

size_t anchor_list::len

Total capacity of array

Definition at line 81 of file pem.h.

◆ ptr

size_t anchor_list::ptr

Current number of trust anchors

Definition at line 80 of file pem.h.

Referenced by https_get().