ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
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
 Endpoint cryptographic state for handshake, packet protection, authentication, and rekeying. More...
struct  openpgp_packet_header_t
 The framing fields needed to locate and classify one OpenPGP packet body. More...
struct  openpgp_public_key_t
 The parsed identity portion of an OpenPGP public-key packet. More...
struct  openpgp_secret_key_t
 The parsed public and private material from an OpenPGP secret-key packet. More...
struct  anchor_list
 Vector type for trust anchors. More...

Macros

#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

Typedefs

typedef struct crypto_context_t crypto_context_t
 Endpoint cryptographic state for handshake, packet protection, authentication, and rekeying.

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_binary_pubkey (const uint8_t *data, size_t data_len, uint8_t ed25519_pk[32])
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.
asciichat_error_t openpgp_parse_binary_seckey (const uint8_t *data, size_t data_len, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])

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[ED25519_SIGNATURE_SIZE])
 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[ED25519_SIGNATURE_SIZE])
 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_with_key (const char *server_ip, uint16_t port, const uint8_t server_key[32], uint8_t expected_key[32], bool *has_expected_key)
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[(32 *2+1)])
 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

Vector type for trust anchors

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.

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

The framing fields needed to locate and classify one OpenPGP packet body

The parsed identity portion of an OpenPGP public-key packet

The parsed public and private material from an OpenPGP secret-key packet

#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:

  • Cryptographic context management
  • X25519 key exchange (Diffie-Hellman)
  • XSalsa20-Poly1305 encryption/decryption
  • Argon2id password-based key derivation
  • HMAC-SHA256 authentication
  • Session rekeying protocol
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 avoid nonce reuse; this alone does not detect replayed ciphertext.
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:

  • PGP armored format (--—BEGIN PGP PUBLIC KEY BLOCK--—)
  • Base64 decoding of armored data
  • OpenPGP packet header parsing (old and new formats)
  • Public Key Packet (tag 6) parsing
  • Ed25519 (algorithm 22) key extraction
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:

  • Identity key entries: <IP:port> x25519 <hex_key> [comment]
  • No-identity entries: <IP:port> no-identity [comment]
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

◆ ANCHOR_LIST_INIT

#define ANCHOR_LIST_INIT   {NULL, 0, 0}

#include <include/ascii-chat/crypto/pem.h>

Initializer for anchor_list.

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

Definition at line 94 of file pem.h.

Referenced by https_get().

◆ CRYPTO_FINGERPRINT_SIZE

#define CRYPTO_FINGERPRINT_SIZE   32

#include <include/ascii-chat/crypto/crypto.h>

Ed25519 key fingerprint size in bytes.

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

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

◆ CRYPTO_HEX_KEY64_SIZE

#define CRYPTO_HEX_KEY64_SIZE   128

#include <include/ascii-chat/crypto/crypto.h>

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

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

◆ CRYPTO_HEX_KEY64_SIZE_NULL

#define CRYPTO_HEX_KEY64_SIZE_NULL   129

#include <include/ascii-chat/crypto/crypto.h>

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

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

◆ CRYPTO_HEX_KEY_SIZE

#define CRYPTO_HEX_KEY_SIZE   64

#include <include/ascii-chat/crypto/crypto.h>

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

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

Referenced by add_known_host(), and compute_key_fingerprint().

◆ CRYPTO_HEX_KEY_SIZE_NULL

#define CRYPTO_HEX_KEY_SIZE_NULL   65

#include <include/ascii-chat/crypto/crypto.h>

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

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

Referenced by add_known_host(), compute_key_fingerprint(), and prompt_unknown_host().

◆ CRYPTO_KEY_SIZE

#define CRYPTO_KEY_SIZE   32

#include <include/ascii-chat/crypto/crypto.h>

Ed25519 key size in bytes.

Ed25519 public and private keys are 32 bytes each.

Definition at line 245 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 <include/ascii-chat/crypto/crypto.h>

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

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

◆ CRYPTO_MAX_PLAINTEXT_SIZE

#define CRYPTO_MAX_PLAINTEXT_SIZE   ((size_t)1024 * 1024)

◆ NO_IDENTITY_MARKER

#define NO_IDENTITY_MARKER   "no-identity"

#include <include/ascii-chat/crypto/crypto.h>

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

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

Referenced by add_known_host(), and check_known_host_with_key().

◆ OPENPGP_ALGO_ECDH

#define OPENPGP_ALGO_ECDH   18

#include <include/ascii-chat/crypto/gpg/openpgp.h>

OpenPGP algorithm ID for ECDH (Curve25519).

Definition at line 60 of file openpgp.h.

◆ OPENPGP_ALGO_EDDSA

#define OPENPGP_ALGO_EDDSA   22

#include <include/ascii-chat/crypto/gpg/openpgp.h>

OpenPGP algorithm ID for EdDSA (Ed25519).

Definition at line 57 of file openpgp.h.

Referenced by openpgp_parse_public_key_packet(), and openpgp_parse_secret_key_packet().

◆ OPENPGP_TAG_PUBLIC_KEY

#define OPENPGP_TAG_PUBLIC_KEY   6

#include <include/ascii-chat/crypto/gpg/openpgp.h>

OpenPGP packet tag for Public Key Packet.

Definition at line 45 of file openpgp.h.

Referenced by openpgp_parse_binary_pubkey().

◆ OPENPGP_TAG_SECRET_KEY

#define OPENPGP_TAG_SECRET_KEY   5

#include <include/ascii-chat/crypto/gpg/openpgp.h>

OpenPGP packet tag for Secret Key Packet.

Definition at line 48 of file openpgp.h.

Referenced by openpgp_parse_armored_seckey(), and openpgp_parse_binary_seckey().

◆ OPENPGP_TAG_SIGNATURE

#define OPENPGP_TAG_SIGNATURE   2

#include <include/ascii-chat/crypto/gpg/openpgp.h>

OpenPGP packet tag for Signature Packet.

Definition at line 54 of file openpgp.h.

◆ OPENPGP_TAG_USER_ID

#define OPENPGP_TAG_USER_ID   13

#include <include/ascii-chat/crypto/gpg/openpgp.h>

OpenPGP packet tag for User ID Packet.

Definition at line 51 of file openpgp.h.

◆ REKEY_DEFAULT_PACKET_THRESHOLD

#define REKEY_DEFAULT_PACKET_THRESHOLD   1000000

#include <include/ascii-chat/crypto/crypto.h>

Default rekey packet threshold (1 million packets).

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

Referenced by crypto_init().

◆ REKEY_DEFAULT_TIME_THRESHOLD

#define REKEY_DEFAULT_TIME_THRESHOLD   (3600LL * NS_PER_SEC_INT)

#include <include/ascii-chat/crypto/crypto.h>

Default rekey time threshold (1 hour in nanoseconds).

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

Referenced by crypto_init().

◆ REKEY_MAX_FAILURE_COUNT

#define REKEY_MAX_FAILURE_COUNT   10

#include <include/ascii-chat/crypto/crypto.h>

Maximum consecutive rekey failures before giving up.

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

Referenced by crypto_rekey_init().

◆ REKEY_MIN_INTERVAL

#define REKEY_MIN_INTERVAL   (3LL * NS_PER_SEC_INT)

#include <include/ascii-chat/crypto/crypto.h>

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

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

Referenced by crypto_rekey_init().

◆ REKEY_MIN_REQUEST_INTERVAL

#define REKEY_MIN_REQUEST_INTERVAL   (60LL * NS_PER_SEC_INT)

#include <include/ascii-chat/crypto/crypto.h>

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

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

◆ REKEY_TEST_PACKET_THRESHOLD

#define REKEY_TEST_PACKET_THRESHOLD   1000

#include <include/ascii-chat/crypto/crypto.h>

Test mode rekey packet threshold (1000 packets).

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

Referenced by crypto_init().

◆ REKEY_TEST_TIME_THRESHOLD

#define REKEY_TEST_TIME_THRESHOLD   (30LL * NS_PER_SEC_INT)

#include <include/ascii-chat/crypto/crypto.h>

Test mode rekey time threshold (30 seconds in nanoseconds).

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

Referenced by crypto_init().

◆ SSH_ED25519_KEY_TYPE

#define SSH_ED25519_KEY_TYPE   "ssh-ed25519"

#include <include/ascii-chat/crypto/crypto.h>

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

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

◆ SSH_KEY_HEADER_SIZE

#include <include/ascii-chat/crypto/crypto.h>

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

231#define SSH_KEY_HEADER_SIZE \
232 (SSH_KEY_TYPE_LENGTH_SIZE + SSH_KEY_TYPE_STRING_SIZE + SSH_KEY_PUBLIC_KEY_LENGTH_SIZE + SSH_KEY_PUBLIC_KEY_SIZE)

Referenced by parse_ssh_ed25519_line().

◆ SSH_KEY_PERMISSIONS_MASK

#define SSH_KEY_PERMISSIONS_MASK   (S_IRWXG | S_IRWXO)

◆ SSH_KEY_PUBLIC_KEY_LENGTH_SIZE

#define SSH_KEY_PUBLIC_KEY_LENGTH_SIZE   4

◆ SSH_KEY_PUBLIC_KEY_SIZE

#define SSH_KEY_PUBLIC_KEY_SIZE   32

◆ SSH_KEY_RECOMMENDED_PERMISSIONS

#define SSH_KEY_RECOMMENDED_PERMISSIONS   0600

◆ SSH_KEY_TYPE_LENGTH_SIZE

#define SSH_KEY_TYPE_LENGTH_SIZE   4

◆ SSH_KEY_TYPE_STRING_SIZE

#define SSH_KEY_TYPE_STRING_SIZE   11

◆ X25519_KEY_TYPE

#define X25519_KEY_TYPE   "x25519"

#include <include/ascii-chat/crypto/crypto.h>

X25519 key type string ("x25519").

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

Referenced by add_known_host().

Typedef Documentation

◆ crypto_context_t

typedef struct crypto_context_t crypto_context_t

#include <include/ascii-chat/crypto/crypto.h>

Endpoint cryptographic state for handshake, packet protection, authentication, and rekeying.

The context owns ephemeral X25519 material, the derived shared and optional password keys, negotiated wire sizes, authentication nonces, packet counters, and temporary keys during a rotation. Transports borrow it when sealing or opening packets. It is initialized through the crypto API and destroyed with crypto_destroy() so secret fields are wiped rather than copied or freed by callers.

Note
Nonce generation: Nonces are constructed as session_id || counter where session_id is 16 bytes and counter fills the remaining bytes. This construction aims to avoid nonce reuse, not to detect replays.
Session ID: Randomly generated for each connection and again on rekey. It contributes to nonce uniqueness; the decryptor does not check it.
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)

Endpoint cryptographic state for handshake, packet protection, authentication, and rekeying.

The context owns ephemeral X25519 material, the derived shared and optional password keys, negotiated wire sizes, authentication nonces, packet counters, and temporary keys during a rotation. Transports borrow it when sealing or opening packets. It must be initialized through the crypto API and destroyed through crypto_destroy() so secret fields are wiped rather than copied or freed by callers.

Enumeration Type Documentation

◆ crypto_result_t

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

203 {
204 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 <include/ascii-chat/crypto/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.

References ED25519_PUBLIC_KEY_SIZE.

◆ check_known_host()

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

#include <include/ascii-chat/crypto/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.

References ED25519_PUBLIC_KEY_SIZE.

◆ check_known_host_no_identity()

asciichat_error_t check_known_host_no_identity ( const char * server_ip,
uint16_t port )

#include <include/ascii-chat/crypto/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 244 of file known_hosts.c.

244 {
245 const char *path = get_known_hosts_path();
246 int fd = platform_open("known_hosts_file", path, PLATFORM_O_RDONLY, FILE_PERM_PRIVATE);
247 if (fd < 0) {
248 // File doesn't exist - this is an unknown host that needs verification
249 log_warn("Known hosts file does not exist: %s", path);
250 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
251 }
252
253 FILE *f = platform_fdopen("known_hosts_stream", fd, "r");
254 defer(SAFE_FCLOSE(f));
255 if (!f) {
256 // Failed to open file descriptor as FILE*
257 platform_close(fd);
258 log_warn("Failed to open known hosts file: %s", path);
259 return ASCIICHAT_OK; // Return 0 to indicate unknown host (first connection)
260 }
261
262 char line[BUFFER_SIZE_XLARGE];
263 char expected_prefix[BUFFER_SIZE_MEDIUM];
264
265 // Format IP:port with proper bracket notation for IPv6
266 char ip_with_port[BUFFER_SIZE_MEDIUM];
267 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
268
269 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid IP format: %s", server_ip);
270 }
271
272 // Add space after IP:port for prefix matching
273 safe_snprintf(expected_prefix, sizeof(expected_prefix), "%s ", ip_with_port);
274
275 while (fgets(line, sizeof(line), f)) {
276 if (line[0] == '#')
277 continue; // Comment
278
279 // Use regex to parse and extract known_hosts line components
280 char *parsed_ip_port = NULL, *parsed_key_type = NULL, *parsed_hex_key = NULL, *parsed_comment = NULL;
281 if (!crypto_regex_match_known_hosts(line, &parsed_ip_port, &parsed_key_type, &parsed_hex_key, &parsed_comment)) {
282 continue; // Line doesn't match known_hosts format
283 }
284 defer(SAFE_FREE(parsed_comment));
285 defer(SAFE_FREE(parsed_hex_key));
286 defer(SAFE_FREE(parsed_key_type));
287 defer(SAFE_FREE(parsed_ip_port));
288
289 // Check if IP:port matches what we're looking for (with IPv6 normalization)
290 if (!compare_ip_port_strings(parsed_ip_port, ip_with_port)) {
291 continue; // Different IP:port
292 }
293
294 // Found matching IP:port - check key type
295 if (strcmp(parsed_key_type, "no-identity") == 0) {
296 // This is a server without identity key that was previously accepted by the user
297 // No warnings or user confirmation needed - user already accepted this server
298 return 1; // Known host (no-identity entry) - secure connection
299 }
300
301 // If we found a normal identity key entry, this is a mismatch
302 // Server previously had identity key but now has none
303 NOTICE(DANGER, "SERVER IDENTITY REMOVED",
304 "Server previously had identity key but now has none - potential security issue");
305 return ERROR_CRYPTO_VERIFICATION; // Mismatch - server changed from identity to no-identity
306 }
307
308 return ASCIICHAT_OK; // Not found = first connection
309}
#define SET_ERRNO(code,...)
#define BUFFER_SIZE_XLARGE
Extra large buffer size (2048 bytes).
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes).
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_FCLOSE(fp)
Definition common.h:395
const char * get_known_hosts_path(void)
Get the path to the known_hosts file.
Definition known_hosts.c:57
@ ERROR_CRYPTO_VERIFICATION
Definition error_codes.h:97
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
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:176
int compare_ip_port_strings(const char *ip_port1, const char *ip_port2)
Compare two "IP:port" strings with IPv6 normalization.
Definition ip.c:1052
#define NOTICE(severity, title,...)
Definition notice.h:68
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:133

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, compare_ip_port_strings(), crypto_regex_match_known_hosts(), ERROR_CRYPTO_VERIFICATION, ERROR_INVALID_PARAM, format_ip_with_port(), get_known_hosts_path(), log_warn, NOTICE, SAFE_FCLOSE, SAFE_FREE, safe_snprintf(), and SET_ERRNO.

Referenced by crypto_handshake_client_key_exchange().

◆ check_known_host_with_key()

asciichat_error_t check_known_host_with_key ( const char * server_ip,
uint16_t port,
const uint8_t server_key[32],
uint8_t expected_key[32],
bool * has_expected_key )

#include <include/ascii-chat/crypto/known_hosts.h>

Same trust decision; optionally return the first stored nonzero identity for diagnostics. A mismatch can have multiple stored candidates. No fabricated key is returned for no-identity entries.

References ED25519_PUBLIC_KEY_SIZE.

◆ compute_key_fingerprint()

void compute_key_fingerprint ( const uint8_t key[32],
char fingerprint[(32 *2+1)] )

#include <include/ascii-chat/crypto/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).

References ED25519_PUBLIC_KEY_SIZE, and HEX_STRING_SIZE_32.

◆ crypto_combine_auth_data()

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

#include <include/ascii-chat/crypto/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 1132 of file lib/crypto/crypto.c.

1132 {
1133 if (!hmac || !challenge_nonce || !combined_out) {
1134 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: hmac=%p, challenge_nonce=%p, combined_out=%p", hmac,
1135 challenge_nonce, combined_out);
1136 return;
1137 }
1138
1139 // Combine HMAC (32 bytes) + challenge nonce (32 bytes) = 64 bytes total
1140 memcpy(combined_out, hmac, HMAC_SHA256_SIZE);
1141 memcpy(combined_out + AUTH_HMAC_SIZE, challenge_nonce, AUTH_CHALLENGE_SIZE);
1142}
#define AUTH_CHALLENGE_SIZE
Challenge nonce size (32 bytes).
Definition sizes.h:99
#define HMAC_SHA256_SIZE
HMAC-SHA256 output size in bytes.
Definition sizes.h:43
#define AUTH_HMAC_SIZE
HMAC size in authentication packets (32 bytes).
Definition sizes.h:97

References AUTH_CHALLENGE_SIZE, AUTH_HMAC_SIZE, ERROR_INVALID_PARAM, HMAC_SHA256_SIZE, 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 <include/ascii-chat/crypto/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.

References AUTH_CHALLENGE_SIZE, and HMAC_SHA256_SIZE.

◆ 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 <include/ascii-chat/crypto/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.

References AUTH_CHALLENGE_SIZE, HMAC_SHA256_KEY_SIZE, and HMAC_SHA256_SIZE.

◆ 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 <include/ascii-chat/crypto/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.

References HMAC_SHA256_KEY_SIZE, and HMAC_SHA256_SIZE.

◆ 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 <include/ascii-chat/crypto/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 1062 of file lib/crypto/crypto.c.

1063 {
1064 if (!ctx || !password_key || !nonce || !shared_secret || !hmac_out) {
1066 "Invalid parameters: ctx=%p, password_key=%p, nonce=%p, shared_secret=%p, hmac_out=%p", ctx,
1067 password_key, nonce, shared_secret, hmac_out);
1068 }
1069
1070 // Combine nonce and shared_secret for HMAC computation
1071 // This binds the password to the DH shared secret, preventing MITM attacks
1073 memcpy(combined_data, nonce, AUTH_CHALLENGE_SIZE);
1074 memcpy(combined_data + AUTH_CHALLENGE_SIZE, shared_secret, CRYPTO_SHARED_KEY_SIZE);
1075
1076 // Compute HMAC using the password-derived key
1077 if (crypto_compute_hmac_ex(ctx, password_key, combined_data, 64, hmac_out) != 0) {
1078 // Securely zero sensitive data containing shared secret even on error
1079 sodium_memzero(combined_data, sizeof(combined_data));
1080 return SET_ERRNO(ERROR_CRYPTO, "Failed to compute password HMAC");
1081 }
1082
1083 // Securely zero sensitive data containing shared secret
1084 sodium_memzero(combined_data, sizeof(combined_data));
1085
1086 return ASCIICHAT_OK;
1087}
unsigned char uint8_t
Definition common.h:60
@ ERROR_CRYPTO
Definition error_codes.h:93
crypto_result_t crypto_compute_hmac_ex(const crypto_context_t *ctx, const uint8_t key[HMAC_SHA256_KEY_SIZE], const uint8_t *data, size_t data_len, uint8_t hmac[HMAC_SHA256_SIZE])
#define CRYPTO_SHARED_KEY_SIZE
Shared key size (X25519).
Definition sizes.h:71

References ASCIICHAT_OK, AUTH_CHALLENGE_SIZE, crypto_compute_hmac_ex(), CRYPTO_SHARED_KEY_SIZE, 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 <include/ascii-chat/crypto/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 963 of file lib/crypto/crypto.c.

964 {
965 if (!ctx || !ctx->initialized || !packet_out || !packet_len_out) {
966 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters (ctx=%p, initialized=%d, packet_out=%p, packet_len_out=%p)", ctx,
967 ctx ? ctx->initialized : 0, packet_out, packet_len_out);
969 }
970
971 size_t required_size = sizeof(uint32_t) + ctx->auth_challenge_size; // type + nonce
972 if (packet_size < required_size) {
973 SET_ERRNO(ERROR_BUFFER, "Buffer too small (size=%zu, required=%zu)", packet_size, required_size);
975 }
976
977 // Generate random nonce (stores result in ctx->auth_nonce)
979 if (result != CRYPTO_OK) {
980 return result;
981 }
982
983 // Pack packet: [type:4][nonce:auth_challenge_size]
984 uint32_t packet_type = CRYPTO_PACKET_AUTH_CHALLENGE;
985 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
986 SAFE_MEMCPY(packet_out + sizeof(packet_type), ctx->auth_challenge_size, ctx->auth_nonce, ctx->auth_challenge_size);
987
988 *packet_len_out = required_size;
989 return CRYPTO_OK;
990}
unsigned int uint32_t
Definition common.h:62
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:483
@ ERROR_BUFFER
crypto_result_t crypto_generate_nonce(uint8_t nonce[AUTH_CHALLENGE_SIZE])

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 <include/ascii-chat/crypto/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 711 of file lib/crypto/crypto.c.

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

659 {
660 if (!ctx || !ctx->initialized || !packet_out || !packet_len_out) {
662 "Invalid parameters (ctx=%p, initialized=%d, packet_out=%p, "
663 "packet_len_out=%p)",
664 ctx, ctx ? ctx->initialized : 0, packet_out, packet_len_out);
666 }
667
668 size_t required_size = sizeof(uint32_t) + ctx->public_key_size; // type + key
669 if (packet_size < required_size) {
670 SET_ERRNO(ERROR_BUFFER, "Buffer too small (size=%zu, required=%zu)", packet_size, required_size);
672 }
673
674 // Pack packet: [type:4][public_key:32]
675 uint32_t packet_type = CRYPTO_PACKET_PUBLIC_KEY;
676 SAFE_MEMCPY(packet_out, sizeof(packet_type), &packet_type, sizeof(packet_type));
677 // Bounds check to prevent buffer overflow
678 size_t copy_size = (ctx->public_key_size <= X25519_KEY_SIZE) ? ctx->public_key_size : X25519_KEY_SIZE;
679 SAFE_MEMCPY(packet_out + sizeof(packet_type), copy_size, ctx->public_key, copy_size);
680
681 *packet_len_out = required_size;
682 return CRYPTO_OK;
683}
#define X25519_KEY_SIZE
X25519 key size in bytes.
Definition sizes.h:31

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 <include/ascii-chat/crypto/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 511 of file lib/crypto/crypto.c.

512 {
513 ASCII_OTEL_SCOPE(CRYPTO_DECRYPT);
514 ASCII_OTEL_ATTR_STRING("crypto.operation", "decrypt");
515 ASCII_OTEL_ATTR_STRING("crypto.algorithm", "xsalsa20-poly1305");
516 ASCII_OTEL_ATTR_INT("crypto.input.size", (int64_t)ciphertext_len);
517 ASCII_OTEL_ATTR_INT("crypto.output.capacity", (int64_t)plaintext_out_size);
518 if (!ctx || !ctx->initialized || !ciphertext || !plaintext_out || !plaintext_len_out) {
520 "Invalid parameters: ctx=%p, initialized=%d, ciphertext=%p, plaintext_out=%p, plaintext_len_out=%p", ctx,
521 ctx ? ctx->initialized : 0, ciphertext, plaintext_out, plaintext_len_out);
523 }
524
525 if (!crypto_is_ready(ctx)) {
526 SET_ERRNO(ERROR_CRYPTO, "Crypto context not ready for decryption");
528 }
529
530 ASCII_OTEL_ATTR_INT("crypto.nonce.size", ctx->nonce_size);
531 ASCII_OTEL_ATTR_INT("crypto.mac.size", ctx->mac_size);
532 ASCII_OTEL_ATTR_INT("crypto.key.size", ctx->encryption_key_size);
533 ASCII_OTEL_ATTR_STRING("crypto.key.source", ctx->key_exchange_complete ? "x25519" : "password");
534 // Check minimum ciphertext size (nonce + MAC)
535 size_t min_ciphertext_size = ctx->nonce_size + ctx->mac_size;
536 if (ciphertext_len < min_ciphertext_size) {
537 SET_ERRNO(ERROR_INVALID_PARAM, "Ciphertext too small: %zu < %zu", ciphertext_len, min_ciphertext_size);
539 }
540
541 size_t plaintext_len = ciphertext_len - ctx->nonce_size - ctx->mac_size;
542 if (plaintext_out_size < plaintext_len) {
543 SET_ERRNO(ERROR_BUFFER, "Plaintext buffer too small: %zu < %zu", plaintext_out_size, plaintext_len);
545 }
546
547 // Extract nonce from beginning of ciphertext
548 const uint8_t *nonce = ciphertext;
549 const uint8_t *encrypted_data = ciphertext + ctx->nonce_size;
550
551 // Choose decryption key (prefer shared key over password key)
552 const uint8_t *decryption_key = NULL;
553 if (ctx->key_exchange_complete) {
554 decryption_key = ctx->shared_key;
555 } else if (ctx->has_password) {
556 decryption_key = ctx->password_key;
557 } else {
558 SET_ERRNO(ERROR_CRYPTO, "No decryption key available");
560 }
561
562 // Decrypt using NaCl secretbox (XSalsa20 + Poly1305)
563 if (crypto_secretbox_open_easy(plaintext_out, encrypted_data, ciphertext_len - ctx->nonce_size, nonce,
564 decryption_key) != 0) {
566 "Decryption failed: cipherlen=%zu key_exchange_complete=%d has_password=%d (invalid MAC)",
567 ciphertext_len - ctx->nonce_size, ctx->key_exchange_complete, ctx->has_password);
568 SET_ERRNO(ERROR_CRYPTO, "Decryption failed - invalid MAC or corrupted data");
570 }
571
572 *plaintext_len_out = plaintext_len;
573 ctx->bytes_decrypted += plaintext_len;
574
575 ASCII_OTEL_ATTR_INT("crypto.output.size", (int64_t)*plaintext_len_out);
576 ASCII_OTEL_ATTR_INT("crypto.authenticated", 1);
577 ascii_otel_metric(ASCII_OTEL_METRIC_CRYPTO_DECRYPT_BYTES, (double)ciphertext_len);
578 return CRYPTO_OK;
579}
#define US_PER_SEC_INT
Definition time.h:173
#define log_error_every(interval_us, fmt,...)
Rate-limited ERROR logging.
Definition log/log.h:758
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66

References ASCII_OTEL_ATTR_INT, ASCII_OTEL_ATTR_STRING, ascii_otel_metric(), ASCII_OTEL_SCOPE, 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, crypto_context_t::encryption_key_size, ERROR_BUFFER, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_password, crypto_context_t::initialized, crypto_context_t::key_exchange_complete, log_error_every, crypto_context_t::mac_size, crypto_context_t::nonce_size, crypto_context_t::password_key, SET_ERRNO, crypto_context_t::shared_key, and US_PER_SEC_INT.

Referenced by crypto_process_encrypted_packet(), and crypto_record_open_sequence().

◆ crypto_derive_password_encryption_key()

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

#include <include/ascii-chat/crypto/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.

References buffer_size, and CRYPTO_ENCRYPTION_KEY_SIZE.

◆ crypto_derive_password_key()

crypto_result_t crypto_derive_password_key ( crypto_context_t * ctx,
const char * password )

#include <include/ascii-chat/crypto/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 316 of file lib/crypto/crypto.c.

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

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 crypto_handshake_client_auth_response(), and crypto_init_with_password().

◆ crypto_destroy()

void crypto_destroy ( crypto_context_t * ctx)

#include <include/ascii-chat/crypto/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 197 of file lib/crypto/crypto.c.

197 {
198 if (!ctx || !ctx->initialized) {
199 return;
200 }
201
203
204 // Securely wipe sensitive data
205 secure_memzero(ctx->private_key, sizeof(ctx->private_key));
206 secure_memzero(ctx->shared_key, sizeof(ctx->shared_key));
207 secure_memzero(ctx->password_key, sizeof(ctx->password_key));
208 secure_memzero(ctx->password_salt, sizeof(ctx->password_salt));
209
210 char encrypted_str[CRYPTO_ENCRYPTED_BUFFER_SIZE], decrypted_str[CRYPTO_DECRYPTED_BUFFER_SIZE];
211 format_bytes_pretty(ctx->bytes_encrypted, encrypted_str, sizeof(encrypted_str));
212 format_bytes_pretty(ctx->bytes_decrypted, decrypted_str, sizeof(decrypted_str));
213
214 log_debug("Crypto context cleaned up (encrypted: %s, decrypted: %s)", encrypted_str, decrypted_str);
215
216 // Clear entire context
217 secure_memzero(ctx, sizeof(crypto_context_t));
218}
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Format byte count into human-readable string.
Definition util/format.c:11
#define CRYPTO_ENCRYPTED_BUFFER_SIZE
#define CRYPTO_DECRYPTED_BUFFER_SIZE
void crypto_session_destroy(crypto_context_t *crypto)
Endpoint cryptographic state for handshake, packet protection, authentication, and rekeying.

References crypto_context_t::bytes_decrypted, crypto_context_t::bytes_encrypted, CRYPTO_DECRYPTED_BUFFER_SIZE, CRYPTO_ENCRYPTED_BUFFER_SIZE, crypto_session_destroy(), 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 <include/ascii-chat/crypto/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 ASCII_OTEL_SCOPE(CRYPTO_ENCRYPT);
434 ASCII_OTEL_ATTR_STRING("crypto.operation", "encrypt");
435 ASCII_OTEL_ATTR_STRING("crypto.algorithm", "xsalsa20-poly1305");
436 ASCII_OTEL_ATTR_INT("crypto.input.size", (int64_t)plaintext_len);
437 ASCII_OTEL_ATTR_INT("crypto.output.capacity", (int64_t)ciphertext_out_size);
438 if (!ctx || !ctx->initialized || !plaintext || !ciphertext_out || !ciphertext_len_out) {
440 "Invalid parameters: ctx=%p, initialized=%d, plaintext=%p, ciphertext_out=%p, ciphertext_len_out=%p", ctx,
441 ctx ? ctx->initialized : 0, plaintext, ciphertext_out, ciphertext_len_out);
443 }
444
445 if (plaintext_len == 0 || plaintext_len > CRYPTO_MAX_PLAINTEXT_SIZE) {
446 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid plaintext length: %zu (max: %d)", plaintext_len, CRYPTO_MAX_PLAINTEXT_SIZE);
448 }
449
450 if (!crypto_is_ready(ctx)) {
451 SET_ERRNO(ERROR_CRYPTO, "Crypto context not ready for encryption");
453 }
454
455 ASCII_OTEL_ATTR_INT("crypto.nonce.size", ctx->nonce_size);
456 ASCII_OTEL_ATTR_INT("crypto.mac.size", ctx->mac_size);
457 ASCII_OTEL_ATTR_INT("crypto.key.size", ctx->encryption_key_size);
458 ASCII_OTEL_ATTR_STRING("crypto.key.source", ctx->key_exchange_complete ? "x25519" : "password");
459 // Check output buffer size
460 size_t required_size = plaintext_len + ctx->nonce_size + ctx->mac_size;
461 if (ciphertext_out_size < required_size) {
462 SET_ERRNO(ERROR_BUFFER, "Ciphertext buffer too small: %zu < %zu", ciphertext_out_size, required_size);
464 }
465
466 // Check for nonce counter exhaustion (extremely unlikely in practice)
467 // Starting from 1, reaching UINT64_MAX would require ~292 billion years at 60 FPS.
468 // With key rotation required at 1M packets (~16 seconds), exhaustion is virtually impossible.
469 // This check is a safety fallback that should never trigger in practice.
470 if (ctx->nonce_counter == 0 || ctx->nonce_counter == UINT64_MAX) {
471 SET_ERRNO(ERROR_CRYPTO, "Nonce counter exhausted - key rotation required");
473 }
474
475 // Choose encryption key BEFORE generating nonce (prevents nonce_counter increment on error)
476 const uint8_t *encryption_key = NULL;
477 if (ctx->key_exchange_complete) {
478 encryption_key = ctx->shared_key;
479 } else if (ctx->has_password) {
480 encryption_key = ctx->password_key;
481 } else {
482 SET_ERRNO(ERROR_CRYPTO, "No encryption key available");
484 }
485
486 // Generate nonce and place at beginning of ciphertext
487 uint8_t nonce[XSALSA20_NONCE_SIZE]; // Use maximum nonce size for buffer
488 generate_nonce(ctx, nonce);
489 SAFE_MEMCPY(ciphertext_out, ctx->nonce_size, nonce, ctx->nonce_size);
490
491 log_debug_every(US_PER_SEC_INT, "Encrypting packet: counter=%llu plaintext_len=%zu",
492 (unsigned long long)(ctx->nonce_counter - 1), plaintext_len);
493
494 // Encrypt using NaCl secretbox (XSalsa20 + Poly1305)
495 if (crypto_secretbox_easy(ciphertext_out + ctx->nonce_size, plaintext, plaintext_len, nonce, encryption_key) != 0) {
496 SET_ERRNO(ERROR_CRYPTO, "Encryption failed");
498 }
499
500 *ciphertext_len_out = required_size;
501 ctx->bytes_encrypted += plaintext_len;
502
503 // Increment rekey packet counter for rekeying trigger detection
504 ctx->rekey_packet_count++;
505
506 ASCII_OTEL_ATTR_INT("crypto.output.size", (int64_t)*ciphertext_len_out);
507 ascii_otel_metric(ASCII_OTEL_METRIC_CRYPTO_ENCRYPT_BYTES, (double)plaintext_len);
508 return CRYPTO_OK;
509}
#define log_debug_every(interval_us, fmt,...)
Rate-limited DEBUG logging.
Definition log/log.h:749
#define XSALSA20_NONCE_SIZE
XSalsa20 nonce size in bytes.
Definition sizes.h:39

References ASCII_OTEL_ATTR_INT, ASCII_OTEL_ATTR_STRING, ascii_otel_metric(), ASCII_OTEL_SCOPE, 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, crypto_context_t::encryption_key_size, ERROR_BUFFER, ERROR_CRYPTO, ERROR_INVALID_PARAM, crypto_context_t::has_password, crypto_context_t::initialized, crypto_context_t::key_exchange_complete, log_debug_every, 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, US_PER_SEC_INT, and XSALSA20_NONCE_SIZE.

Referenced by crypto_create_encrypted_packet(), and crypto_record_seal_into_domain().

◆ crypto_extract_auth_data()

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

#include <include/ascii-chat/crypto/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 1144 of file lib/crypto/crypto.c.

1144 {
1145 if (!combined_data || !hmac_out || !challenge_out) {
1146 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: combined_data=%p, hmac_out=%p, challenge_out=%p", combined_data,
1147 hmac_out, challenge_out);
1148 return;
1149 }
1150
1151 // Extract HMAC (first 32 bytes) and challenge nonce (last 32 bytes)
1152 memcpy(hmac_out, combined_data, HMAC_SHA256_SIZE);
1153 memcpy(challenge_out, combined_data + AUTH_CHALLENGE_SIZE, AUTH_CHALLENGE_SIZE);
1154}

References AUTH_CHALLENGE_SIZE, ERROR_INVALID_PARAM, HMAC_SHA256_SIZE, and SET_ERRNO.

◆ crypto_generate_keypair()

crypto_result_t crypto_generate_keypair ( crypto_context_t * ctx)

#include <include/ascii-chat/crypto/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 220 of file lib/crypto/crypto.c.

220 {
221 if (!ctx) {
222 SET_ERRNO(ERROR_INVALID_PARAM, "NULL context");
224 }
225
226 log_debug("About to call crypto_box_keypair");
227 // Generate X25519 key pair for key exchange
228 if (crypto_box_keypair(ctx->public_key, ctx->private_key) != 0) {
229 SET_ERRNO(ERROR_CRYPTO, "Failed to generate X25519 key pair");
231 }
232
233 log_debug("Generated X25519 key pair for key exchange");
234 return CRYPTO_OK;
235}

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

Referenced by crypto_init().

◆ crypto_generate_nonce()

crypto_result_t crypto_generate_nonce ( uint8_t nonce[32])

#include <include/ascii-chat/crypto/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.

References AUTH_CHALLENGE_SIZE.

◆ crypto_get_public_key()

crypto_result_t crypto_get_public_key ( const crypto_context_t * ctx,
uint8_t * public_key_out )

#include <include/ascii-chat/crypto/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 241 of file lib/crypto/crypto.c.

241 {
242 if (!ctx || !ctx->initialized || !public_key_out) {
243 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters (ctx=%p, initialized=%d, public_key_out=%p)", ctx,
244 ctx ? ctx->initialized : 0, public_key_out);
246 }
247
248 // Bounds check to prevent buffer overflow
249 size_t copy_size = (ctx->public_key_size <= X25519_KEY_SIZE) ? ctx->public_key_size : X25519_KEY_SIZE;
250 SAFE_MEMCPY(public_key_out, copy_size, ctx->public_key, copy_size);
251 return CRYPTO_OK;
252}

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 <include/ascii-chat/crypto/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 1356 of file lib/crypto/crypto.c.

1356 {
1357 if (!ctx || !status_buffer || buffer_size == 0) {
1358 return;
1359 }
1360
1361 time_t now = time(NULL);
1362 time_t elapsed = now - ctx->rekey_last_time;
1363 time_t remaining_time = (ctx->rekey_time_threshold > elapsed) ? (ctx->rekey_time_threshold - elapsed) : 0;
1364 uint64_t remaining_packets = (ctx->rekey_packet_threshold > ctx->rekey_packet_count)
1366 : 0;
1367
1368 safe_snprintf(status_buffer, buffer_size,
1369 "Rekey status: %s | "
1370 "Packets: %llu/%llu (%llu remaining) | "
1371 "Time: %ld/%ld sec (%ld sec remaining) | "
1372 "Rekeys: %llu | Failures: %d",
1373 ctx->rekey_in_progress ? "IN_PROGRESS" : "IDLE", (unsigned long long)ctx->rekey_packet_count,
1374 (unsigned long long)ctx->rekey_packet_threshold, (unsigned long long)remaining_packets, (long)elapsed,
1375 (long)ctx->rekey_time_threshold, (long)remaining_time, (unsigned long long)ctx->rekey_count,
1376 ctx->rekey_failure_count);
1377}
int buffer_size
Size of circular buffer.
Definition grep.c:68
unsigned long long uint64_t
Definition common.h:63

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 <include/ascii-chat/crypto/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 614 of file lib/crypto/crypto.c.

614 {
615 if (!ctx || !status_buffer || buffer_size == 0) {
616 return;
617 }
618
619 if (!ctx->initialized) {
620 SAFE_SNPRINTF(status_buffer, buffer_size, "Not initialized");
621 return;
622 }
623
624 SAFE_SNPRINTF(status_buffer, buffer_size,
625 "Initialized: %s, Password: %s, Key Exchange: %s, Ready: %s, "
626 "Encrypted: %" PRIu64 " bytes, Decrypted: %" PRIu64 " bytes, Nonce: %" PRIu64,
627 ctx->initialized ? "yes" : "no", ctx->has_password ? "yes" : "no",
628 ctx->key_exchange_complete ? "complete" : "incomplete", crypto_is_ready(ctx) ? "yes" : "no",
630}
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507

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 <include/ascii-chat/crypto/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 90 of file lib/crypto/crypto.c.

90 {
91 if (!ctx) {
92 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p", ctx);
94 }
95
96 // Initialize libsodium
97 crypto_result_t result = init_libsodium();
98 if (result != CRYPTO_OK) {
99 return result;
100 }
101
102 // Clear context
103 secure_memzero(ctx, sizeof(crypto_context_t));
104
105 // Generate key pair for X25519 key exchange
106 result = crypto_generate_keypair(ctx);
107 if (result != CRYPTO_OK) {
108 return result;
109 }
110
111 ctx->initialized = true;
112 ctx->has_password = false;
113 ctx->key_exchange_complete = false;
114 ctx->peer_key_received = false;
115 ctx->handshake_complete = false;
116 ctx->encrypt_data = true; // Default: enable payload encryption
117 ctx->nonce_counter = 1; // Start from 1 (0 reserved for testing)
118 ctx->bytes_encrypted = 0;
119 ctx->bytes_decrypted = 0;
120
121 // Set default algorithm-specific parameters (can be overridden during handshake)
131 // signature_size and auth_public_key_size remain 0 until set by CRYPTO_PARAMETERS
132 // (must be 0 for unauthenticated format detection in crypto_handshake_client_key_exchange)
133
134 // Generate a random nonce prefix for this connection.
135 randombytes_buf(ctx->session_id, sizeof(ctx->session_id));
136
137 // Initialize rekeying state
138 ctx->rekey_packet_count = 0;
139 ctx->rekey_last_time = time(NULL); // Initialize to current time
140 ctx->rekey_last_request_time = 0; // No rekey request yet
141 ctx->rekey_in_progress = false;
142 ctx->rekey_failure_count = 0;
143 ctx->has_temp_key = false;
144 ctx->rekey_count = 0;
145
146 // SECURITY: Use production-safe rekey thresholds by default
147 // Rekey every 1 hour OR 1 million packets (whichever comes first)
148 // Only use test mode if explicitly requested via environment variable
149 if (is_test_environment()) {
152 char duration_str[TIME_DURATION_BUFFER_SIZE];
153 time_pretty(ctx->rekey_time_threshold, -1, duration_str, sizeof(duration_str));
154 log_dev("Crypto context initialized with X25519 key exchange (TEST MODE rekey thresholds: %llu packets, %s)",
155 (unsigned long long)ctx->rekey_packet_threshold, duration_str);
156 } else {
157 ctx->rekey_packet_threshold = REKEY_DEFAULT_PACKET_THRESHOLD; // 1 million packets
158 ctx->rekey_time_threshold = REKEY_DEFAULT_TIME_THRESHOLD; // already in nanoseconds (3600 * NS_PER_SEC_INT)
159 char duration_str[TIME_DURATION_BUFFER_SIZE];
160 time_pretty(ctx->rekey_time_threshold, -1, duration_str, sizeof(duration_str));
161 log_dev("Crypto context initialized with X25519 key exchange (rekey thresholds: %llu packets, %s)",
162 (unsigned long long)ctx->rekey_packet_threshold, duration_str);
163 }
165}
#define REKEY_DEFAULT_TIME_THRESHOLD
Default rekey time threshold (1 hour in nanoseconds).
crypto_result_t crypto_generate_keypair(crypto_context_t *ctx)
Generate new X25519 key pair for key exchange.
#define REKEY_TEST_PACKET_THRESHOLD
Test mode rekey packet threshold (1000 packets).
#define REKEY_TEST_TIME_THRESHOLD
Test mode rekey time threshold (30 seconds in nanoseconds).
#define REKEY_DEFAULT_PACKET_THRESHOLD
Default rekey packet threshold (1 million packets).
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define TIME_DURATION_BUFFER_SIZE
Definition time.h:40
asciichat_error_t crypto_session_create(crypto_context_t *crypto)
#define SECRETBOX_KEY_SIZE
Secretbox key size in bytes.
Definition sizes.h:49
#define ARGON2ID_SALT_SIZE
Argon2id salt size in bytes.
Definition sizes.h:47
#define POLY1305_MAC_SIZE
Poly1305 MAC size in bytes.
Definition sizes.h:41

References ARGON2ID_SALT_SIZE, ASCIICHAT_OK, AUTH_CHALLENGE_SIZE, crypto_context_t::auth_challenge_size, crypto_context_t::bytes_decrypted, crypto_context_t::bytes_encrypted, CRYPTO_ERROR_INVALID_PARAMS, CRYPTO_ERROR_MEMORY, crypto_generate_keypair(), CRYPTO_OK, crypto_session_create(), 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, 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_DURATION_BUFFER_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 <include/ascii-chat/crypto/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 167 of file lib/crypto/crypto.c.

167 {
168 if (!ctx || !password) {
169 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ctx=%p, password=%p", ctx, password);
171 }
172
173 if (strlen(password) == 0) {
174 SET_ERRNO(ERROR_INVALID_PARAM, "Password cannot be empty");
176 }
177
178 // First initialize basic crypto
179 crypto_result_t result = crypto_init(ctx);
180 if (result != CRYPTO_OK) {
181 return result;
182 }
183
184 // Derive password key
185 result = crypto_derive_password_key(ctx, password);
186 if (result != CRYPTO_OK) {
187 crypto_destroy(ctx);
188 return result;
189 }
190
191 ctx->has_password = true;
192
193 log_dev("Crypto context initialized with password-based encryption");
194 return CRYPTO_OK;
195}
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 <include/ascii-chat/crypto/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 280 of file lib/crypto/crypto.c.

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

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

Referenced by acip_packet_plan(), 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 <include/ascii-chat/crypto/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 992 of file lib/crypto/crypto.c.

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

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 <include/ascii-chat/crypto/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 1022 of file lib/crypto/crypto.c.

1022 {
1023 if (!ctx || !ctx->initialized || !packet) {
1024 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid context or packet (ctx=%p, initialized=%d, packet=%p)", ctx,
1025 ctx ? ctx->initialized : 0, packet);
1027 }
1028
1029 size_t expected_size = sizeof(uint32_t) + 32; // type + hmac
1030 if (packet_len != expected_size) {
1031 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid packet size (expected=%zu, got=%zu)", expected_size, packet_len);
1033 }
1034
1035 // Unpack packet: [type:4][hmac:32]
1036 uint32_t packet_type;
1037 SAFE_MEMCPY(&packet_type, sizeof(packet_type), packet, sizeof(packet_type));
1038
1039 if (packet_type != CRYPTO_PACKET_AUTH_RESPONSE) {
1040 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid packet type (expected=0x%x, got=0x%x)", CRYPTO_PACKET_AUTH_RESPONSE,
1041 packet_type);
1043 }
1044
1045 const uint8_t *received_hmac = packet + sizeof(packet_type);
1046
1047 // Verify HMAC using shared secret
1048 if (!crypto_verify_hmac(ctx->shared_key, ctx->auth_nonce, received_hmac)) {
1049 SET_ERRNO(ERROR_CRYPTO, "HMAC verification failed");
1051 }
1052
1053 ctx->handshake_complete = true;
1054 log_debug("Authentication successful - handshake complete");
1055 return CRYPTO_OK;
1056}
bool crypto_verify_hmac(const uint8_t key[HMAC_SHA256_KEY_SIZE], const uint8_t data[AUTH_CHALLENGE_SIZE], const uint8_t expected_hmac[HMAC_SHA256_SIZE])

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 <include/ascii-chat/crypto/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 770 of file lib/crypto/crypto.c.

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

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

639 {
640 if (!buffer || len == 0) {
641 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters (buffer=%p, len=%zu)", buffer, len);
643 }
644
645 crypto_result_t result = init_libsodium();
646 if (result != CRYPTO_OK) {
647 return result;
648 }
649
650 randombytes_buf(buffer, len);
651 return CRYPTO_OK;
652}

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 <include/ascii-chat/crypto/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 1334 of file lib/crypto/crypto.c.

1334 {
1335 if (!ctx) {
1336 return;
1337 }
1338
1339 log_warn("Aborting rekey (attempt %d failed), keeping old encryption key", ctx->rekey_failure_count + 1);
1340
1341 // Wipe temporary keys securely
1342 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1343 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1344 secure_memzero(ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1345
1346 // Reset rekey state
1347 ctx->has_temp_key = false;
1348 ctx->rekey_in_progress = false;
1349
1350 // Increment failure counter for exponential backoff
1351 ctx->rekey_failure_count++;
1352
1353 // Continue using old key - no disruption to connection
1354}

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_rekey_process_response(), and crypto_rekey_session_cleanup().

◆ crypto_rekey_commit()

crypto_result_t crypto_rekey_commit ( crypto_context_t * ctx)

#include <include/ascii-chat/crypto/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 1290 of file lib/crypto/crypto.c.

1290 {
1291 if (!ctx || !ctx->initialized) {
1292 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid context");
1294 }
1295
1296 if (!ctx->rekey_in_progress || !ctx->has_temp_key) {
1297 SET_ERRNO(ERROR_CRYPTO, "No rekey in progress to commit");
1299 }
1300
1301 // Wipe old shared secret securely
1302 secure_memzero(ctx->shared_key, sizeof(ctx->shared_key));
1303
1304 // Switch to new shared secret
1305 SAFE_MEMCPY(ctx->shared_key, sizeof(ctx->shared_key), ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1306
1307 // Reset nonce counter to 1 (fresh start with new key)
1308 ctx->nonce_counter = 1;
1309
1310 // Generate a new random nonce prefix for this key epoch.
1311 randombytes_buf(ctx->session_id, sizeof(ctx->session_id));
1312
1313 // Reset rekey tracking
1314 ctx->rekey_packet_count = 0;
1315 ctx->rekey_last_time = time(NULL);
1316 ctx->rekey_count++;
1317
1318 // Clear rekey state
1319 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1320 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1321 secure_memzero(ctx->temp_shared_key, sizeof(ctx->temp_shared_key));
1322 ctx->has_temp_key = false;
1323 ctx->rekey_in_progress = false;
1324
1325 // Reset failure counter on successful rekey
1326 ctx->rekey_failure_count = 0;
1327
1328 log_debug("Rekey committed successfully (rekey #%llu, nonce reset to 1, new session_id generated)",
1329 (unsigned long long)ctx->rekey_count);
1330
1331 return CRYPTO_OK;
1332}

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_record_commit_epoch().

◆ crypto_rekey_init()

crypto_result_t crypto_rekey_init ( crypto_context_t * ctx)

#include <include/ascii-chat/crypto/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 1194 of file lib/crypto/crypto.c.

1194 {
1195 if (!ctx || !ctx->initialized) {
1196 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid context");
1198 }
1199
1200 if (ctx->rekey_in_progress) {
1201 SET_ERRNO(ERROR_CRYPTO, "Rekey already in progress");
1203 }
1204
1205 // Rate limiting: check minimum interval since last rekey
1206 time_t now = time(NULL);
1207 time_t since_last_rekey = now - ctx->rekey_last_time;
1208 time_t min_interval_seconds = (time_t)(REKEY_MIN_INTERVAL / NS_PER_SEC_INT);
1209 if (since_last_rekey < min_interval_seconds) {
1210 char elapsed_str[TIME_DURATION_BUFFER_SIZE], min_str[TIME_DURATION_BUFFER_SIZE];
1211 time_pretty((uint64_t)(since_last_rekey * 1e9), -1, elapsed_str, sizeof(elapsed_str));
1212 time_pretty((uint64_t)(min_interval_seconds * 1e9), -1, min_str, sizeof(min_str));
1213 log_warn("Rekey rate limited: %s since last rekey (minimum: %s)", elapsed_str, min_str);
1215 }
1216
1217 // Check if too many consecutive failures
1219 log_error("Too many consecutive rekey failures (%d), giving up", ctx->rekey_failure_count);
1221 }
1222
1223 // Generate new ephemeral X25519 keypair for rekeying
1224 if (crypto_box_keypair(ctx->temp_public_key, ctx->temp_private_key) != 0) {
1225 SET_ERRNO(ERROR_CRYPTO, "Failed to generate rekey ephemeral keypair");
1227 }
1228
1229 ctx->rekey_in_progress = true;
1230 ctx->has_temp_key = true;
1231
1232 log_debug("Rekey initiated (packets: %llu, time elapsed: %ld sec, attempt %d)",
1233 (unsigned long long)ctx->rekey_packet_count, (long)since_last_rekey, ctx->rekey_failure_count + 1);
1234
1235 return CRYPTO_OK;
1236}
#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 log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define NS_PER_SEC_INT
Definition time.h:169

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, TIME_DURATION_BUFFER_SIZE, and time_pretty().

Referenced by crypto_rekey_session_tick().

◆ crypto_rekey_process_request()

crypto_result_t crypto_rekey_process_request ( crypto_context_t * ctx,
const uint8_t * peer_new_public_key )

#include <include/ascii-chat/crypto/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 1238 of file lib/crypto/crypto.c.

1238 {
1239 if (!ctx || !ctx->initialized || !peer_new_public_key) {
1240 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
1242 }
1243
1244 // Generate our own new ephemeral keypair (responder side)
1245 if (crypto_box_keypair(ctx->temp_public_key, ctx->temp_private_key) != 0) {
1246 SET_ERRNO(ERROR_CRYPTO, "Failed to generate rekey ephemeral keypair");
1248 }
1249
1250 // Compute new shared secret: DH(our_new_private_key, peer_new_public_key)
1251 if (crypto_scalarmult(ctx->temp_shared_key, ctx->temp_private_key, peer_new_public_key) != 0) {
1252 SET_ERRNO(ERROR_CRYPTO, "Failed to compute rekey shared secret");
1253 secure_memzero(ctx->temp_private_key, sizeof(ctx->temp_private_key));
1254 secure_memzero(ctx->temp_public_key, sizeof(ctx->temp_public_key));
1256 }
1257
1258 ctx->rekey_in_progress = true;
1259 ctx->has_temp_key = true;
1260
1261 log_debug("Rekey request processed (responder side), new shared secret computed");
1262
1263 return CRYPTO_OK;
1264}

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_rekey_session_tick().

◆ crypto_rekey_process_response()

crypto_result_t crypto_rekey_process_response ( crypto_context_t * ctx,
const uint8_t * peer_new_public_key )

#include <include/ascii-chat/crypto/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 1266 of file lib/crypto/crypto.c.

1266 {
1267 if (!ctx || !ctx->initialized || !peer_new_public_key) {
1268 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
1270 }
1271
1272 if (!ctx->rekey_in_progress || !ctx->has_temp_key) {
1273 SET_ERRNO(ERROR_CRYPTO, "No rekey in progress");
1275 }
1276
1277 // Compute new shared secret: DH(our_new_private_key, peer_new_public_key)
1278 // Use crypto_scalarmult to produce raw 32-byte shared secret for crypto_secretbox
1279 if (crypto_scalarmult(ctx->temp_shared_key, ctx->temp_private_key, peer_new_public_key) != 0) {
1280 SET_ERRNO(ERROR_CRYPTO, "Failed to compute rekey shared secret");
1281 crypto_rekey_abort(ctx);
1283 }
1284
1285 log_debug("Rekey response processed (initiator side), new shared secret computed");
1286
1287 return CRYPTO_OK;
1288}
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_rekey_session_receive().

◆ crypto_result_to_string()

const char * crypto_result_to_string ( crypto_result_t result)

#include <include/ascii-chat/crypto/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 606 of file lib/crypto/crypto.c.

606 {
607 int idx = -result; // CRYPTO_OK=0, errors are negative
608 if (idx >= 0 && (size_t)idx < CRYPTO_RESULT_STRING_COUNT) {
609 return g_crypto_result_strings[idx];
610 }
611 return "Unknown error";
612}
#define CRYPTO_RESULT_STRING_COUNT

References CRYPTO_RESULT_STRING_COUNT.

Referenced by crypto_handshake_client_auth_response(), crypto_handshake_client_key_exchange(), crypto_handshake_init(), crypto_handshake_init_with_password(), crypto_handshake_server_auth_challenge(), and crypto_handshake_server_complete().

◆ crypto_secure_compare()

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

#include <include/ascii-chat/crypto/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 632 of file lib/crypto/crypto.c.

632 {
633 if (!lhs || !rhs) {
634 return false;
635 }
636 return sodium_memcmp(lhs, rhs, len) == 0;
637}

◆ crypto_set_peer_public_key()

crypto_result_t crypto_set_peer_public_key ( crypto_context_t * ctx,
const uint8_t * peer_public_key )

#include <include/ascii-chat/crypto/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 254 of file lib/crypto/crypto.c.

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

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 <include/ascii-chat/crypto/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 1160 of file lib/crypto/crypto.c.

1160 {
1161 if (!ctx || !ctx->initialized) {
1162 return false;
1163 }
1164
1165 // Don't trigger rekey if one is already in progress
1166 if (ctx->rekey_in_progress) {
1167 return false;
1168 }
1169
1170 // Don't trigger rekey if handshake isn't complete yet
1171 if (!ctx->handshake_complete) {
1172 return false;
1173 }
1174
1175 // Check packet count threshold
1176 if (ctx->rekey_packet_count >= ctx->rekey_packet_threshold) {
1177 log_debug("Rekey triggered: packet count (%llu) >= threshold (%llu)", (unsigned long long)ctx->rekey_packet_count,
1178 (unsigned long long)ctx->rekey_packet_threshold);
1179 return true;
1180 }
1181
1182 // Check time threshold
1183 time_t now = time(NULL);
1184 time_t elapsed = now - ctx->rekey_last_time;
1185 if (elapsed >= ctx->rekey_time_threshold) {
1186 log_debug("Rekey triggered: time elapsed (%ld sec) >= threshold (%ld sec)", (long)elapsed,
1187 (long)ctx->rekey_time_threshold);
1188 return true;
1189 }
1190
1191 return false;
1192}

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.

◆ 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 <include/ascii-chat/crypto/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 1108 of file lib/crypto/crypto.c.

1109 {
1110 if (!private_key || !ephemeral_key || !signature_out) {
1111 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: private_key=%p, ephemeral_key=%p, signature_out=%p",
1112 private_key, ephemeral_key, signature_out);
1113 }
1114
1115 if (ephemeral_key_size == 0) {
1116 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ephemeral key size: %zu", ephemeral_key_size);
1117 return ERROR_INVALID_PARAM;
1118 }
1119
1120 // Sign the ephemeral key with our Ed25519 private key
1121 if (private_key->type == KEY_TYPE_ED25519) {
1122 if (crypto_sign_detached(signature_out, NULL, ephemeral_key, ephemeral_key_size, private_key->key.ed25519) != 0) {
1123 return SET_ERRNO(ERROR_CRYPTO, "Failed to sign ephemeral key");
1124 }
1125 } else {
1126 return SET_ERRNO(ERROR_CRYPTO, "Unsupported private key type for signing");
1127 }
1128
1129 return ASCIICHAT_OK;
1130}
uint8_t ed25519[ED25519_PRIVATE_KEY_SIZE]
Definition key_types.h:100
key_type_t type
Definition key_types.h:98
union private_key_t::@040247167361101132173265304166142033301213115260 key
@ KEY_TYPE_ED25519
Definition key_types.h:54

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 <include/ascii-chat/crypto/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 293 of file lib/crypto/crypto.c.

293 {
294 if (!password) {
295 SET_ERRNO(ERROR_INVALID_PARAM, "Password is NULL");
297 }
298
299 size_t password_len = strlen(password);
300
301 if (password_len < MIN_PASSWORD_LENGTH) {
302 SET_ERRNO(ERROR_INVALID_PARAM, "Password too short (minimum %d characters, got %zu)", MIN_PASSWORD_LENGTH,
303 password_len);
305 }
306
307 if (password_len > MAX_PASSWORD_LENGTH) {
308 SET_ERRNO(ERROR_INVALID_PARAM, "Password too long (maximum %d characters, got %zu)", MAX_PASSWORD_LENGTH,
309 password_len);
311 }
312
313 return CRYPTO_OK;
314}
#define MIN_PASSWORD_LENGTH
Minimum password length (8 characters).
Definition sizes.h:19
#define MAX_PASSWORD_LENGTH
Maximum password length (256 characters).
Definition sizes.h:21

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 <include/ascii-chat/crypto/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.

References AUTH_CHALLENGE_SIZE, and HMAC_SHA256_SIZE.

◆ crypto_verify_hmac()

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

#include <include/ascii-chat/crypto/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.

References AUTH_CHALLENGE_SIZE, HMAC_SHA256_KEY_SIZE, and HMAC_SHA256_SIZE.

◆ 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 <include/ascii-chat/crypto/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.

References HMAC_SHA256_KEY_SIZE, and HMAC_SHA256_SIZE.

◆ crypto_verify_password()

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

#include <include/ascii-chat/crypto/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 354 of file lib/crypto/crypto.c.

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

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 <include/ascii-chat/crypto/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 1089 of file lib/crypto/crypto.c.

1090 {
1091 if (!peer_public_key || !ephemeral_key || !signature) {
1092 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: peer_public_key=%p, ephemeral_key=%p, signature=%p",
1093 peer_public_key, ephemeral_key, signature);
1094 }
1095
1096 if (ephemeral_key_size == 0) {
1097 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ephemeral key size: %zu", ephemeral_key_size);
1098 }
1099
1100 // Verify the signature using Ed25519
1101 if (crypto_sign_verify_detached(signature, ephemeral_key, ephemeral_key_size, peer_public_key) != 0) {
1102 return SET_ERRNO(ERROR_CRYPTO, "Peer signature verification failed");
1103 }
1104
1105 return ASCIICHAT_OK;
1106}

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 <include/ascii-chat/crypto/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_keyFirst stored server key (32 bytes), or NULL if no stored public identity is available
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.

References ED25519_PUBLIC_KEY_SIZE.

◆ free_ta_contents()

void free_ta_contents ( br_x509_trust_anchor * ta)

#include <include/ascii-chat/crypto/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 436 of file pem.c.

436 {
437 if (!ta) {
438 return;
439 }
440 xfree(ta->dn.data);
441 switch (ta->pkey.key_type) {
442 case BR_KEYTYPE_RSA:
443 xfree((void *)ta->pkey.key.rsa.n);
444 xfree((void *)ta->pkey.key.rsa.e);
445 break;
446 case BR_KEYTYPE_EC:
447 xfree((void *)ta->pkey.key.ec.q);
448 break;
449 default:
450 SET_ERRNO(ERROR_CRYPTO, "Unknown public key type in CA");
451 break;
452 }
453}

References ERROR_CRYPTO, and SET_ERRNO.

Referenced by read_trust_anchors_from_memory().

◆ get_known_hosts_path()

const char * get_known_hosts_path ( void )

#include <lib/crypto/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 57 of file known_hosts.c.

57 {
58 // Return cached path if already determined
59 if (!g_known_hosts_path_cache) {
60 char *config_dir = get_config_dir();
61 if (!config_dir) {
62 log_error("Failed to determine configuration directory for known_hosts");
63 return NULL;
64 }
65 defer(SAFE_FREE(config_dir));
66
67 // Build path: config_dir + "known_hosts"
68 size_t config_len = strlen(config_dir);
69 size_t total_len = config_len + strlen("known_hosts") + 1;
70 char *path = SAFE_MALLOC(total_len, char *);
71 if (!path) {
72 log_error("Failed to allocate memory for known_hosts path");
73 return NULL;
74 }
75
76 safe_snprintf(path, total_len, "%sknown_hosts", config_dir);
77 g_known_hosts_path_cache = path;
78 log_debug("KNOWN_HOSTS: Using path %s", g_known_hosts_path_cache);
79 }
80 return g_known_hosts_path_cache;
81}
#define SAFE_MALLOC(size, cast)
Definition common.h:265
char * get_config_dir(void)
Get configuration directory path with XDG_CONFIG_HOME support.
Definition path.c:281

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 <include/ascii-chat/crypto/gpg/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 103 of file agent.c.

103 {
104 char agent_path[PLATFORM_MAX_PATH_LENGTH];
105 if (platform_get_gpg_agent_socket(agent_path, sizeof(agent_path)) != 0) {
106 log_error("Failed to get GPG agent path");
107 return -1;
108 }
109
110 log_debug("Connecting to GPG agent at: %s", agent_path);
111
112 // Use platform abstraction for pipe/socket connection
113 pipe_t pipe = platform_pipe_connect(agent_path);
114 if (!platform_pipe_is_valid(pipe)) {
115 log_error("Failed to connect to GPG agent");
116 return -1;
117 }
118
119 // Read initial greeting
120 char response[GPG_AGENT_MAX_RESPONSE];
121 if (read_agent_line(pipe, response, sizeof(response), NULL) != 0) {
122 log_error("Failed to read GPG agent greeting");
124 return -1;
125 }
126
127 if (!is_ok_response(response)) {
128 log_error("Unexpected GPG agent greeting: %s", response);
130 return -1;
131 }
132
133 log_debug("Connected to GPG agent successfully");
134
135 // Set loopback pinentry mode to avoid interactive prompts
136 // This allows GPG agent to work in non-interactive environments
137 if (send_agent_command(pipe, "OPTION pinentry-mode=loopback") != 0) {
138 log_warn("Failed to set loopback pinentry mode (continuing anyway)");
139 } else {
140 // Read response for OPTION command
141 if (read_agent_line(pipe, response, sizeof(response), NULL) != 0) {
142 log_warn("Failed to read OPTION command response (continuing anyway)");
143 } else if (is_ok_response(response)) {
144 log_debug("Loopback pinentry mode enabled");
145 } else {
146 log_warn("Failed to enable loopback pinentry mode: %s (continuing anyway)", response);
147 }
148 }
149
150 return (int)(intptr_t)pipe;
151}
#define GPG_AGENT_MAX_RESPONSE
Definition agent.c:35
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
pipe_t platform_pipe_connect(const char *path)
Connect to an agent via named pipe (Windows) or Unix socket (POSIX).
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.

References GPG_AGENT_MAX_RESPONSE, log_debug, log_error, log_warn, 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 <include/ascii-chat/crypto/gpg/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 153 of file agent.c.

153 {
154 if (handle_as_int >= 0) {
155 pipe_t pipe = (pipe_t)(intptr_t)handle_as_int;
156 send_agent_command(pipe, "BYE");
158 }
159}

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 <include/ascii-chat/crypto/gpg/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 344 of file agent.c.

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

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 <include/ascii-chat/crypto/gpg/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 161 of file agent.c.

162 {
163 if (handle_as_int < 0 || !keygrip || !message || !signature_out || !signature_len_out) {
164 log_error("Invalid arguments");
165 return -1;
166 }
167
168 pipe_t handle = (pipe_t)(intptr_t)handle_as_int;
169 char response[GPG_AGENT_MAX_RESPONSE];
170
171 // 1. Set the key to use (SIGKEY command)
173 safe_snprintf(sigkey_cmd, sizeof(sigkey_cmd), "SIGKEY %s", keygrip);
174 if (send_agent_command(handle, sigkey_cmd) != 0) {
175 log_error("Failed to send SIGKEY command");
176 return -1;
177 }
178
179 if (read_agent_line(handle, response, sizeof(response), NULL) != 0) {
180 log_error("Failed to read SIGKEY response");
181 return -1;
182 }
183
184 if (!is_ok_response(response)) {
185 log_error("SIGKEY failed: %s", response);
186 return -1;
187 }
188
189 // 2. For EdDSA/Ed25519, GPG agent requires SETHASH with a hash algorithm
190 // GPG agent doesn't support --inquire for SETHASH - the command syntax is:
191 // SETHASH (--hash=<name>)|(<algonumber>) <hexstring>
192 // For Ed25519, we hash the message with SHA512 (algo 10) first
193
194 // Hash the message with SHA512 using libsodium
195 uint8_t hash[crypto_hash_sha512_BYTES];
196 crypto_hash_sha512(hash, message, message_len);
197
198 // Build SETHASH command with SHA512 hash (algo 10)
199 // Format: "SETHASH 10 <128 hex chars for 64-byte SHA512 hash>"
201 int offset = safe_snprintf(sethash_cmd, sizeof(sethash_cmd), "SETHASH 10 ");
202 for (size_t i = 0; i < crypto_hash_sha512_BYTES; i++) {
203 offset += safe_snprintf(sethash_cmd + offset, sizeof(sethash_cmd) - (size_t)offset, "%02X", hash[i]);
204 }
205
206 log_debug("Sending SETHASH command with SHA512 hash");
207 if (send_agent_command(handle, sethash_cmd) != 0) {
208 log_error("Failed to send SETHASH command");
209 return -1;
210 }
211
212 // Read SETHASH response
213 if (read_agent_line(handle, response, sizeof(response), NULL) != 0) {
214 log_error("Failed to read SETHASH response");
215 return -1;
216 }
217
218 if (!is_ok_response(response)) {
219 log_error("SETHASH failed: %s", response);
220 return -1;
221 }
222
223 // 3. Request signature using PKSIGN
224 if (send_agent_command(handle, "PKSIGN") != 0) {
225 log_error("Failed to send PKSIGN command");
226 return -1;
227 }
228
229 // Read response - skip status/error lines and wait for data line (D ...)
230 // GPG agent sends informational ERR lines that are not fatal (e.g., "Not implemented")
231 // Keep reading until we get the actual signature data
232 bool found_data = false;
233 size_t response_len = 0;
234 for (int attempts = 0; attempts < 20; attempts++) {
235 if (read_agent_line(handle, response, sizeof(response), &response_len) != 0) {
236 log_error("Failed to read PKSIGN response");
237 return -1;
238 }
239
240 log_debug("PKSIGN response line %d (%zu bytes)", attempts + 1, response_len);
241
242 // Skip status lines (S INQUIRE_MAXLEN, etc)
243 if (response[0] == 'S' && response[1] == ' ') {
244 log_debug("Skipping PKSIGN status line: %s", response);
245 continue;
246 }
247
248 // Skip informational ERR lines (GPG agent sends these even on success)
249 // Common ERR codes: 67109141 (IPC cancelled), 67108933 (Not implemented)
250 if (strncmp(response, "ERR", 3) == 0) {
251 log_debug("Skipping PKSIGN error line (informational): %s", response);
252 continue;
253 }
254
255 // Check if it's a data line (D followed by space)
256 if (response[0] == 'D' && response[1] == ' ') {
257 log_debug("Found signature data line");
258 found_data = true;
259 break;
260 }
261
262 // Check for OK (success without data would be unexpected)
263 if (strncmp(response, "OK", 2) == 0) {
264 log_warn("PKSIGN returned OK without data line");
265 continue; // Keep trying in case D line follows
266 }
267
268 // Check if GPG agent is sending another INQUIRE (shouldn't happen)
269 if (strncmp(response, "INQUIRE", 7) == 0) {
270 log_error("Unexpected INQUIRE after PKSIGN: %s", response);
271 return -1;
272 }
273
274 // Unknown response type
275 log_warn("Unexpected PKSIGN response (attempt %d): %s", attempts + 1, response);
276 }
277
278 if (!found_data) {
279 log_error("Expected D line from PKSIGN after %d attempts", 20);
280 return -1;
281 }
282
283 // Assuan escapes binary bytes as %HH. Decode before reading canonical S-expression fields.
285 size_t decoded_len = 0;
286 for (size_t input = 2; input < response_len; ++input) {
287 if (decoded_len == sizeof(decoded)) {
288 log_error("GPG agent signature response exceeds buffer");
289 return -1;
290 }
291 if (response[input] == '%') {
292 if (response_len - input < 3 || !isxdigit((unsigned char)response[input + 1]) ||
293 !isxdigit((unsigned char)response[input + 2])) {
294 log_error("Invalid percent escape in GPG agent signature");
295 return -1;
296 }
297 char hex[3] = {response[input + 1], response[input + 2], 0};
298 decoded[decoded_len++] = (uint8_t)strtoul(hex, NULL, 16);
299 input += 2;
300 } else {
301 decoded[decoded_len++] = (uint8_t)response[input];
302 }
303 }
304 static const char prefix[] = "(7:sig-val(5:eddsa(1:r32:";
305 static const char separator[] = ")(1:s32:";
306 static const char suffix[] = ")))";
307 const size_t prefix_len = sizeof(prefix) - 1;
308 const size_t separator_len = sizeof(separator) - 1;
309 const size_t expected_len = prefix_len + 32 + separator_len + 32 + sizeof(suffix) - 1;
310 if (decoded_len != expected_len || memcmp(decoded, prefix, prefix_len) != 0 ||
311 memcmp(decoded + prefix_len + 32, separator, separator_len) != 0 ||
312 memcmp(decoded + expected_len - (sizeof(suffix) - 1), suffix, sizeof(suffix) - 1) != 0) {
313 log_error("Invalid Ed25519 signature S-expression from GPG agent");
314 return -1;
315 }
316 memcpy(signature_out, decoded + prefix_len, ED25519_SIGNATURE_PART_SIZE);
317 memcpy(signature_out + ED25519_SIGNATURE_PART_SIZE, decoded + prefix_len + 32 + separator_len,
319 *signature_len_out = 64;
320
321 // DEBUG: Print signature in hex
322 char sig_hex[HEX_STRING_SIZE_64];
323 for (int i = 0; i < 64; i++) {
324 safe_snprintf(sig_hex + i * 2, 3, "%02x", (unsigned char)signature_out[i]);
325 }
326 sig_hex[128] = '\0';
327 log_debug("Extracted signature (64 bytes): %s", sig_hex);
328
329 // Read final OK
330 if (read_agent_line(handle, response, sizeof(response), &response_len) != 0) {
331 log_error("Failed to read final PKSIGN response");
332 return -1;
333 }
334
335 if (!is_ok_response(response)) {
336 log_error("PKSIGN final response not OK: %s", response);
337 return -1;
338 }
339
340 log_debug("Successfully signed message with GPG agent");
341 return 0;
342}
#define CRYPTO_GPG_AGENT_SIGKEY_CMD_BUFFER_SIZE
Definition agent.c:8
#define CRYPTO_GPG_AGENT_SETHASH_CMD_BUFFER_SIZE
Definition agent.c:9
#define ED25519_SIGNATURE_PART_SIZE
Definition sizes.h:7
#define HEX_STRING_SIZE_64
Hex string size for 64-byte values (128 hex chars + null terminator).
Definition sizes.h:156

References CRYPTO_GPG_AGENT_SETHASH_CMD_BUFFER_SIZE, CRYPTO_GPG_AGENT_SIGKEY_CMD_BUFFER_SIZE, ED25519_SIGNATURE_PART_SIZE, GPG_AGENT_MAX_RESPONSE, HEX_STRING_SIZE_64, 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 <include/ascii-chat/crypto/gpg/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 249 of file export.c.

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

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_SMALL, BUFFER_SIZE_XLARGE, BUFFER_SIZE_XXXLARGE, bytes_written, CRYPTO_GPG_EXPORT_FOUND_KEYGRIP_BUFFER_SIZE, crypto_regex_extract_gpg_keygrip(), ED25519_PUBLIC_KEY_SIZE, 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(), SAFE_FREE, safe_snprintf(), and SAFE_STRNCPY.

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[ED25519_SIGNATURE_SIZE] )

#include <include/ascii-chat/crypto/gpg/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 145 of file signing.c.

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

References CRYPTO_GPG_SIGNING_HEX_S_BUFFER_SIZE, ED25519_SIGNATURE_SIZE, gpg_sign_with_key(), GPG_SIGNATURE_PACKET_CAPACITY, HEX_STRING_SIZE_32, 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 <include/ascii-chat/crypto/gpg/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 46 of file signing.c.

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

References CRYPTO_GPG_SIGNING_ESCAPED_KEY_ID_BUFFER_SIZE, errno, escape_path_for_shell(), log_debug, log_error, LOG_IO, platform_execute_subprocess(), 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[ED25519_SIGNATURE_SIZE] )

#include <include/ascii-chat/crypto/gpg/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 37 of file verification.c.

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

References CRYPTO_GPG_VERIFICATION_CMD_BUFFER_SIZE, CRYPTO_GPG_VERIFICATION_OUTPUT_BUFFER_SIZE, ED25519_SIGNATURE_SIZE, gpg_sign_with_key(), GPG_SIGNATURE_PACKET_CAPACITY, log_debug, log_error, PLATFORM_MAX_PATH_LENGTH, platform_pclose(), platform_popen(), 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 <include/ascii-chat/crypto/gpg/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 133 of file verification.c.

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

References CRYPTO_GPG_VERIFICATION_MSG_HEX_BUFFER_SIZE, HEX_STRING_SIZE_32, 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 <include/ascii-chat/crypto/gpg/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 225 of file verification.c.

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

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

◆ known_hosts_destroy()

void known_hosts_destroy ( void )

#include <include/ascii-chat/crypto/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 819 of file known_hosts.c.

819 {
820 if (g_known_hosts_path_cache) {
821 SAFE_FREE(g_known_hosts_path_cache);
822 g_known_hosts_path_cache = NULL;
823 }
824}

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 <include/ascii-chat/crypto/gpg/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 40 of file openpgp.c.

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

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, SAFE_FREE, SAFE_MALLOC, SAFE_MALLOC_DEFER_FREE, 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 <include/ascii-chat/crypto/gpg/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

References ED25519_PUBLIC_KEY_SIZE.

◆ openpgp_parse_armored_pubkey()

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

#include <include/ascii-chat/crypto/gpg/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-----

References ED25519_PUBLIC_KEY_SIZE.

◆ 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 <include/ascii-chat/crypto/gpg/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-----

References ED25519_PUBLIC_KEY_SIZE, and ED25519_SEED_SIZE.

◆ openpgp_parse_binary_pubkey()

asciichat_error_t openpgp_parse_binary_pubkey ( const uint8_t * data,
size_t data_len,
uint8_t ed25519_pk[32] )

#include <include/ascii-chat/crypto/gpg/openpgp.h>

Parse a binary OpenPGP public-key packet stream and extract its Ed25519 key.

References ED25519_PUBLIC_KEY_SIZE.

◆ openpgp_parse_binary_seckey()

asciichat_error_t openpgp_parse_binary_seckey ( const uint8_t * data,
size_t data_len,
uint8_t ed25519_pk[32],
uint8_t ed25519_sk[32] )

#include <include/ascii-chat/crypto/gpg/openpgp.h>

Parse an unencrypted binary OpenPGP secret-key packet stream.

References ED25519_PUBLIC_KEY_SIZE, and ED25519_SEED_SIZE.

◆ 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 <include/ascii-chat/crypto/gpg/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 76 of file openpgp.c.

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

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

Referenced by openpgp_parse_armored_seckey(), openpgp_parse_binary_pubkey(), and openpgp_parse_binary_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 <include/ascii-chat/crypto/gpg/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 207 of file openpgp.c.

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

References openpgp_public_key_t::algorithm, ASCIICHAT_OK, openpgp_public_key_t::created, ED25519_PUBLIC_KEY_SIZE, 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_binary_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 <include/ascii-chat/crypto/gpg/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 378 of file openpgp.c.

379 {
380 if (!packet_body || !seckey || body_len < 6) {
381 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for secret key packet parsing");
382 }
383
384 memset(seckey, 0, sizeof(openpgp_secret_key_t));
385
386 size_t offset = 0;
387
388 // Parse public key portion (same as public key packet)
389 // Version (1 byte, must be 4)
390 seckey->version = packet_body[offset++];
391 if (seckey->version != 4) {
392 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported OpenPGP secret key version: %u (only version 4 supported)",
393 seckey->version);
394 }
395
396 // Creation time (4 bytes, big-endian Unix timestamp)
397 seckey->created = ((uint32_t)packet_body[offset] << 24) | ((uint32_t)packet_body[offset + 1] << 16) |
398 ((uint32_t)packet_body[offset + 2] << 8) | packet_body[offset + 3];
399 offset += 4;
400
401 // Algorithm (1 byte)
402 seckey->algorithm = packet_body[offset++];
403
404 log_debug("Secret key packet: version=%u, created=%u, algorithm=%u", seckey->version, seckey->created,
405 seckey->algorithm);
406
407 // Only support EdDSA (algorithm 22)
408 if (seckey->algorithm != OPENPGP_ALGO_EDDSA) {
409 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported secret key algorithm: %u (only EdDSA/22 supported)",
410 seckey->algorithm);
411 }
412
413 // EdDSA public key: OID + 0x40 prefix + 32 bytes of Ed25519 public key
414 // Search for 0x40 prefix byte
415 bool found_prefix = false;
416 size_t pubkey_offset = 0;
417
418 for (size_t i = offset; i < body_len - 32; i++) {
419 if (packet_body[i] == 0x40) {
420 pubkey_offset = i + 1;
421 found_prefix = true;
422 log_debug("Found Ed25519 public key prefix 0x40 at offset %zu", i);
423 break;
424 }
425 }
426
427 if (!found_prefix) {
428 return SET_ERRNO(ERROR_CRYPTO_KEY, "Ed25519 public key prefix (0x40) not found in secret key packet");
429 }
430
431 if (pubkey_offset + 32 > body_len) {
432 return SET_ERRNO(ERROR_CRYPTO_KEY, "Insufficient data for Ed25519 public key (need 32 bytes after 0x40 prefix)");
433 }
434
435 // Extract the 32-byte Ed25519 public key
436 memcpy(seckey->pubkey, packet_body + pubkey_offset, ED25519_PUBLIC_KEY_SIZE);
437
438 // Move offset past public key material
439 offset = pubkey_offset + 32;
440
441 // S2K usage byte (1 byte)
442 // 0x00 = secret key is not encrypted
443 // 0xFE or 0xFF = secret key is encrypted with S2K
444 if (offset >= body_len) {
445 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing S2K usage byte in secret key packet");
446 }
447
448 uint8_t s2k_usage = packet_body[offset++];
449 log_debug("S2K usage byte: 0x%02x", s2k_usage);
450
451 if (s2k_usage != 0x00) {
452 seckey->is_encrypted = true;
453 log_debug("Detected encrypted secret key (S2K usage = 0x%02x)", s2k_usage);
454 // Don't parse encrypted key material here - caller will need to decrypt with gpg
455 return ASCIICHAT_OK;
456 }
457
458 seckey->is_encrypted = false;
459
460 // The Ed25519 secret scalar is encoded as an OpenPGP MPI (bit count followed by bytes).
461 if (offset + 2 > body_len) {
462 return SET_ERRNO(ERROR_CRYPTO_KEY, "Missing Ed25519 secret-key MPI length");
463 }
464 uint16_t secret_bits = (uint16_t)(((uint16_t)packet_body[offset] << 8) | packet_body[offset + 1]);
465 size_t secret_bytes = (secret_bits + 7u) / 8u;
466 offset += 2;
467 if (secret_bits == 0 || secret_bits > 256 || secret_bytes > sizeof(seckey->seckey) ||
468 offset + secret_bytes + 2 > body_len) {
469 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid Ed25519 secret-key MPI length: %u bits", secret_bits);
470 }
471
472 // Ed25519 private material is 32 bytes; left-pad shorter MPIs as specified by OpenPGP.
473 memcpy(seckey->seckey + sizeof(seckey->seckey) - secret_bytes, packet_body + offset, secret_bytes);
474
475 log_debug("Extracted Ed25519 secret key");
476
477 return ASCIICHAT_OK;
478}
The parsed public and private material from an OpenPGP secret-key packet.
Definition openpgp.h:124
uint8_t seckey[32]
Ed25519 secret key (32 bytes).
Definition openpgp.h:129
uint8_t pubkey[32]
Ed25519 public key (32 bytes).
Definition openpgp.h:128
bool is_encrypted
True if secret key material is encrypted.
Definition openpgp.h:131
uint32_t created
Creation timestamp (Unix epoch).
Definition openpgp.h:126
uint8_t algorithm
Public key algorithm (22 = EdDSA).
Definition openpgp.h:127
uint8_t version
Packet version (should be 4).
Definition openpgp.h:125

References openpgp_secret_key_t::algorithm, ASCIICHAT_OK, openpgp_secret_key_t::created, ED25519_PUBLIC_KEY_SIZE, 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(), and openpgp_parse_binary_seckey().

◆ prompt_unknown_host()

bool prompt_unknown_host ( const char * server_ip,
uint16_t port,
const uint8_t server_key[32] )

#include <include/ascii-chat/crypto/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.

References ED25519_PUBLIC_KEY_SIZE.

◆ prompt_unknown_host_no_identity()

bool prompt_unknown_host_no_identity ( const char * server_ip,
uint16_t port )

#include <include/ascii-chat/crypto/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 761 of file known_hosts.c.

761 {
762 // Format IP:port with proper bracket notation for IPv6
763 char ip_with_port[BUFFER_SIZE_MEDIUM];
764 if (format_ip_with_port(server_ip, port, ip_with_port, sizeof(ip_with_port)) != ASCIICHAT_OK) {
765 // Fallback to basic format if error
766 safe_snprintf(ip_with_port, sizeof(ip_with_port), "%s:%u", server_ip, port);
767 }
768
769 // LOCALHOST OPTIMIZATION: Skip authentication prompt for localhost addresses
770 // Localhost is inherently trusted (same machine, no network exposure)
771 // This matches SSH behavior (ssh-keyscan doesn't prompt for localhost)
772 if (is_localhost_address(server_ip)) {
773 log_debug(
774 "SECURITY: Localhost address %s detected - skipping unknown host prompt (localhost is inherently trusted)",
775 ip_with_port);
776 return true; // Silently trust localhost
777 }
778
779 // Check if running in automated/non-interactive environment
780 // In debug builds, Claude Code automation can't interact with prompts
781 if (is_automated_mode()) {
782 SET_ERRNO(ERROR_CRYPTO, "SECURITY: Cannot verify server without identity key in non-interactive/automated mode");
783 NOTICE(DANGER, "SERVER IDENTITY MISSING",
784 "ERROR: Cannot verify server without identity key in non-interactive/automated mode.\n"
785 "ERROR: This connection is vulnerable to man-in-the-middle attacks!\n"
786 "\n"
787 "To connect to this host:\n"
788 " 1. Run the client interactively (from a terminal with TTY)\n"
789 " 2. Verify you trust this server despite no identity key\n"
790 " 3. Accept the risk when prompted\n"
791 " OR better: Ask server admin to use --key for proper authentication\n"
792 "\n"
793 "Connection aborted for security.\n"
794 "\n");
795 return false;
796 }
797
799 safe_snprintf(question, sizeof(question),
800 "Host '%s' has no identity key.\n"
801 "This connection is vulnerable to man-in-the-middle attacks.\n"
802 "Anyone intercepting it could read your data.\n\n"
803 "Use --key on the server and --server-key on the client to verify identity.\n\n"
804 "Are you sure you want to continue connecting",
805 ip_with_port);
806 if (ui_notice_confirm_kind(UI_PROMPT_UNVERIFIED_HOST, UI_NOTICE_DANGER, question, 120)) {
807 NOTICE(DANGER, "UNVERIFIED CONNECTION",
808 "Warning: Proceeding with unverified connection.\n"
809 "Your data may be intercepted by attackers!\n"
810 "\n");
811 return true;
812 }
813
814 log_plain("Connection aborted by user.");
815 return false;
816}
#define log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:665
#define CRYPTO_KNOWN_HOSTS_QUESTION_BUFFER_SIZE
Definition known_hosts.c:9
bool ui_notice_confirm_kind(ui_prompt_kind_t kind, ui_notice_severity_t severity, const char *text, unsigned timeout_seconds)
Definition notice.c:498
@ UI_NOTICE_DANGER
Definition notice.h:12

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, CRYPTO_KNOWN_HOSTS_QUESTION_BUFFER_SIZE, ERROR_CRYPTO, format_ip_with_port(), log_debug, log_plain, NOTICE, safe_snprintf(), SET_ERRNO, ui_notice_confirm_kind(), and UI_NOTICE_DANGER.

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 <include/ascii-chat/crypto/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 457 of file pem.c.

457 {
458 br_x509_certificate *xcs;
459 anchor_list tas = VEC_INIT;
460 size_t u, num;
461
462 xcs = read_certificates_from_memory(pem_data, pem_len, &num);
463 if (xcs == NULL) {
464 return 0;
465 }
466
467 for (u = 0; u < num; u++) {
468 br_x509_trust_anchor ta;
469
470 if (certificate_to_trust_anchor_inner(&ta, &xcs[u]) != ASCIICHAT_OK) {
472 free_certificates(xcs, num);
473 return 0;
474 }
475 VEC_ADD(tas, ta);
476 }
477
478 VEC_ADDMANY(*dst, &VEC_ELT(tas, 0), num);
479 VEC_CLEAR(tas);
480 free_certificates(xcs, num);
481 return num;
482}
void free_ta_contents(br_x509_trust_anchor *ta)
Free the contents of a trust anchor.
Definition pem.c:436
#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:79

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 <include/ascii-chat/crypto/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 474 of file known_hosts.c.

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

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_XLARGE, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_MEMORY, format_ip_with_port(), get_known_hosts_path(), log_debug, 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 <include/ascii-chat/crypto/ssh/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 149 of file ssh_agent.c.

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

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, BUFFER_SIZE_XXLARGE, bytes_written, private_key_t::ed25519, ED25519_PRIVATE_KEY_SIZE, ED25519_PUBLIC_KEY_SIZE, ED25519_SEED_SIZE, 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.

◆ ssh_agent_get_key()

asciichat_error_t ssh_agent_get_key ( const public_key_t * public_key,
private_key_t * key_out )

#include <include/ascii-chat/crypto/ssh/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.

References ED25519_SIGNATURE_SIZE.

◆ ssh_agent_has_key()

bool ssh_agent_has_key ( const public_key_t * public_key)

#include <include/ascii-chat/crypto/ssh/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 54 of file ssh_agent.c.

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

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().

◆ ssh_agent_is_available()

bool ssh_agent_is_available ( void )

#include <include/ascii-chat/crypto/ssh/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 42 of file ssh_agent.c.

42 {
43 /* Try to open the SSH agent connection */
44 pipe_t pipe = ssh_agent_open_pipe();
45 if (pipe != INVALID_PIPE_VALUE) {
47 log_dev("ssh-agent is available");
48 return true;
49 }
50 log_dev("ssh-agent not available");
51 return false;
52}

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 <include/ascii-chat/crypto/ssh/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 260 of file ssh_agent.c.

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

References ASCIICHAT_OK, BUFFER_SIZE_XXLARGE, ED25519_PUBLIC_KEY_SIZE, ED25519_SIGNATURE_SIZE, 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 80 of file pem.h.

Referenced by https_get().

◆ len

size_t anchor_list::len

Total capacity of array

Definition at line 82 of file pem.h.

◆ ptr

size_t anchor_list::ptr

Current number of trust anchors

Definition at line 81 of file pem.h.

Referenced by https_get().