ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Keys Module

🔑 SSH key, GPG key, and key validation APIs More...

Data Structures

struct  public_key_t
 Public key structure. More...
 
struct  private_key_t
 Private key structure (for server –ssh-key) More...
 

Typedefs

typedef struct crypto_handshake_context_t crypto_handshake_context_t
 

Variables

key_type_t public_key_t::type
 
uint8_t public_key_t::key [32]
 
char public_key_t::comment [256]
 
key_type_t private_key_t::type
 
uint8_t   private_key_t::ed25519 [64] 
 
uint8_t   private_key_t::x25519 [32] 
 
union { 
 
   uint8_t   private_key_t::ed25519 [64] 
 
   uint8_t   private_key_t::x25519 [32] 
 
} private_key_t::key 
 
bool private_key_t::use_ssh_agent
 
bool private_key_t::use_gpg_agent
 
uint8_t private_key_t::public_key [32]
 
char private_key_t::key_comment [256]
 
char private_key_t::gpg_keygrip [64]
 

Key Type Definitions

enum  key_type_t { KEY_TYPE_UNKNOWN = 0 , KEY_TYPE_ED25519 , KEY_TYPE_X25519 , KEY_TYPE_GPG }
 Key type enumeration. More...
 

GPG Key Parsing

asciichat_error_t parse_gpg_key (const char *gpg_key_text, public_key_t *key_out)
 Parse GPG key from armored text format.
 
asciichat_error_t parse_gpg_key_binary (const uint8_t *gpg_key_binary, size_t key_size, public_key_t *key_out)
 Parse GPG key from binary format.
 
asciichat_error_t validate_gpg_key_format (const char *gpg_key_text)
 Validate GPG key format and structure.
 
asciichat_error_t extract_ed25519_from_gpg (const char *gpg_key_text, uint8_t ed25519_pk[32])
 Extract Ed25519 public key from GPG key.
 
asciichat_error_t gpg_to_x25519_public (const char *gpg_key_text, uint8_t x25519_pk[32])
 Convert GPG key to X25519 for key exchange.
 

GPG Key Operations

asciichat_error_t get_gpg_fingerprint (const char *gpg_key_text, uint8_t fingerprint_out[20])
 Get GPG key fingerprint.
 
asciichat_error_t get_gpg_key_id (const char *gpg_key_text, uint8_t key_id_out[8])
 Get GPG key ID (short fingerprint)
 
asciichat_error_t check_gpg_key_expiry (const char *gpg_key_text, bool *is_expired)
 Check if GPG key is expired.
 

GPG Key Formatting

asciichat_error_t format_gpg_key_display (const char *gpg_key_text, char *output, size_t output_size)
 Format GPG key for display.
 
asciichat_error_t extract_gpg_key_comment (const char *gpg_key_text, char *comment_out, size_t comment_size)
 Extract key comment/email from GPG key.
 

HTTPS Key Fetching

Fetch SSH/GPG keys from GitHub and GitLab using HTTPS (BearSSL). Requires network connectivity and valid usernames.

asciichat_error_t fetch_github_ssh_keys (const char *username, char ***keys_out, size_t *num_keys)
 Fetch SSH keys from GitHub using HTTPS.
 
asciichat_error_t fetch_gitlab_ssh_keys (const char *username, char ***keys_out, size_t *num_keys)
 Fetch SSH keys from GitLab using HTTPS.
 
asciichat_error_t fetch_github_gpg_keys (const char *username, char ***keys_out, size_t *num_keys)
 Fetch GPG keys from GitHub using HTTPS.
 
asciichat_error_t fetch_gitlab_gpg_keys (const char *username, char ***keys_out, size_t *num_keys)
 Fetch GPG keys from GitLab using HTTPS.
 

Key Parsing from HTTPS Responses

asciichat_error_t parse_ssh_keys_from_response (const char *response_text, size_t response_len, char ***keys_out, size_t *num_keys, size_t max_keys)
 Parse SSH keys from HTTPS response text.
 
asciichat_error_t parse_gpg_keys_from_response (const char *response_text, size_t response_len, char ***keys_out, size_t *num_keys, size_t max_keys)
 Parse GPG keys from HTTPS response text.
 

URL Construction

asciichat_error_t build_github_ssh_url (const char *username, char *url_out, size_t url_size)
 Construct GitHub SSH keys URL.
 
asciichat_error_t build_gitlab_ssh_url (const char *username, char *url_out, size_t url_size)
 Construct GitLab SSH keys URL.
 
asciichat_error_t build_github_gpg_url (const char *username, char *url_out, size_t url_size)
 Construct GitHub GPG keys URL.
 
asciichat_error_t build_gitlab_gpg_url (const char *username, char *url_out, size_t url_size)
 Construct GitLab GPG keys URL.
 

Public Key Parsing

asciichat_error_t parse_public_key (const char *input, public_key_t *key_out)
 Parse SSH/GPG public key from any format (returns first key only)
 
asciichat_error_t parse_public_keys (const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys)
 Parse all SSH/GPG public keys from any format (returns all keys)
 

Private Key Parsing

asciichat_error_t parse_private_key (const char *path, private_key_t *key_out)
 Parse SSH private key from file.
 

Key Conversion

asciichat_error_t public_key_to_x25519 (const public_key_t *key, uint8_t x25519_pk[32])
 Convert public key to X25519 for Diffie-Hellman key exchange.
 
asciichat_error_t private_key_to_x25519 (const private_key_t *key, uint8_t x25519_sk[32])
 Convert private key to X25519 for Diffie-Hellman key exchange.
 

Ed25519 Signing and Verification

asciichat_error_t ed25519_sign_message (const private_key_t *key, const uint8_t *message, size_t message_len, uint8_t signature[64])
 Sign a message with Ed25519 (uses SSH agent if available, otherwise in-memory key)
 
asciichat_error_t ed25519_verify_signature (const uint8_t public_key[32], const uint8_t *message, size_t message_len, const uint8_t signature[64], const char *gpg_key_id)
 Verify an Ed25519 signature.
 

Key Fetching (GitHub/GitLab)

Fetch SSH/GPG keys from GitHub and GitLab using HTTPS (BearSSL). Requires network connectivity and valid usernames.

asciichat_error_t fetch_github_keys (const char *username, char ***keys_out, size_t *num_keys, bool use_gpg)
 Fetch SSH/GPG keys from GitHub using BearSSL.
 
asciichat_error_t fetch_gitlab_keys (const char *username, char ***keys_out, size_t *num_keys, bool use_gpg)
 Fetch SSH/GPG keys from GitLab using BearSSL.
 

Key Parsing

asciichat_error_t parse_keys_from_file (const char *path, public_key_t *keys, size_t *num_keys, size_t max_keys)
 Parse SSH keys from file (supports authorized_keys and known_hosts formats)
 

Key Formatting and Utilities

void format_public_key (const public_key_t *key, char *output, size_t output_size)
 Convert public key to display format (ssh-ed25519 or x25519 hex)
 
asciichat_error_t hex_decode (const char *hex, uint8_t *output, size_t output_len)
 Decode hex string to binary (utility function for testing)
 
asciichat_error_t validate_ssh_key_file (const char *key_path)
 Validate SSH key file before parsing.
 

Key Validation

asciichat_error_t validate_public_key (const public_key_t *key)
 Validate a public key structure.
 
asciichat_error_t validate_private_key (const private_key_t *key)
 Validate a private key structure.
 
asciichat_error_t check_key_expiry (const public_key_t *key, bool *is_expired)
 Check if a key is expired.
 
asciichat_error_t validate_key_security (const char *key_path)
 Validate key permissions and security.
 

Key Format Validation

asciichat_error_t validate_ssh_key_format (const char *key_text)
 Validate SSH key format.
 
asciichat_error_t validate_x25519_key_format (const char *key_hex)
 Validate X25519 key format.
 

Key Security Checks

asciichat_error_t check_key_strength (const public_key_t *key, bool *is_weak)
 Check if key has weak parameters.
 
asciichat_error_t check_key_patterns (const public_key_t *key, bool *has_weak_patterns)
 Check for key reuse or weak patterns.
 

Key Comparison and Matching

asciichat_error_t compare_public_keys (const public_key_t *key1, const public_key_t *key2, bool *are_equal)
 Compare two public keys for equality.
 
asciichat_error_t check_key_fingerprint (const public_key_t *key, const uint8_t *fingerprint, size_t fingerprint_len, bool *matches)
 Check if key matches a fingerprint.
 
asciichat_error_t generate_key_fingerprint (const public_key_t *key, uint8_t *fingerprint_out, size_t fingerprint_size)
 Generate key fingerprint.
 

SSH Key Parsing

asciichat_error_t parse_ssh_ed25519_line (const char *line, uint8_t ed25519_pk[32])
 Parse SSH Ed25519 public key from "ssh-ed25519 AAAAC3..." format.
 
asciichat_error_t parse_ssh_private_key (const char *key_path, private_key_t *key_out)
 Parse SSH Ed25519 private key from file.
 

Key Conversion

asciichat_error_t ed25519_to_x25519_public (const uint8_t ed25519_pk[32], uint8_t x25519_pk[32])
 Convert Ed25519 public key to X25519 for key exchange.
 
asciichat_error_t ed25519_to_x25519_private (const uint8_t ed25519_sk[64], uint8_t x25519_sk[32])
 Convert Ed25519 private key to X25519 for key exchange.
 

Detailed Description

🔑 SSH key, GPG key, and key validation APIs

SSH key, GPG key, and key validation APIs.

This module handles GPG key parsing, validation, and conversion to X25519 for key exchange operations.

Warning
GPG SUPPORT IS CURRENTLY DISABLED: This code exists but many functions are not fully implemented. GPG key parsing may not work until GPG support is re-enabled and functions are completed.
Note
Key format: Only Ed25519 GPG keys are supported. RSA/ECDSA GPG keys are NOT supported.
Key conversion: GPG Ed25519 keys are extracted and converted to X25519 for key exchange using libsodium's conversion functions.
OpenPGP format: Functions parse OpenPGP packet structure to extract Ed25519 public keys from GPG keys.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This module handles fetching SSH and GPG keys from GitHub and GitLab using HTTPS requests with BearSSL for secure communication.

Note
Network operations: Requires network connectivity and valid usernames. May fail if network is unavailable or usernames don't exist.
TLS security: Uses BearSSL for HTTPS connections with system CA certificates. Validates server certificates against system trust store.
Key format: Only Ed25519 SSH keys are parsed from responses. Other key types (RSA, ECDSA) are skipped.
GPG support: GPG key fetching code exists but GPG parsing may not work until GPG support is re-enabled.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header contains the core key type definitions that can be shared between the main keys.h and the specialized key modules without circular dependencies.

All keys in this system are 32 bytes - Ed25519, X25519, and GPG-derived keys. This fixed size simplifies protocol design and key management.

Note
Key Type Restriction: Only Ed25519 and X25519 are supported. RSA and ECDSA are NOT supported due to libsodium limitations and protocol design requiring fixed-size keys.
GPG Support: GPG keys are parsed and converted to Ed25519/X25519 format. GPG agent support exists but is currently disabled.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This header provides a unified interface for parsing SSH, GPG, and X25519 keys from various sources including files, URLs, and raw formats.

Supported input formats:

Note
All keys are normalized to 32-byte X25519 format for key exchange.
GPG support: GPG Ed25519 keys are fully supported via gpg-agent (requires gpg binary in PATH).
GitHub/GitLab fetching: Uses BearSSL for HTTPS requests.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This module provides comprehensive key validation, security checks, and format verification for all supported key types.

Note
Key validation: Validates key structure, format, and security properties. Used before key operations to ensure keys are valid and secure.
Security checks: Includes permission checking, weak key detection, and pattern analysis for security vulnerabilities.
Key format validation: Validates SSH, GPG, and X25519 key formats. Returns error early if format is invalid.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

This module handles SSH Ed25519 key parsing, validation, and conversion to X25519 for key exchange operations.

Note
Key format: Only Ed25519 keys are supported. RSA/ECDSA keys are NOT supported.
OpenSSH format: Supports OpenSSH private key format (openssh-key-v1). Supports encrypted and unencrypted keys.
Key conversion: Ed25519 keys are converted to X25519 for key exchange using libsodium's conversion functions.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
October 2025

Key Management README

Overview

The Keys Module module provides comprehensive support for parsing, validating, and managing cryptographic keys from multiple sources and formats. It serves as the foundation for secure authentication and key exchange in ascii-chat.

Implementation: lib/crypto/keys/

Key Features:

  • Unified key parsing from multiple sources (SSH, GPG, raw formats)
  • Support for multiple key sources (files, GitHub, GitLab, raw hex/base64)
  • Automatic key format detection and conversion
  • Key validation and security checks
  • SSH agent integration for secure signing
  • Ed25519 to X25519 conversion for key exchange

Supported Key Formats

SSH Ed25519 Keys

Format: OpenSSH Ed25519 public and private keys

Public Key Format:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... comment

Private Key Format:

-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

Supported Operations:

  • Parse SSH public key from string
  • Parse SSH private key from file
  • Validate key file permissions
  • Support for encrypted private keys (with password prompt)
  • Convert Ed25519 to X25519 for key exchange

Example:

// Parse SSH public key from string
public_key_t pubkey;
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@example.com",
&pubkey
);
// Parse SSH private key from file
private_key_t privkey;
err = parse_private_key("~/.ssh/id_ed25519", &privkey);
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out)
Parse SSH private key from file.
Definition keys.c:193
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
Parse SSH/GPG public key from any format (returns first key only)
Definition keys.c:28
Private key structure (for server –ssh-key)
Definition key_types.h:91
Public key structure.
Definition key_types.h:69

GPG Keys

Format: OpenPGP armored format (PEM-style)

Key Format:

-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----

Supported Operations:

  • Parse GPG public key from keyring via gpg --list-keys and keygrip
  • Extract Ed25519 public key via gpg-agent READKEY command
  • Sign challenges using gpg-agent PKSIGN command (Assuan protocol)
  • Verify signatures using gpg --verify for deterministic Ed25519
  • Convert GPG Ed25519 keys to X25519 for key exchange
  • Accept 8, 16, or 40 character key IDs (short, long, full fingerprint)

Requirements:

  • Only Ed25519 GPG keys are supported (not RSA/DSA)
  • gpg binary must be in PATH
  • gpg-agent must be running for signing operations

Example:

// Parse GPG key by key ID
private_key_t privkey;
"gpg:897607FA43DC66F612710AF97FE90A79F2E80ED3",
&privkey
);
// Parse GPG key for server verification
public_key_t pubkey;
"gpg:897607FA43DC66F612710AF97FE90A79F2E80ED3",
&pubkey
);

Raw X25519 Keys

Format: 64 hex characters or base64-encoded 32-byte key

Supported Formats:

  • Hex: 1234567890abcdef... (64 hex chars for 32 bytes)
  • Base64: Base64-encoded 32-byte key

Example:

// Parse raw X25519 key from hex
public_key_t pubkey;
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
&pubkey
);

Key Sources

Local Files

Keys can be loaded from local files:

Public Keys:

// Load from .pub file (reads first line)
public_key_t pubkey;
asciichat_error_t err = parse_public_key("~/.ssh/id_ed25519.pub", &pubkey);
// Or read first line and parse manually
FILE *f = fopen("~/.ssh/id_ed25519.pub", "r");
char line[1024];
fgets(line, sizeof(line), f);
err = parse_public_key(line, &pubkey);
fclose(f);
// Load multiple keys from file (one per line)
public_key_t keys[10];
size_t num_keys;
err = parse_keys_from_file("~/.ssh/authorized_keys", keys, &num_keys, 10);
// File can contain multiple keys, one per line:
// ssh-ed25519 AAAAC3... alice@example.com
// ssh-ed25519 AAAAC3... bob@example.com
asciichat_error_t parse_keys_from_file(const char *path, public_key_t *keys, size_t *num_keys, size_t max_keys)
Parse SSH keys from file (supports authorized_keys and known_hosts formats)
Definition keys.c:544

Private Keys:

// Load private key from file
private_key_t privkey;
asciichat_error_t err = parse_private_key("~/.ssh/id_ed25519", &privkey);
// File permissions are validated (warns if overly permissive)
// Encrypted keys prompt for password automatically

GitHub and GitLab Integration

Keys can be fetched directly from GitHub or GitLab using HTTPS:

SSH Keys:

// Fetch SSH keys from GitHub
char **keys;
size_t num_keys;
asciichat_error_t err = fetch_github_ssh_keys("username", &keys, &num_keys);
if (err == ASCIICHAT_OK && num_keys > 0) {
// Parse first Ed25519 key
public_key_t pubkey;
parse_public_key(keys[0], &pubkey);
// Free keys array
for (size_t i = 0; i < num_keys; i++) {
free(keys[i]);
}
free(keys);
}
@ ASCIICHAT_OK
Definition error_codes.h:51
asciichat_error_t fetch_github_ssh_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch SSH keys from GitHub using HTTPS.
Definition https_keys.c:166

GPG Keys:

// Fetch GPG keys from GitHub
char **keys;
size_t num_keys;
asciichat_error_t err = fetch_github_gpg_keys("username", &keys, &num_keys);
asciichat_error_t fetch_github_gpg_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch GPG keys from GitHub using HTTPS.
Definition https_keys.c:230

GitLab Support:

// Fetch SSH keys from GitLab
err = fetch_gitlab_ssh_keys("username", &keys, &num_keys);
// Fetch GPG keys from GitLab
err = fetch_gitlab_gpg_keys("username", &keys, &num_keys);
asciichat_error_t fetch_gitlab_gpg_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch GPG keys from GitLab using HTTPS.
Definition https_keys.c:262
asciichat_error_t fetch_gitlab_ssh_keys(const char *username, char ***keys_out, size_t *num_keys)
Fetch SSH keys from GitLab using HTTPS.
Definition https_keys.c:198

Unified Parsing:

// Parse key from GitHub using unified interface
public_key_t pubkey;
asciichat_error_t err = parse_public_key("github:username", &pubkey);
// Automatically fetches from https://github.com/username.keys
// Uses first Ed25519 key found
// Parse GPG key from GitHub
err = parse_public_key("github:username.gpg", &pubkey);
// Parse from GitLab
err = parse_public_key("gitlab:username", &pubkey);
err = parse_public_key("gitlab:username.gpg", &pubkey);

Unified Key Parsing

The parse_public_key() function supports multiple input formats automatically:

public_key_t pubkey;
// SSH Ed25519 format
err = parse_public_key("ssh-ed25519 AAAAC3... comment", &pubkey);
// GitHub SSH keys
err = parse_public_key("github:username", &pubkey);
// GitLab SSH keys
err = parse_public_key("gitlab:username", &pubkey);
// GitHub GPG keys
err = parse_public_key("github:username.gpg", &pubkey);
// File path
err = parse_public_key("~/.ssh/id_ed25519.pub", &pubkey);
// Raw hex
err = parse_public_key("1234567890abcdef...", &pubkey);

Key Validation

Public Key Validation

All public keys are validated before use:

public_key_t pubkey;
asciichat_error_t err = parse_public_key(input, &pubkey);
// Validate key structure
err = validate_public_key(&pubkey);
if (err != ASCIICHAT_OK) {
log_error("Invalid public key");
return err;
}
asciichat_error_t validate_public_key(const public_key_t *key)
Validate a public key structure.
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587

Validation Checks:

  • Key type is valid (Ed25519, X25519, or GPG)
  • Key data is not all zeros
  • Comment length is within limits (< 256 chars)
  • Key size matches type (32 bytes for Ed25519/X25519)

Private Key Validation

Private keys are validated similarly:

private_key_t privkey;
asciichat_error_t err = parse_private_key("~/.ssh/id_ed25519", &privkey);
// Validate key structure
err = validate_private_key(&privkey);
if (err != ASCIICHAT_OK) {
log_error("Invalid private key");
return err;
}
asciichat_error_t validate_private_key(const private_key_t *key)
Validate a private key structure.

Validation Checks:

  • Key type is valid (Ed25519 or X25519)
  • Key data is not all zeros
  • Key size matches type (32 bytes for X25519, 64 bytes for Ed25519)
  • Comment length is within limits

File Permission Validation

Private key files are checked for appropriate permissions:

// Validate SSH key file before parsing
asciichat_error_t err = validate_ssh_key_file("~/.ssh/id_ed25519");
// Checks:
// - File exists and is readable
// - File has valid SSH private key header
// - File permissions are appropriate (warns if overly permissive)
asciichat_error_t validate_ssh_key_file(const char *key_path)
Validate SSH key file before parsing.
Definition ssh_keys.c:939

Permission Recommendations:

  • Private keys should have restrictive permissions (0600 on Unix)
  • Function warns if file has world-readable permissions
  • Permission checking is Unix-specific (Windows doesn't have Unix-style permissions)

Key Conversion

Ed25519 to X25519 Conversion

Ed25519 keys are converted to X25519 format for Diffie-Hellman key exchange:

Public Key Conversion:

// Parse Ed25519 public key
public_key_t ed25519_pubkey;
parse_public_key("ssh-ed25519 AAAAC3...", &ed25519_pubkey);
// Convert to X25519 for key exchange
uint8_t x25519_pk[32];
asciichat_error_t err = public_key_to_x25519(&ed25519_pubkey, x25519_pk);
// Use x25519_pk for Diffie-Hellman key exchange
unsigned char uint8_t
Definition common.h:56
asciichat_error_t public_key_to_x25519(const public_key_t *key, uint8_t x25519_pk[32])
Convert public key to X25519 for Diffie-Hellman key exchange.
Definition keys.c:474

Private Key Conversion:

// Parse Ed25519 private key
private_key_t ed25519_privkey;
parse_private_key("~/.ssh/id_ed25519", &ed25519_privkey);
// Convert to X25519 for key exchange
uint8_t x25519_sk[32];
asciichat_error_t err = private_key_to_x25519(&ed25519_privkey, x25519_sk);
// Use x25519_sk for Diffie-Hellman key exchange
asciichat_error_t private_key_to_x25519(const private_key_t *key, uint8_t x25519_sk[32])
Convert private key to X25519 for Diffie-Hellman key exchange.
Definition keys.c:493

Conversion Details:

  • Uses libsodium's conversion functions (crypto_sign_ed25519_pk_to_curve25519)
  • Mathematically safe (same curve, different representation)
  • Both Ed25519 and X25519 keys are 32 bytes
  • Ed25519 private keys are 64 bytes (seed + public), extracts 32-byte seed for conversion

Direct Conversion Functions

Lower-level conversion functions are also available:

// Direct Ed25519 public key conversion
uint8_t ed25519_pk[32] = {...};
uint8_t x25519_pk[32];
asciichat_error_t err = ed25519_to_x25519_public(ed25519_pk, x25519_pk);
// Direct Ed25519 private key conversion
uint8_t ed25519_sk[64] = {...}; // seed + public
uint8_t x25519_sk[32];
asciichat_error_t err = ed25519_to_x25519_private(ed25519_sk, x25519_sk);
asciichat_error_t ed25519_to_x25519_private(const uint8_t ed25519_sk[64], uint8_t x25519_sk[32])
Convert Ed25519 private key to X25519 for key exchange.
Definition ssh_keys.c:1013
asciichat_error_t ed25519_to_x25519_public(const uint8_t ed25519_pk[32], uint8_t x25519_pk[32])
Convert Ed25519 public key to X25519 for key exchange.
Definition ssh_keys.c:1000

Signing and Verification

Signing Messages

Messages can be signed with Ed25519 private keys:

// Load private key
private_key_t privkey;
parse_private_key("~/.ssh/id_ed25519", &privkey);
// Sign message
const uint8_t message[] = "Hello, secure world!";
uint8_t signature[64];
&privkey,
message,
sizeof(message) - 1, // Length without null terminator
signature
);
// Use signature...
asciichat_error_t ed25519_sign_message(const private_key_t *key, const uint8_t *message, size_t message_len, uint8_t signature[64])
Sign a message with Ed25519 (uses SSH agent if available, otherwise in-memory key)
Definition ssh_keys.c:1030

SSH Agent Support:

// If SSH agent is available, signing uses agent
// Key stays in agent (not loaded into memory)
// More secure - private key never touches application memory
// Configure key to use SSH agent
privkey.use_ssh_agent = true;
// Signing now uses SSH agent protocol
ed25519_sign_message(&privkey, message, message_len, signature);
bool use_ssh_agent
Definition key_types.h:97

Verifying Signatures

Signatures can be verified with Ed25519 public keys:

// Load public key
public_key_t pubkey;
parse_public_key("ssh-ed25519 AAAAC3...", &pubkey);
// Convert to raw Ed25519 public key
uint8_t ed25519_pk[32];
// ... extract from pubkey ...
// Verify signature
const uint8_t message[] = "Hello, secure world!";
uint8_t signature[64] = {...}; // From signing
ed25519_pk,
message,
sizeof(message) - 1,
signature
);
if (err == ASCIICHAT_OK) {
log_info("Signature is valid!");
} else {
log_error("Signature verification failed");
}
asciichat_error_t ed25519_verify_signature(const uint8_t public_key[32], const uint8_t *message, size_t message_len, const uint8_t signature[64], const char *gpg_key_id)
Verify an Ed25519 signature.
Definition ssh_keys.c:1126
#define log_info(...)
Log an INFO message.
Definition log/log.h:561

Security Features:

  • Constant-time signature verification (prevents timing attacks)
  • Automatic validation of signature format (must be 64 bytes)
  • Returns error immediately if signature is invalid

Architecture

Module Structure

The key management module is organized into specialized submodules:

Core Modules:

  • keys.h: Unified key parsing interface
  • types.h: Key type definitions and structures
  • validation.h: Key validation and security functions

Format-Specific Modules:

Support Modules:

  • pem.h: PEM encoding/decoding utilities
  • http_client.h: BearSSL HTTPS client for key fetching

Data Structures

Public Key Structure:

typedef struct {
key_type_t type; // KEY_TYPE_ED25519, KEY_TYPE_X25519, or KEY_TYPE_GPG
uint8_t data[32]; // 32-byte public key
char comment[256]; // Optional comment/label
key_type_t
Key type enumeration.
Definition key_types.h:50

Private Key Structure:

typedef struct {
key_type_t type; // KEY_TYPE_ED25519 or KEY_TYPE_X25519
union {
uint8_t x25519[32]; // X25519 private key (32 bytes)
uint8_t ed25519[64]; // Ed25519 private key (seed + public, 64 bytes)
} key;
char comment[256]; // Optional comment/label
bool use_ssh_agent; // Use SSH agent for signing
bool use_gpg_agent; // Use GPG agent for signing (may be disabled)

Usage Examples

Basic Key Loading

#include "crypto/keys/keys.h"
// Load server public key
public_key_t server_pubkey;
"github:server-username",
&server_pubkey
);
if (err != ASCIICHAT_OK) {
log_error("Failed to load server public key");
return err;
}
// Validate key
err = validate_public_key(&server_pubkey);
if (err != ASCIICHAT_OK) {
log_error("Invalid server public key");
return err;
}
// Convert to X25519 for key exchange
uint8_t x25519_pk[32];
err = public_key_to_x25519(&server_pubkey, x25519_pk);

Client Authentication

// Load client private key
private_key_t client_privkey;
asciichat_error_t err = parse_private_key("~/.ssh/id_ed25519", &client_privkey);
// Try to use SSH agent if available
client_privkey.use_ssh_agent = true;
// Sign handshake challenge
uint8_t challenge[] = {...}; // From server
uint8_t signature[64];
&client_privkey,
challenge,
sizeof(challenge),
signature
);
// Send signature to server
send_signature_to_server(signature);

Server Key Whitelist

Loading from file (recommended):

// Server: Load authorized client keys from file
// File can be authorized_keys format or .pub file with multiple entries (one per line)
size_t num_authorized = 0;
asciichat_error_t err = parse_client_keys(
"~/.ssh/authorized_keys", // File with multiple keys, one per line
client_keys,
&num_authorized,
);
// File format (one key per line):
// ssh-ed25519 AAAAC3... alice@example.com
// ssh-ed25519 AAAAC3... bob@example.com
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
Definition limits.h:26

Loading from array:

// Server: Load authorized client keys from array
const char *authorized_clients[] = {
"github:alice",
"github:bob",
"ssh-ed25519 AAAAC3... charlie@example.com"
};
size_t num_authorized = 0;
for (size_t i = 0; i < sizeof(authorized_clients) / sizeof(authorized_clients[0]); i++) {
if (parse_public_key(authorized_clients[i], &client_keys[num_authorized]) == ASCIICHAT_OK) {
if (validate_public_key(&client_keys[num_authorized]) == ASCIICHAT_OK) {
num_authorized++;
}
}
}

// Verify incoming client signatures against whitelist bool is_authorized = false; for (size_t i = 0; i < num_authorized; i++) { if (ed25519_verify_signature( client_keys[i].key, // .key field (32 bytes), not .data challenge, sizeof(challenge), signature ) == ASCIICHAT_OK) { is_authorized = true; break; } }

Security Considerations

File Permissions

Private Keys:

  • Should have restrictive permissions (0600 on Unix)
  • Function validates and warns if permissions are too permissive
  • Never share private keys

Public Keys:

  • Can be world-readable (no sensitive data)
  • Still validate format before use

Key Storage

In-Memory Keys:

  • Private keys in memory should be zeroed after use
  • Use sodium_memzero() for secure memory clearing
  • Avoid leaving keys in memory longer than necessary

SSH Agent:

  • More secure: keys stay in agent process (isolated)
  • Private keys never loaded into application memory
  • Recommended for production use

GPG Agent:

  • Similar security benefits to SSH agent
  • Currently may be disabled (check build configuration)

Validation

Always Validate Keys:

  • Check return values from parsing functions
  • Explicitly validate keys before use
  • Reject keys with all-zero data
  • Verify key sizes match expected values

Zero Key Detection:

  • Validation functions reject all-zero keys
  • Zero keys are mathematically invalid
  • Protects against accidentally using uninitialized keys

Limitations and Restrictions

Supported Key Types

Currently Supported:

  • Ed25519 public and private keys (SSH format)
  • X25519 keys (raw hex or base64)
  • GPG Ed25519 keys (may be disabled)

Not Supported:

  • RSA keys (any size)
  • ECDSA keys (any curve)
  • GPG RSA/ECDSA keys
  • Other key formats

Rationale:

  • libsodium only supports Ed25519/X25519
  • Protocol assumes fixed-size keys (32 bytes public, 32/64 bytes private)
  • RSA/ECDSA would require OpenSSL and protocol changes

GPG Support Status

Current Status:

  • GPG key parsing code exists
  • GPG support may be disabled in build configuration
  • GPG agent support exists but may not be enabled

Workaround:

  • Use SSH keys instead of GPG keys
  • SSH keys provide equivalent functionality
  • SSH agent support is fully functional

API Reference

Unified Key Parsing:

asciichat_error_t parse_public_key(const char *input, public_key_t *key_out);

Key Validation:

Key Conversion:

Signing and Verification:

size_t message_len, uint8_t signature[64]);
asciichat_error_t ed25519_verify_signature(const uint8_t public_key[32], const uint8_t *message,
size_t message_len, const uint8_t signature[64]);

HTTPS Key Fetching:

asciichat_error_t fetch_github_ssh_keys(const char *username, char ***keys_out, size_t *num_keys);
asciichat_error_t fetch_gitlab_ssh_keys(const char *username, char ***keys_out, size_t *num_keys);
asciichat_error_t fetch_github_gpg_keys(const char *username, char ***keys_out, size_t *num_keys);
asciichat_error_t fetch_gitlab_gpg_keys(const char *username, char ***keys_out, size_t *num_keys);
See also
crypto/keys/keys.h
crypto/keys/types.h
crypto/keys/validation.h
crypto/keys/ssh_keys.h
crypto/keys/gpg_keys.h
crypto/keys/https_keys.h

Typedef Documentation

◆ crypto_handshake_context_t

#include <keys.h>

Definition at line 541 of file keys.h.

Enumeration Type Documentation

◆ key_type_t

enum key_type_t

#include <key_types.h>

Key type enumeration.

Represents the type of cryptographic key being used. All keys are ultimately converted to X25519 for key exchange.

Note
RSA and ECDSA are NOT supported because:
  • libsodium (our crypto library) only supports Ed25519/X25519
  • RSA/ECDSA require variable-length keys and signatures
  • Protocol assumes fixed 128-byte authenticated handshake (ephemeral:32 + identity:32 + sig:64)
  • Adding RSA/ECDSA support would require OpenSSL and protocol changes
Enumerator
KEY_TYPE_UNKNOWN 

Unknown or invalid key type

KEY_TYPE_ED25519 

SSH Ed25519 key (converts to X25519 for key exchange)

KEY_TYPE_X25519 

Native X25519 key (raw hex or base64)

KEY_TYPE_GPG 

GPG key (Ed25519 variant, derived to X25519)

Definition at line 50 of file key_types.h.

50 {
@ KEY_TYPE_UNKNOWN
Definition key_types.h:51
@ KEY_TYPE_ED25519
Definition key_types.h:52
@ KEY_TYPE_GPG
Definition key_types.h:54
@ KEY_TYPE_X25519
Definition key_types.h:53

Function Documentation

◆ build_github_gpg_url()

asciichat_error_t build_github_gpg_url ( const char *  username,
char *  url_out,
size_t  url_size 
)

#include <https_keys.h>

Construct GitHub GPG keys URL.

Parameters
usernameGitHub username (must not be NULL)
url_outOutput buffer for URL (must not be NULL)
url_sizeSize of URL buffer (must be >= 64)
Returns
ASCIICHAT_OK on success, error code on failure

Constructs GitHub GPG keys URL: https://github.com/username.gpg

Note
URL format: Uses standard GitHub GPG keys endpoint. Format: "https://github.com/{username}.gpg"
Buffer requirements: URL buffer must be at least 64 bytes (same as build_github_ssh_url).
Warning
URL buffer must be large enough for formatted URL.

Definition at line 104 of file https_keys.c.

104 {
105 if (!username || !url_out) {
106 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
107 return ERROR_INVALID_PARAM;
108 }
109
110 if (url_size < 64) {
111 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
112 return ERROR_INVALID_PARAM;
113 }
114
115 // Strip .gpg suffix if user already included it (e.g., "github:zfogg.gpg")
116 char clean_username[BUFFER_SIZE_SMALL];
117 SAFE_STRNCPY(clean_username, username, sizeof(clean_username) - 1);
118 size_t len = strlen(clean_username);
119 if (len > 4 && strcmp(clean_username + len - 4, ".gpg") == 0) {
120 clean_username[len - 4] = '\0'; // Remove .gpg suffix
121 }
122
123 // Construct GitHub GPG keys URL: https://github.com/username.gpg
124 int result = safe_snprintf(url_out, url_size, "https://github.com/%s.gpg", clean_username);
125 if (result < 0 || result >= (int)url_size) {
126 SET_ERRNO(ERROR_STRING, "Failed to construct GitHub GPG URL");
127 return ERROR_STRING;
128 }
129
130 return ASCIICHAT_OK;
131}
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_STRING
@ ERROR_INVALID_PARAM
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, ERROR_INVALID_PARAM, ERROR_STRING, safe_snprintf(), SAFE_STRNCPY, and SET_ERRNO.

Referenced by fetch_github_gpg_keys().

◆ build_github_ssh_url()

asciichat_error_t build_github_ssh_url ( const char *  username,
char *  url_out,
size_t  url_size 
)

#include <https_keys.h>

Construct GitHub SSH keys URL.

Parameters
usernameGitHub username (must not be NULL)
url_outOutput buffer for URL (must not be NULL)
url_sizeSize of URL buffer (must be >= 64)
Returns
ASCIICHAT_OK on success, error code on failure

Constructs GitHub SSH keys URL: https://github.com/username.keys

Note
URL format: Uses standard GitHub SSH keys endpoint. Format: "https://github.com/{username}.keys"
Buffer requirements: URL buffer must be at least 64 bytes. Format: "https://github.com/" (19 chars) + username + ".keys" (6 chars) = 25 + username length
Warning
URL buffer must be large enough for formatted URL. Function validates buffer size but may overflow if buffer is too small.

Definition at line 62 of file https_keys.c.

62 {
63 if (!username || !url_out) {
64 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
66 }
67
68 if (url_size < 64) {
69 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
71 }
72
73 // Construct GitHub SSH keys URL: https://github.com/username.keys
74 int result = safe_snprintf(url_out, url_size, "https://github.com/%s.keys", username);
75 if (result < 0 || result >= (int)url_size) {
76 SET_ERRNO(ERROR_STRING, "Failed to construct GitHub SSH URL");
77 return ERROR_STRING;
78 }
79
80 return ASCIICHAT_OK;
81}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_STRING, safe_snprintf(), and SET_ERRNO.

Referenced by fetch_github_ssh_keys().

◆ build_gitlab_gpg_url()

asciichat_error_t build_gitlab_gpg_url ( const char *  username,
char *  url_out,
size_t  url_size 
)

#include <https_keys.h>

Construct GitLab GPG keys URL.

Parameters
usernameGitLab username (must not be NULL)
url_outOutput buffer for URL (must not be NULL)
url_sizeSize of URL buffer (must be >= 64)
Returns
ASCIICHAT_OK on success, error code on failure

Constructs GitLab GPG keys URL: https://gitlab.com/username.gpg

Note
URL format: Uses standard GitLab GPG keys endpoint. Format: "https://gitlab.com/{username}.gpg"
Buffer requirements: URL buffer must be at least 64 bytes (same as build_gitlab_ssh_url).
Warning
URL buffer must be large enough for formatted URL.

Definition at line 133 of file https_keys.c.

133 {
134 if (!username || !url_out) {
135 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
136 return ERROR_INVALID_PARAM;
137 }
138
139 if (url_size < 64) {
140 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
141 return ERROR_INVALID_PARAM;
142 }
143
144 // Strip .gpg suffix if user already included it (e.g., "gitlab:zfogg.gpg")
145 char clean_username[BUFFER_SIZE_SMALL];
146 SAFE_STRNCPY(clean_username, username, sizeof(clean_username) - 1);
147 size_t len = strlen(clean_username);
148 if (len > 4 && strcmp(clean_username + len - 4, ".gpg") == 0) {
149 clean_username[len - 4] = '\0'; // Remove .gpg suffix
150 }
151
152 // Construct GitLab GPG keys URL: https://gitlab.com/username.gpg
153 int result = safe_snprintf(url_out, url_size, "https://gitlab.com/%s.gpg", clean_username);
154 if (result < 0 || result >= (int)url_size) {
155 SET_ERRNO(ERROR_STRING, "Failed to construct GitLab GPG URL");
156 return ERROR_STRING;
157 }
158
159 return ASCIICHAT_OK;
160}

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, ERROR_INVALID_PARAM, ERROR_STRING, safe_snprintf(), SAFE_STRNCPY, and SET_ERRNO.

Referenced by fetch_gitlab_gpg_keys().

◆ build_gitlab_ssh_url()

asciichat_error_t build_gitlab_ssh_url ( const char *  username,
char *  url_out,
size_t  url_size 
)

#include <https_keys.h>

Construct GitLab SSH keys URL.

Parameters
usernameGitLab username (must not be NULL)
url_outOutput buffer for URL (must not be NULL)
url_sizeSize of URL buffer (must be >= 64)
Returns
ASCIICHAT_OK on success, error code on failure

Constructs GitLab SSH keys URL: https://gitlab.com/username.keys

Note
URL format: Uses standard GitLab SSH keys endpoint. Format: "https://gitlab.com/{username}.keys"
Buffer requirements: URL buffer must be at least 64 bytes (same as build_github_ssh_url).
Warning
URL buffer must be large enough for formatted URL.

Definition at line 83 of file https_keys.c.

83 {
84 if (!username || !url_out) {
85 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
87 }
88
89 if (url_size < 64) {
90 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
92 }
93
94 // Construct GitLab SSH keys URL: https://gitlab.com/username.keys
95 int result = safe_snprintf(url_out, url_size, "https://gitlab.com/%s.keys", username);
96 if (result < 0 || result >= (int)url_size) {
97 SET_ERRNO(ERROR_STRING, "Failed to construct GitLab SSH URL");
98 return ERROR_STRING;
99 }
100
101 return ASCIICHAT_OK;
102}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_STRING, safe_snprintf(), and SET_ERRNO.

Referenced by fetch_gitlab_ssh_keys().

◆ check_gpg_key_expiry()

asciichat_error_t check_gpg_key_expiry ( const char *  gpg_key_text,
bool *  is_expired 
)

#include <gpg_keys.h>

Check if GPG key is expired.

Parameters
gpg_key_textGPG key in armored format (must not be NULL)
is_expiredOutput: true if key is expired, false otherwise (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Checks if GPG key has expired by parsing expiration date from key structure.

Note
Expiration checking: Parses OpenPGP packet structure to find expiration date. Compares expiration date with current date.
Default behavior: Currently defaults to not expired if parsing fails. Function returns error but sets is_expired to false.
Warning
NOT YET IMPLEMENTED: This function is not yet implemented. Returns ERROR_CRYPTO_KEY with "GPG key expiry checking not yet implemented". Defaults is_expired to false before returning error.
GPG support is currently disabled. Function will not work until implemented.

Definition at line 176 of file gpg_keys.c.

176 {
177 if (!gpg_key_text || !is_expired) {
178 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, is_expired=%p", gpg_key_text, is_expired);
179 return ERROR_INVALID_PARAM;
180 }
181
182 // For GPG key expiry checking, we need a key ID
183 // If gpg_key_text is a key ID (8, 16, or 40 hex chars), use it directly
184 // Otherwise, we'd need to parse the GPG armored text (complex)
185 size_t key_len = strlen(gpg_key_text);
186 const char *key_id = gpg_key_text;
187
188 // Check if it looks like a key ID (hex characters only)
189 bool is_hex = true;
190 for (size_t i = 0; i < key_len; i++) {
191 if (!((gpg_key_text[i] >= '0' && gpg_key_text[i] <= '9') || (gpg_key_text[i] >= 'A' && gpg_key_text[i] <= 'F') ||
192 (gpg_key_text[i] >= 'a' && gpg_key_text[i] <= 'f'))) {
193 is_hex = false;
194 break;
195 }
196 }
197
198 // Only support key ID format (8, 16, or 40 hex chars)
199 if (!is_hex || (key_len != 8 && key_len != 16 && key_len != 40)) {
200 log_warn("check_gpg_key_expiry: Input is not a key ID format (expected 8/16/40 hex chars)");
201 *is_expired = false; // Assume not expired if we can't check
202 return ASCIICHAT_OK;
203 }
204
205 // Use gpg --list-keys with colon-separated output to check expiry
206 char cmd[BUFFER_SIZE_MEDIUM];
207 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-colons %s " PLATFORM_SHELL_NULL_REDIRECT, key_id);
208
209 FILE *fp = NULL;
210 if (platform_popen("gpg_keys", cmd, "r", &fp) != ASCIICHAT_OK || !fp) {
211 log_error("Failed to run gpg --list-keys for key %s", key_id);
212 *is_expired = false; // Assume not expired if we can't check
213 return ASCIICHAT_OK;
214 }
215
216 char line[BUFFER_SIZE_LARGE];
217 bool found_pub = false;
218 *is_expired = false;
219
220 // Parse colon-separated output
221 // Format: pub:trust:keylen:algo:keyid:creation:expiry:...
222 // Field 6 is expiry timestamp (seconds since epoch), or empty if no expiry
223 while (fgets(line, sizeof(line), fp)) {
224 if (strncmp(line, "pub:", 4) == 0) {
225 found_pub = true;
226
227 // Split line by colons
228 char *fields[12];
229 int field_count = 0;
230 char *ptr = line;
231 char *field_start = ptr;
232
233 while (*ptr && field_count < 12) {
234 if (*ptr == ':' || *ptr == '\n') {
235 *ptr = '\0';
236 fields[field_count++] = field_start;
237 field_start = ptr + 1;
238 }
239 ptr++;
240 }
241
242 // Field 6 is expiry date (index 6)
243 if (field_count >= 7 && strlen(fields[6]) > 0) {
244 // Parse expiry timestamp
245 long expiry_timestamp = strtol(fields[6], NULL, 10);
246 time_t now = time(NULL);
247
248 if (expiry_timestamp > 0 && expiry_timestamp <= now) {
249 *is_expired = true;
250 log_warn("GPG key %s has expired (expiry: %ld, now: %ld)", key_id, expiry_timestamp, (long)now);
251 } else if (expiry_timestamp > 0) {
252 log_debug("GPG key %s expires at timestamp %ld (valid)", key_id, expiry_timestamp);
253 } else {
254 log_debug("GPG key %s has no expiration date", key_id);
255 }
256 } else {
257 log_debug("GPG key %s has no expiration date (field empty)", key_id);
258 }
259
260 break; // Found the pub line, stop parsing
261 }
262 }
263
264 platform_pclose(&fp);
265
266 if (!found_pub) {
267 log_warn("Could not find GPG key %s in keyring", key_id);
268 *is_expired = false; // Assume not expired if key not found
269 }
270
271 return ASCIICHAT_OK;
272}
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes)
#define BUFFER_SIZE_MEDIUM
Medium buffer size (512 bytes)
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
asciichat_error_t platform_popen(const char *name, const char *command, const char *mode, FILE **out_stream)
Execute a command and return a file stream for reading/writing.
#define PLATFORM_SHELL_NULL_REDIRECT
Shell null device/error redirect for platform.
asciichat_error_t platform_pclose(FILE **stream_ptr)
Close a process stream opened with platform_popen()

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, BUFFER_SIZE_MEDIUM, ERROR_INVALID_PARAM, log_debug, log_error, log_warn, platform_pclose(), platform_popen(), PLATFORM_SHELL_NULL_REDIRECT, safe_snprintf(), and SET_ERRNO.

◆ check_key_expiry()

asciichat_error_t check_key_expiry ( const public_key_t *  key,
bool *  is_expired 
)

#include <keys_validation.h>

Check if a key is expired.

Parameters
keyKey to check (must not be NULL)
is_expiredOutput: true if expired, false otherwise (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Checks if key has expired by parsing expiration date from key structure.

Note
Expiration checking: Would parse key creation dates and expiration times from key structure. Currently not implemented.
Current implementation: Always returns false (not expired). Key expiry checking is not yet implemented.
Warning
NOT YET IMPLEMENTED: This function always returns false (not expired). Key expiry checking is not yet implemented.

Definition at line 118 of file keys_validation.c.

118 {
119 if (!key || !is_expired) {
120 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, is_expired=%p", key, is_expired);
121 return ERROR_INVALID_PARAM;
122 }
123
124 // For now, we don't implement key expiry checking
125 // This would require parsing key creation dates and expiration times
126 *is_expired = false;
127
128 return ASCIICHAT_OK;
129}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ check_key_fingerprint()

asciichat_error_t check_key_fingerprint ( const public_key_t *  key,
const uint8_t *  fingerprint,
size_t  fingerprint_len,
bool *  matches 
)

#include <keys_validation.h>

Check if key matches a fingerprint.

Parameters
keyKey to check (must not be NULL)
fingerprintFingerprint to match against (must not be NULL)
fingerprint_lenLength of fingerprint (must be > 0)
matchesOutput: true if fingerprint matches, false otherwise (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Checks if key matches a fingerprint by computing key fingerprint and comparing.

Note
Fingerprint comparison: Computes key fingerprint using generate_key_fingerprint() and compares with provided fingerprint using constant-time comparison.
Constant-time: Uses constant-time comparison to prevent timing attacks.
Fingerprint format: Fingerprint is typically SHA-256 hash of key material. Length depends on hash algorithm used (typically 32 bytes for SHA-256).
Warning
Always uses constant-time comparison for fingerprints. Do NOT use regular memcmp() for fingerprint comparison.

Definition at line 420 of file keys_validation.c.

421 {
422 if (!key || !fingerprint || !matches) {
423 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, fingerprint=%p, matches=%p", key, fingerprint, matches);
424 return ERROR_INVALID_PARAM;
425 }
426
427 *matches = false;
428
429 // Generate fingerprint for the key
430 uint8_t key_fingerprint[32];
431 asciichat_error_t fingerprint_result = generate_key_fingerprint(key, key_fingerprint, 32);
432 if (fingerprint_result != ASCIICHAT_OK) {
433 return fingerprint_result;
434 }
435
436 // Compare fingerprints
437 size_t compare_len = (fingerprint_len < 32) ? fingerprint_len : 32;
438 if (sodium_memcmp(key_fingerprint, fingerprint, compare_len) == 0) {
439 *matches = true;
440 }
441
442 return ASCIICHAT_OK;
443}
asciichat_error_t generate_key_fingerprint(const public_key_t *key, uint8_t *fingerprint_out, size_t fingerprint_size)
Generate key fingerprint.

References ASCIICHAT_OK, ERROR_INVALID_PARAM, generate_key_fingerprint(), and SET_ERRNO.

◆ check_key_patterns()

asciichat_error_t check_key_patterns ( const public_key_t *  key,
bool *  has_weak_patterns 
)

#include <keys_validation.h>

Check for key reuse or weak patterns.

Parameters
keyKey to check (must not be NULL)
has_weak_patternsOutput: true if weak patterns found, false otherwise (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Checks key for weak patterns that could indicate security vulnerabilities. Detects keys with predictable or weak patterns.

Note
Pattern analysis: Would analyze key material for weak patterns. Currently not fully implemented.
Weak patterns: Would detect:
  • Keys with repeated patterns
  • Keys with low entropy
  • Keys matching known weak patterns
Warning
NOT YET FULLY IMPLEMENTED: Weak pattern detection is not yet fully implemented. Function may always return false (no weak patterns).

Definition at line 305 of file keys_validation.c.

305 {
306 if (!key || !has_weak_patterns) {
307 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, has_weak_patterns=%p", key, has_weak_patterns);
308 return ERROR_INVALID_PARAM;
309 }
310
311 *has_weak_patterns = false;
312
313 // Check for sequential patterns
314 bool is_sequential = true;
315 for (int i = 1; i < 32; i++) {
316 if (key->key[i] != key->key[i - 1] + 1) {
317 is_sequential = false;
318 break;
319 }
320 }
321
322 if (is_sequential) {
323 *has_weak_patterns = true;
324 return ASCIICHAT_OK;
325 }
326
327 // Check for repeated byte patterns (e.g., all 0xAA, all 0x55)
328 // Common weak patterns to check
329 const uint8_t weak_bytes[] = {0x00, 0xFF, 0xAA, 0x55, 0x11, 0x22, 0x33, 0x44,
330 0x66, 0x77, 0x88, 0x99, 0xBB, 0xCC, 0xDD, 0xEE};
331 for (size_t pattern_idx = 0; pattern_idx < sizeof(weak_bytes); pattern_idx++) {
332 uint8_t pattern_byte = weak_bytes[pattern_idx];
333 bool is_repeated = true;
334 for (size_t j = 0; j < 32; j++) {
335 // CORRECT: Compare against pattern_byte, not key->key[0]
336 if (key->key[j] != pattern_byte) {
337 is_repeated = false;
338 break;
339 }
340 }
341 if (is_repeated) {
342 *has_weak_patterns = true;
343 return ASCIICHAT_OK;
344 }
345 }
346
347 // Check for alternating bit patterns (e.g., 0xAA 0xBB 0xAA 0xBB or 0x55 0xAA 0x55 0xAA)
348 // A proper alternating pattern has at most 2 unique byte values that truly alternate
349 bool byte_appeared[256] = {0};
350 int unique_values = 0;
351 for (size_t i = 0; i < 32; i++) {
352 if (!byte_appeared[key->key[i]]) {
353 byte_appeared[key->key[i]] = true;
354 unique_values++;
355 }
356 }
357
358 // Only consider it weak if there are 2 or fewer unique values AND they properly alternate
359 if (unique_values <= 2) {
360 bool is_alternating = true;
361 for (size_t i = 1; i < 32; i++) {
362 // In a true alternating pattern:
363 // 1. Adjacent bytes must be different
364 // 2. Pattern must repeat every 2 bytes (byte[i] == byte[i-2])
365 if (key->key[i] == key->key[i - 1] || (i >= 2 && key->key[i] != key->key[i - 2])) {
366 is_alternating = false;
367 break;
368 }
369 }
370 if (is_alternating) {
371 *has_weak_patterns = true;
372 return ASCIICHAT_OK;
373 }
374 }
375
376 // Check for common cryptographic initialization patterns
377 // OpenSSL weak keys, old Debian SSH keys, etc.
378 uint32_t *words = (uint32_t *)key->key;
379 // Check if all 32-bit words are identical
380 bool all_words_same = true;
381 for (size_t i = 1; i < 8; i++) {
382 if (words[i] != words[0]) {
383 all_words_same = false;
384 break;
385 }
386 }
387 if (all_words_same) {
388 *has_weak_patterns = true;
389 return ASCIICHAT_OK;
390 }
391
392 return ASCIICHAT_OK;
393}
unsigned int uint32_t
Definition common.h:58
uint8_t key[32]
Definition key_types.h:71

References ASCIICHAT_OK, ERROR_INVALID_PARAM, public_key_t::key, and SET_ERRNO.

◆ check_key_strength()

asciichat_error_t check_key_strength ( const public_key_t *  key,
bool *  is_weak 
)

#include <keys_validation.h>

Check if key has weak parameters.

Parameters
keyKey to check (must not be NULL)
is_weakOutput: true if weak, false otherwise (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Checks if key has weak cryptographic parameters. Detects keys with weak entropy or predictable patterns.

Note
Weak key detection: Would analyze key material for weak patterns. Currently not fully implemented.
Security analysis: Would check for:
  • Low entropy keys
  • Predictable patterns
  • Known weak keys
Warning
NOT YET FULLY IMPLEMENTED: Weak key detection is not yet fully implemented. Function may always return false (not weak).

Definition at line 255 of file keys_validation.c.

255 {
256 if (!key || !is_weak) {
257 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, is_weak=%p", key, is_weak);
258 return ERROR_INVALID_PARAM;
259 }
260
261 *is_weak = false;
262
263 // Check for all zeros (already checked in validate_public_key)
264 // Check for all ones
265 bool is_ones = true;
266 for (int i = 0; i < 32; i++) {
267 if (key->key[i] != 0xFF) {
268 is_ones = false;
269 break;
270 }
271 }
272
273 if (is_ones) {
274 *is_weak = true;
275 return ASCIICHAT_OK;
276 }
277
278 // Check for low entropy: Count unique bytes
279 // A properly random 32-byte key should have ~24-32 unique values
280 // Low entropy keys will have very few unique bytes
281 bool byte_seen[256] = {0};
282 int unique_bytes = 0;
283 for (int i = 0; i < 32; i++) {
284 uint8_t byte_val = key->key[i];
285 if (!byte_seen[byte_val]) {
286 byte_seen[byte_val] = true;
287 unique_bytes++;
288 }
289 }
290
291 // If less than 8 unique bytes in 32 bytes, entropy is very low
292 // This indicates non-random key generation (e.g., PRNG seed not properly initialized)
293 if (unique_bytes < 8) {
294 *is_weak = true;
295 return ASCIICHAT_OK;
296 }
297
298 // Additional weak key pattern detection:
299 // - Check for repeated patterns (handled in check_key_patterns)
300 // - Check for known weak sequences (see check_key_patterns)
301
302 return ASCIICHAT_OK;
303}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, public_key_t::key, and SET_ERRNO.

◆ compare_public_keys()

asciichat_error_t compare_public_keys ( const public_key_t *  key1,
const public_key_t *  key2,
bool *  are_equal 
)

#include <keys_validation.h>

Compare two public keys for equality.

Parameters
key1First key to compare (must not be NULL)
key2Second key to compare (must not be NULL)
are_equalOutput: true if keys are equal, false otherwise (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Compares two public keys for equality using constant-time comparison. Compares key type and key data.

Note
Key comparison: Compares key type and 32-byte key data. Uses constant-time comparison to prevent timing attacks.
Constant-time: Uses sodium_memcmp() for constant-time comparison. Prevents timing attacks that could leak key information.
Comment comparison: Comments are NOT compared. Only key type and key data are compared.
Warning
Always uses constant-time comparison for keys. Do NOT use regular memcmp() for key comparison.

Definition at line 399 of file keys_validation.c.

399 {
400 if (!key1 || !key2 || !are_equal) {
401 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key1=%p, key2=%p, are_equal=%p", key1, key2, are_equal);
402 return ERROR_INVALID_PARAM;
403 }
404
405 *are_equal = false;
406
407 // Compare key types
408 if (key1->type != key2->type) {
409 return ASCIICHAT_OK;
410 }
411
412 // Compare key data using constant-time comparison
413 if (sodium_memcmp(key1->key, key2->key, 32) == 0) {
414 *are_equal = true;
415 }
416
417 return ASCIICHAT_OK;
418}
key_type_t type
Definition key_types.h:70

References ASCIICHAT_OK, ERROR_INVALID_PARAM, public_key_t::key, SET_ERRNO, and public_key_t::type.

◆ ed25519_sign_message()

asciichat_error_t ed25519_sign_message ( const private_key_t *  key,
const uint8_t *  message,
size_t  message_len,
uint8_t  signature[64] 
)

#include <keys.h>

Sign a message with Ed25519 (uses SSH agent if available, otherwise in-memory key)

Sign a message with Ed25519 private key.

Parameters
keyPrivate key for signing (must not be NULL)
messageMessage to sign (must not be NULL)
message_lenLength of message to sign
signatureOutput buffer for Ed25519 signature (64 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Signs a message using Ed25519. This is the main signing function that abstracts SSH agent vs in-memory signing.

Note
Agent support: If use_ssh_agent is true, uses SSH agent for signing. Key stays in agent (not loaded into memory). Signing happens via agent protocol.
In-memory signing: If use_ssh_agent is false, uses in-memory key for signing. Key must be loaded into memory (via parse_private_key()).

Setting use_gpg_agent=true will not work until GPG support is re-enabled.

Note
Signature format: Ed25519 signature is always 64 bytes (R || S format).
Warning
Agent availability: If use_ssh_agent is true but SSH agent is not available, function returns error. Check SSH agent availability before using.

exists but will not work until GPG support is re-enabled.

Parameters
keyEd25519 private key (must not be NULL)
messageMessage to sign (must not be NULL)
message_lenLength of message to sign
signatureOutput buffer for Ed25519 signature (64 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Signs a message using Ed25519. Uses SSH agent if available, otherwise in-memory key.

Note
Agent support: If use_ssh_agent is true, uses SSH agent for signing. Key stays in agent (not loaded into memory). Signing happens via agent protocol.
In-memory signing: If use_ssh_agent is false, uses in-memory key for signing. Key must be loaded into memory (via parse_ssh_private_key()).
Signature format: Ed25519 signature is always 64 bytes (R || S format).
Warning
Agent availability: If use_ssh_agent is true but SSH agent is not available, function returns error. Check SSH agent availability before using.

Definition at line 1030 of file ssh_keys.c.

1031 {
1032 if (!key || !message || !signature) {
1033 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, message=%p, signature=%p", key, message,
1034 signature);
1035 }
1036
1037 if (key->type != KEY_TYPE_ED25519) {
1038 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key is not an Ed25519 key");
1039 }
1040
1041 // If using GPG agent, delegate signing to GPG agent
1042 if (key->use_gpg_agent) {
1043 log_debug("Using GPG agent for Ed25519 signing (keygrip: %.40s)", key->gpg_keygrip);
1044
1045 // Connect to GPG agent
1046 int agent_sock = gpg_agent_connect();
1047 log_debug("GPG agent connection result: %d", agent_sock);
1048
1049 if (agent_sock < 0) {
1050 log_debug("GPG agent not available, falling back to gpg --detach-sign for signing");
1051
1052 // Extract GPG key ID from key comment ("GPG key <key_id>")
1053 const char *key_id_prefix = "GPG key ";
1054 const char *key_id = NULL;
1055 if (strncmp(key->key_comment, key_id_prefix, strlen(key_id_prefix)) == 0) {
1056 key_id = key->key_comment + strlen(key_id_prefix);
1057 }
1058
1059 log_debug("Extracted GPG key ID: %s (len=%zu)", key_id ? key_id : "NULL", key_id ? strlen(key_id) : 0);
1060
1061 if (!key_id || strlen(key_id) != 16) {
1062 return SET_ERRNO(ERROR_CRYPTO, "Cannot extract GPG key ID from comment for fallback signing");
1063 }
1064
1065 // Use gpg --detach-sign fallback
1066 log_debug("Calling gpg_sign_detached_ed25519 with key %s", key_id);
1067 int result = gpg_sign_detached_ed25519(key_id, message, message_len, signature);
1068 log_debug("gpg_sign_detached_ed25519 result: %d", result);
1069
1070 if (result != 0) {
1071 return SET_ERRNO(ERROR_CRYPTO, "GPG fallback signing failed (both agent and gpg command failed)");
1072 }
1073
1074 log_debug("Successfully signed message with gpg --detach-sign fallback (64 bytes)");
1075 return ASCIICHAT_OK;
1076 }
1077
1078 log_debug("Using GPG agent for signing");
1079
1080 // Sign the message using GPG agent
1081 size_t sig_len = 0;
1082 int result = gpg_agent_sign(agent_sock, key->gpg_keygrip, message, message_len, signature, &sig_len);
1083
1084 // Disconnect from agent
1085 gpg_agent_disconnect(agent_sock);
1086
1087 if (result != 0) {
1088 return SET_ERRNO(ERROR_CRYPTO, "GPG agent signing failed");
1089 }
1090
1091 if (sig_len != 64) {
1092 return SET_ERRNO(ERROR_CRYPTO, "GPG agent returned invalid signature length: %zu (expected 64)", sig_len);
1093 }
1094
1095 log_debug("Successfully signed message with GPG agent (64 bytes)");
1096 return ASCIICHAT_OK;
1097 }
1098
1099 // If using SSH agent, delegate signing to SSH agent
1100 if (key->use_ssh_agent) {
1101 log_debug("Using SSH agent for Ed25519 signing");
1102
1103 // Create a public_key_t from the stored public key
1104 public_key_t pub_key = {0};
1105 pub_key.type = KEY_TYPE_ED25519;
1106 memcpy(pub_key.key, key->public_key, 32);
1107
1108 // Sign using SSH agent
1109 asciichat_error_t result = ssh_agent_sign(&pub_key, message, message_len, signature);
1110 if (result != ASCIICHAT_OK) {
1111 return result; // Error already set by ssh_agent_sign
1112 }
1113
1114 log_debug("Successfully signed message with SSH agent (64 bytes)");
1115 return ASCIICHAT_OK;
1116 }
1117
1118 // Sign the message with Ed25519 (in-memory key)
1119 if (crypto_sign_detached(signature, NULL, message, message_len, key->key.ed25519) != 0) {
1120 return SET_ERRNO(ERROR_CRYPTO, "Failed to sign message with Ed25519");
1121 }
1122
1123 return ASCIICHAT_OK;
1124}
int gpg_agent_connect(void)
Connect to gpg-agent.
Definition agent.c:96
int gpg_agent_sign(int handle_as_int, 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.
Definition agent.c:154
int gpg_sign_detached_ed25519(const char *key_id, const uint8_t *message, size_t message_len, uint8_t signature_out[64])
Sign message with GPG and extract raw Ed25519 signature.
Definition signing.c:140
void gpg_agent_disconnect(int handle_as_int)
Disconnect from gpg-agent.
Definition agent.c:146
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.
Definition ssh_agent.c:256
@ ERROR_CRYPTO_KEY
Definition error_codes.h:97
@ ERROR_CRYPTO
Definition error_codes.h:96
key_type_t type
Definition key_types.h:92
char key_comment[256]
Definition key_types.h:100
char gpg_keygrip[64]
Definition key_types.h:101
bool use_gpg_agent
Definition key_types.h:98
uint8_t public_key[32]
Definition key_types.h:99
uint8_t ed25519[64]
Definition key_types.h:94
union private_key_t::@14 key

References ASCIICHAT_OK, private_key_t::ed25519, ERROR_CRYPTO, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, gpg_agent_connect(), gpg_agent_disconnect(), gpg_agent_sign(), private_key_t::gpg_keygrip, gpg_sign_detached_ed25519(), public_key_t::key, private_key_t::key, private_key_t::key_comment, KEY_TYPE_ED25519, log_debug, private_key_t::public_key, SET_ERRNO, ssh_agent_sign(), public_key_t::type, private_key_t::type, private_key_t::use_gpg_agent, and private_key_t::use_ssh_agent.

Referenced by crypto_handshake_client_key_exchange(), and crypto_handshake_server_start().

◆ ed25519_to_x25519_private()

asciichat_error_t ed25519_to_x25519_private ( const uint8_t  ed25519_sk[64],
uint8_t  x25519_sk[32] 
)

#include <ssh_keys.h>

Convert Ed25519 private key to X25519 for key exchange.

Parameters
ed25519_skEd25519 private key (64 bytes: seed + public, must not be NULL)
x25519_skOutput buffer for X25519 private key (32 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Converts Ed25519 private key to X25519 format for Diffie-Hellman key exchange. Uses libsodium's conversion function.

Note
Ed25519 key format: Ed25519 private key is 64 bytes (32-byte seed + 32-byte public key). Function extracts seed (first 32 bytes) and converts to X25519 scalar.
Conversion: Uses crypto_sign_ed25519_sk_to_curve25519() from libsodium. Conversion is mathematically safe (same curve, different representation).
Key size: Ed25519 private key is 64 bytes, X25519 private key is 32 bytes. Extracts 32-byte seed from Ed25519 key and converts to X25519 scalar.
Use case: Used for key exchange when Ed25519 key is used for signing but X25519 is needed for key exchange (DH).
Warning
Ed25519 key must be exactly 64 bytes (32-byte seed + 32-byte public). Function validates key size before conversion.

Definition at line 1013 of file ssh_keys.c.

1013 {
1014 if (!ed25519_sk || !x25519_sk) {
1015 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ed25519_sk=%p, x25519_sk=%p", ed25519_sk, x25519_sk);
1016 }
1017
1018 // Convert Ed25519 private key to X25519 private key
1019 if (crypto_sign_ed25519_sk_to_curve25519(x25519_sk, ed25519_sk) != 0) {
1020 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 private key to X25519");
1021 }
1022
1023 return ASCIICHAT_OK;
1024}

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by private_key_to_x25519().

◆ ed25519_to_x25519_public()

asciichat_error_t ed25519_to_x25519_public ( const uint8_t  ed25519_pk[32],
uint8_t  x25519_pk[32] 
)

#include <ssh_keys.h>

Convert Ed25519 public key to X25519 for key exchange.

Parameters
ed25519_pkEd25519 public key (32 bytes, must not be NULL)
x25519_pkOutput buffer for X25519 public key (32 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Converts Ed25519 public key to X25519 format for Diffie-Hellman key exchange. Uses libsodium's conversion function.

Note
Conversion: Uses crypto_sign_ed25519_pk_to_curve25519() from libsodium. Conversion is mathematically safe (same curve, different representation).
Key size: Both Ed25519 and X25519 keys are 32 bytes. No size change during conversion.
Use case: Used for key exchange when Ed25519 key is used for signing but X25519 is needed for key exchange (DH).
Warning
All keys must be 32 bytes. Function validates key size before conversion.

Definition at line 1000 of file ssh_keys.c.

1000 {
1001 if (!ed25519_pk || !x25519_pk) {
1002 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ed25519_pk=%p, x25519_pk=%p", ed25519_pk, x25519_pk);
1003 }
1004
1005 // Convert Ed25519 public key to X25519 public key
1006 if (crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk) != 0) {
1007 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 public key to X25519");
1008 }
1009
1010 return ASCIICHAT_OK;
1011}

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by public_key_to_x25519().

◆ ed25519_verify_signature()

asciichat_error_t ed25519_verify_signature ( const uint8_t  public_key[32],
const uint8_t *  message,
size_t  message_len,
const uint8_t  signature[64],
const char *  gpg_key_id 
)

#include <keys.h>

Verify an Ed25519 signature.

Parameters
public_keyEd25519 public key (32 bytes)
messageMessage that was signed (must not be NULL)
message_lenLength of message that was signed
signatureEd25519 signature to verify (64 bytes)
Returns
ASCIICHAT_OK if signature is valid, error code on failure

Verifies an Ed25519 signature using libsodium's verification function.

Note
Signature format: Ed25519 signature is always 64 bytes (R || S format).
Verification: Uses crypto_sign_ed25519_verify_detached() from libsodium. Returns error if signature is invalid or message was tampered with.
Constant-time: Verification uses constant-time comparison to prevent timing attacks.
Warning
Always check return value. Error indicates invalid signature or tampering.
Parameters
public_keyEd25519 public key (32 bytes, must not be NULL)
messageMessage that was signed (must not be NULL)
message_lenLength of message that was signed
signatureEd25519 signature to verify (64 bytes, must not be NULL)
Returns
ASCIICHAT_OK if signature is valid, error code on failure

Verifies an Ed25519 signature using libsodium's verification function.

Note
Signature format: Ed25519 signature is always 64 bytes (R || S format).
Verification: Uses crypto_sign_ed25519_verify_detached() from libsodium. Returns error if signature is invalid or message was tampered with.
Constant-time: Verification uses constant-time comparison to prevent timing attacks.
Warning
Always check return value. Error indicates invalid signature or tampering.

Definition at line 1126 of file ssh_keys.c.

1127 {
1128 if (!public_key || !message || !signature) {
1129 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: public_key=%p, message=%p, signature=%p", public_key,
1130 message, signature);
1131 }
1132
1133 // Try standard Ed25519 verification first
1134 if (crypto_sign_verify_detached(signature, message, message_len, public_key) == 0) {
1135 return ASCIICHAT_OK;
1136 }
1137
1138 // If standard verification fails, try GPG fallback (for GPG-signed messages)
1139 // Use provided gpg_key_id if available, otherwise check environment variable (for tests)
1140 const char *key_id_to_use = gpg_key_id;
1141 size_t key_id_len = key_id_to_use ? strlen(key_id_to_use) : 0;
1142 // Accept 8, 16, or 40 character GPG key IDs (short, long, or full fingerprint)
1143 bool valid_key_id = (key_id_len == 8 || key_id_len == 16 || key_id_len == 40);
1144
1145 if (!valid_key_id) {
1146 key_id_to_use = platform_getenv("TEST_GPG_KEY_ID");
1147 key_id_len = key_id_to_use ? strlen(key_id_to_use) : 0;
1148 valid_key_id = (key_id_len == 8 || key_id_len == 16 || key_id_len == 40);
1149 }
1150
1151 if (valid_key_id) {
1152 log_debug("Standard Ed25519 verification failed, trying GPG fallback with key %s", key_id_to_use);
1153 int gpg_result = gpg_verify_detached_ed25519(key_id_to_use, message, message_len, signature);
1154 if (gpg_result == 0) {
1155 log_debug("GPG verification succeeded");
1156 return ASCIICHAT_OK;
1157 }
1158 log_debug("GPG verification also failed");
1159 }
1160
1161 return SET_ERRNO(ERROR_CRYPTO, "Ed25519 signature verification failed (tried both libsodium and GPG)");
1162}
int gpg_verify_detached_ed25519(const char *key_id, const uint8_t *message, size_t message_len, const uint8_t signature[64])
Verify Ed25519 signature using GPG binary.
const char * platform_getenv(const char *name)
Get an environment variable value.
Definition wasm/system.c:39

References ASCIICHAT_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, gpg_verify_detached_ed25519(), log_debug, platform_getenv(), and SET_ERRNO.

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

◆ extract_ed25519_from_gpg()

asciichat_error_t extract_ed25519_from_gpg ( const char *  gpg_key_text,
uint8_t  ed25519_pk[32] 
)

#include <gpg_keys.h>

Extract Ed25519 public key from GPG key.

Parameters
gpg_key_textGPG key in armored format (must not be NULL)
ed25519_pkOutput buffer for Ed25519 public key (32 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Extracts Ed25519 public key from GPG key by parsing OpenPGP packet structure.

Note
OpenPGP parsing: Parses OpenPGP packet structure to find Ed25519 public key packet. Requires parsing of packet headers and packet data.
Key extraction: Finds Ed25519 subkey in GPG key structure. Extracts 32-byte Ed25519 public key material.
Warning
NOT YET IMPLEMENTED: This function is not yet implemented. Returns ERROR_CRYPTO_KEY with "Ed25519 extraction from GPG key not yet implemented".
GPG support is currently disabled. Function will not work until implemented.

Definition at line 107 of file gpg_keys.c.

107 {
108 if (!gpg_key_id || !ed25519_pk) {
109 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_id=%p, ed25519_pk=%p", gpg_key_id, ed25519_pk);
110 return ERROR_INVALID_PARAM;
111 }
112
113 // Use gpg_get_public_key() to extract Ed25519 public key from GPG keyring
114 // Note: gpg_key_id should be the key ID (e.g., "7FE90A79F2E80ED3")
115 char keygrip[64];
116 if (gpg_get_public_key(gpg_key_id, ed25519_pk, keygrip) != 0) {
117 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to extract Ed25519 public key from GPG for key ID: %s", gpg_key_id);
118 }
119
120 return ASCIICHAT_OK;
121}
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.
Definition export.c:248

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, gpg_get_public_key(), and SET_ERRNO.

Referenced by gpg_to_x25519_public(), and parse_gpg_key().

◆ extract_gpg_key_comment()

asciichat_error_t extract_gpg_key_comment ( const char *  gpg_key_text,
char *  comment_out,
size_t  comment_size 
)

#include <gpg_keys.h>

Extract key comment/email from GPG key.

Parameters
gpg_key_textGPG key in armored format (must not be NULL)
comment_outOutput buffer for comment (must not be NULL)
comment_sizeSize of comment buffer (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Extracts comment/email from GPG key by parsing user ID packet.

Note
Comment extraction: Parses OpenPGP packet structure to find user ID packet. Extracts comment/email from user ID packet.
User ID format: GPG user IDs typically contain name and email. Format: "Name <email@example.com>" or just "email@example.com"
Warning
GPG support is currently disabled. Function may not work until GPG support is re-enabled.

Definition at line 315 of file gpg_keys.c.

315 {
316 if (!gpg_key_text || !comment_out) {
317 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, comment_out=%p", gpg_key_text, comment_out);
318 return ERROR_INVALID_PARAM;
319 }
320
321 if (comment_size == 0) {
322 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid comment size: %zu", comment_size);
323 return ERROR_INVALID_PARAM;
324 }
325
326 // TODO: Implement GPG key comment extraction
327 // This requires parsing the OpenPGP packet structure and extracting user ID packets
328
329 // For now, use a generic comment
330 SAFE_STRNCPY(comment_out, "GPG key", comment_size - 1);
331 return ASCIICHAT_OK;
332}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, SAFE_STRNCPY, and SET_ERRNO.

◆ fetch_github_gpg_keys()

asciichat_error_t fetch_github_gpg_keys ( const char *  username,
char ***  keys_out,
size_t *  num_keys 
)

#include <https_keys.h>

Fetch GPG keys from GitHub using HTTPS.

Fetch GPG keys from GitHub using BearSSL.

Parameters
usernameGitHub username (must not be NULL)
keys_outOutput array of GPG key strings (caller must free each string and array)
num_keysOutput parameter for number of keys fetched
Returns
ASCIICHAT_OK on success, error code on failure

Fetches GPG keys from GitHub: GET https://github.com/username.gpg

Note
Network operation: Requires network connectivity and valid GitHub username. May fail if network is unavailable or username doesn't exist.
GPG format: Fetches GPG keys in armored format. Response contains GPG key blocks (--—BEGIN PGP PUBLIC KEY BLOCK--—).
Memory management: Caller must free each key string and the keys array (same as fetch_github_ssh_keys).
Warning
Network dependency: Requires active network connection.
GPG support: GPG key parsing may not work until GPG support is re-enabled.
Parameters
usernameGitHub username (must not be NULL)
keys_outOutput array of GPG key strings (caller must free each string and array)
num_keysOutput parameter for number of GPG keys fetched
Returns
ASCIICHAT_OK on success, error code on failure

Fetches GPG keys from GitHub: GET https://github.com/username.gpg

Note
Memory management: Caller must free each key string and the keys array (same as fetch_github_keys).
Warning
Network dependency: Requires active network connection.

Definition at line 230 of file https_keys.c.

230 {
231 if (!username || !keys_out || !num_keys) {
232 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
233 num_keys);
234 return ERROR_INVALID_PARAM;
235 }
236
237 // Build the GitHub GPG keys URL
238 char url[BUFFER_SIZE_SMALL];
239 asciichat_error_t url_result = build_github_gpg_url(username, url, sizeof(url));
240 if (url_result != ASCIICHAT_OK) {
241 return url_result;
242 }
243
244 // Fetch the keys using HTTPS
245 char *response_text = NULL;
246 size_t response_len = 0;
247 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
248 if (fetch_result != ASCIICHAT_OK) {
249 return fetch_result;
250 }
251
252 // Parse GPG keys from the response
253 asciichat_error_t parse_result =
254 parse_gpg_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
255
256 // Clean up response text
257 SAFE_FREE(response_text);
258
259 return parse_result;
260}
#define SAFE_FREE(ptr)
Definition common.h:376
asciichat_error_t build_github_gpg_url(const char *username, char *url_out, size_t url_size)
Construct GitHub GPG keys URL.
Definition https_keys.c:104
asciichat_error_t parse_gpg_keys_from_response(const char *response_text, size_t response_len, char ***keys_out, size_t *num_keys, size_t max_keys)
Parse GPG keys from HTTPS response text.
Definition https_keys.c:396

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, build_github_gpg_url(), ERROR_INVALID_PARAM, MAX_CLIENTS, parse_gpg_keys_from_response(), SAFE_FREE, and SET_ERRNO.

Referenced by fetch_github_keys(), and parse_public_keys().

◆ fetch_github_keys()

asciichat_error_t fetch_github_keys ( const char *  username,
char ***  keys_out,
size_t *  num_keys,
bool  use_gpg 
)

#include <keys.h>

Fetch SSH/GPG keys from GitHub using BearSSL.

Parameters
usernameGitHub username (must not be NULL)
keys_outOutput array of key strings (caller must free each string and array)
num_keysOutput parameter for number of keys fetched
use_gpgTrue to fetch GPG keys, false to fetch SSH keys
Returns
ASCIICHAT_OK on success, error code on failure

Fetches keys from GitHub:

Note
Network operation: Requires network connectivity and valid GitHub username. May fail if network is unavailable or username doesn't exist.
Memory management: Caller must free each key string and the keys array: c for (size_t i = 0; i < num_keys; i++) { free(keys_out[i]); } free(keys_out);
Warning
Network dependency: Requires active network connection. May fail if GitHub is unreachable or username doesn't exist.

Definition at line 516 of file keys.c.

516 {
517 if (!username || !keys_out || !num_keys) {
518 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_github_keys");
519 }
520
521 if (use_gpg) {
522 return fetch_github_gpg_keys(username, keys_out, num_keys);
523 } else {
524 return fetch_github_ssh_keys(username, keys_out, num_keys);
525 }
526}

References ERROR_INVALID_PARAM, fetch_github_gpg_keys(), fetch_github_ssh_keys(), and SET_ERRNO.

◆ fetch_github_ssh_keys()

asciichat_error_t fetch_github_ssh_keys ( const char *  username,
char ***  keys_out,
size_t *  num_keys 
)

#include <https_keys.h>

Fetch SSH keys from GitHub using HTTPS.

Parameters
usernameGitHub username (must not be NULL)
keys_outOutput array of SSH key strings (caller must free each string and array)
num_keysOutput parameter for number of keys fetched
Returns
ASCIICHAT_OK on success, error code on failure

Fetches SSH keys from GitHub: GET https://github.com/username.keys

Note
Network operation: Requires network connectivity and valid GitHub username. May fail if network is unavailable or username doesn't exist.
Key format: Only Ed25519 SSH keys are parsed from response. Other key types (RSA, ECDSA) are skipped.
Memory management: Caller must free each key string and the keys array: c for (size_t i = 0; i < num_keys; i++) { free(keys_out[i]); } free(keys_out);
Response parsing: Parses response as one key per line. Each line is parsed as SSH key format ("ssh-ed25519 AAAAC3... comment").
Warning
Network dependency: Requires active network connection. May fail if GitHub is unreachable or username doesn't exist.
Memory leak: Caller must free returned strings and array. Function allocates memory that must be freed.

Definition at line 166 of file https_keys.c.

166 {
167 if (!username || !keys_out || !num_keys) {
168 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
169 num_keys);
170 return ERROR_INVALID_PARAM;
171 }
172
173 // Build the GitHub SSH keys URL
174 char url[BUFFER_SIZE_SMALL];
175 asciichat_error_t url_result = build_github_ssh_url(username, url, sizeof(url));
176 if (url_result != ASCIICHAT_OK) {
177 return url_result;
178 }
179
180 // Fetch the keys using HTTPS
181 char *response_text = NULL;
182 size_t response_len = 0;
183 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
184 if (fetch_result != ASCIICHAT_OK) {
185 return fetch_result;
186 }
187
188 // Parse SSH keys from the response
189 asciichat_error_t parse_result =
190 parse_ssh_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
191
192 // Clean up response text
193 SAFE_FREE(response_text);
194
195 return parse_result;
196}
asciichat_error_t build_github_ssh_url(const char *username, char *url_out, size_t url_size)
Construct GitHub SSH keys URL.
Definition https_keys.c:62
asciichat_error_t parse_ssh_keys_from_response(const char *response_text, size_t response_len, char ***keys_out, size_t *num_keys, size_t max_keys)
Parse SSH keys from HTTPS response text.
Definition https_keys.c:298

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, build_github_ssh_url(), ERROR_INVALID_PARAM, MAX_CLIENTS, parse_ssh_keys_from_response(), SAFE_FREE, and SET_ERRNO.

Referenced by fetch_github_keys(), and parse_public_keys().

◆ fetch_gitlab_gpg_keys()

asciichat_error_t fetch_gitlab_gpg_keys ( const char *  username,
char ***  keys_out,
size_t *  num_keys 
)

#include <https_keys.h>

Fetch GPG keys from GitLab using HTTPS.

Fetch GPG keys from GitLab using BearSSL.

Parameters
usernameGitLab username (must not be NULL)
keys_outOutput array of GPG key strings (caller must free each string and array)
num_keysOutput parameter for number of keys fetched
Returns
ASCIICHAT_OK on success, error code on failure

Fetches GPG keys from GitLab: GET https://gitlab.com/username.gpg

Note
Network operation: Requires network connectivity and valid GitLab username. May fail if network is unavailable or username doesn't exist.
GPG format: Fetches GPG keys in armored format. Response contains GPG key blocks (--—BEGIN PGP PUBLIC KEY BLOCK--—).
Memory management: Caller must free each key string and the keys array (same as fetch_gitlab_ssh_keys).
Warning
Network dependency: Requires active network connection.
GPG support: GPG key parsing may not work until GPG support is re-enabled.
Parameters
usernameGitLab username (must not be NULL)
keys_outOutput array of GPG key strings (caller must free each string and array)
num_keysOutput parameter for number of GPG keys fetched
Returns
ASCIICHAT_OK on success, error code on failure

Fetches GPG keys from GitLab: GET https://gitlab.com/username.gpg

Note
Memory management: Caller must free each key string and the keys array (same as fetch_gitlab_keys).
Warning
Network dependency: Requires active network connection.

Definition at line 262 of file https_keys.c.

262 {
263 if (!username || !keys_out || !num_keys) {
264 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
265 num_keys);
266 return ERROR_INVALID_PARAM;
267 }
268
269 // Build the GitLab GPG keys URL
270 char url[BUFFER_SIZE_SMALL];
271 asciichat_error_t url_result = build_gitlab_gpg_url(username, url, sizeof(url));
272 if (url_result != ASCIICHAT_OK) {
273 return url_result;
274 }
275
276 // Fetch the keys using HTTPS
277 char *response_text = NULL;
278 size_t response_len = 0;
279 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
280 if (fetch_result != ASCIICHAT_OK) {
281 return fetch_result;
282 }
283
284 // Parse GPG keys from the response
285 asciichat_error_t parse_result =
286 parse_gpg_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
287
288 // Clean up response text
289 SAFE_FREE(response_text);
290
291 return parse_result;
292}
asciichat_error_t build_gitlab_gpg_url(const char *username, char *url_out, size_t url_size)
Construct GitLab GPG keys URL.
Definition https_keys.c:133

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, build_gitlab_gpg_url(), ERROR_INVALID_PARAM, MAX_CLIENTS, parse_gpg_keys_from_response(), SAFE_FREE, and SET_ERRNO.

Referenced by fetch_gitlab_keys(), and parse_public_keys().

◆ fetch_gitlab_keys()

asciichat_error_t fetch_gitlab_keys ( const char *  username,
char ***  keys_out,
size_t *  num_keys,
bool  use_gpg 
)

#include <keys.h>

Fetch SSH/GPG keys from GitLab using BearSSL.

Parameters
usernameGitLab username (must not be NULL)
keys_outOutput array of key strings (caller must free each string and array)
num_keysOutput parameter for number of keys fetched
use_gpgTrue to fetch GPG keys, false to fetch SSH keys
Returns
ASCIICHAT_OK on success, error code on failure

Fetches keys from GitLab:

Note
Network operation: Requires network connectivity and valid GitLab username. May fail if network is unavailable or username doesn't exist.
Memory management: Caller must free each key string and the keys array (same as fetch_github_keys).
Warning
Network dependency: Requires active network connection. May fail if GitLab is unreachable or username doesn't exist.

Definition at line 528 of file keys.c.

528 {
529 if (!username || !keys_out || !num_keys) {
530 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_gitlab_keys");
531 }
532
533 if (use_gpg) {
534 return fetch_gitlab_gpg_keys(username, keys_out, num_keys);
535 } else {
536 return fetch_gitlab_ssh_keys(username, keys_out, num_keys);
537 }
538}

References ERROR_INVALID_PARAM, fetch_gitlab_gpg_keys(), fetch_gitlab_ssh_keys(), and SET_ERRNO.

◆ fetch_gitlab_ssh_keys()

asciichat_error_t fetch_gitlab_ssh_keys ( const char *  username,
char ***  keys_out,
size_t *  num_keys 
)

#include <https_keys.h>

Fetch SSH keys from GitLab using HTTPS.

Parameters
usernameGitLab username (must not be NULL)
keys_outOutput array of SSH key strings (caller must free each string and array)
num_keysOutput parameter for number of keys fetched
Returns
ASCIICHAT_OK on success, error code on failure

Fetches SSH keys from GitLab: GET https://gitlab.com/username.keys

Note
Network operation: Requires network connectivity and valid GitLab username. May fail if network is unavailable or username doesn't exist.
Key format: Only Ed25519 SSH keys are parsed from response. Other key types (RSA, ECDSA) are skipped.
Memory management: Caller must free each key string and the keys array (same as fetch_github_ssh_keys).
Warning
Network dependency: Requires active network connection. May fail if GitLab is unreachable or username doesn't exist.

Definition at line 198 of file https_keys.c.

198 {
199 if (!username || !keys_out || !num_keys) {
200 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
201 num_keys);
202 return ERROR_INVALID_PARAM;
203 }
204
205 // Build the GitLab SSH keys URL
206 char url[BUFFER_SIZE_SMALL];
207 asciichat_error_t url_result = build_gitlab_ssh_url(username, url, sizeof(url));
208 if (url_result != ASCIICHAT_OK) {
209 return url_result;
210 }
211
212 // Fetch the keys using HTTPS
213 char *response_text = NULL;
214 size_t response_len = 0;
215 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
216 if (fetch_result != ASCIICHAT_OK) {
217 return fetch_result;
218 }
219
220 // Parse SSH keys from the response
221 asciichat_error_t parse_result =
222 parse_ssh_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
223
224 // Clean up response text
225 SAFE_FREE(response_text);
226
227 return parse_result;
228}
asciichat_error_t build_gitlab_ssh_url(const char *username, char *url_out, size_t url_size)
Construct GitLab SSH keys URL.
Definition https_keys.c:83

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, build_gitlab_ssh_url(), ERROR_INVALID_PARAM, MAX_CLIENTS, parse_ssh_keys_from_response(), SAFE_FREE, and SET_ERRNO.

Referenced by fetch_gitlab_keys(), and parse_public_keys().

◆ format_gpg_key_display()

asciichat_error_t format_gpg_key_display ( const char *  gpg_key_text,
char *  output,
size_t  output_size 
)

#include <gpg_keys.h>

Format GPG key for display.

Parameters
gpg_key_textGPG key in armored format (must not be NULL)
outputOutput buffer for formatted key (must not be NULL)
output_sizeSize of output buffer (must be >= 64)
Returns
ASCIICHAT_OK on success, error code on failure

Formats GPG key for display by extracting key ID and creating display string.

Note
Display format: "GPG key ID: <hex_key_id>" (e.g., "GPG key ID: EDDAE1DA7360D7F4").
Key ID extraction: Uses get_gpg_key_id() to extract key ID. Falls back to generic message if extraction fails.
Buffer requirements: Output buffer must be at least 64 bytes. Format string is "GPG key ID: " (12 chars) + 16 hex chars + null = 29 bytes minimum.
Warning
GPG support is currently disabled. Function depends on get_gpg_key_id() which is not yet implemented. Falls back to generic message if extraction fails.

Definition at line 278 of file gpg_keys.c.

278 {
279 if (!gpg_key_text || !output) {
280 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, output=%p", gpg_key_text, output);
281 return ERROR_INVALID_PARAM;
282 }
283
284 if (output_size < 64) {
285 SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer too small: %zu (minimum 64)", output_size);
286 return ERROR_INVALID_PARAM;
287 }
288
289 // Extract key ID for display
290 uint8_t key_id[8];
291 asciichat_error_t key_id_result = get_gpg_key_id(gpg_key_text, key_id);
292 if (key_id_result != ASCIICHAT_OK) {
293 // Fallback to generic GPG key display
294 SAFE_STRNCPY(output, "GPG key (key ID extraction failed)", output_size - 1);
295 return ASCIICHAT_OK;
296 }
297
298 // Format as hex key ID
299 char hex_key_id[17];
300 for (int i = 0; i < 8; i++) {
301 safe_snprintf(hex_key_id + i * 2, 3, "%02x", key_id[i]);
302 }
303 hex_key_id[16] = '\0';
304
305 // Create display string
306 int result = safe_snprintf(output, output_size, "GPG key ID: %s", hex_key_id);
307 if (result < 0 || result >= (int)output_size) {
308 SET_ERRNO(ERROR_STRING, "Failed to format GPG key display");
309 return ERROR_STRING;
310 }
311
312 return ASCIICHAT_OK;
313}
asciichat_error_t get_gpg_key_id(const char *gpg_key_text, uint8_t key_id_out[8])
Get GPG key ID (short fingerprint)
Definition gpg_keys.c:163

References ASCIICHAT_OK, ERROR_INVALID_PARAM, ERROR_STRING, get_gpg_key_id(), safe_snprintf(), SAFE_STRNCPY, and SET_ERRNO.

◆ format_public_key()

void format_public_key ( const public_key_t *  key,
char *  output,
size_t  output_size 
)

#include <keys.h>

Convert public key to display format (ssh-ed25519 or x25519 hex)

Parameters
keyPublic key to format (must not be NULL)
outputOutput buffer for formatted key string (must not be NULL)
output_sizeSize of output buffer (must be large enough for formatted key)

Formats public key for display:

  • Ed25519: "ssh-ed25519 AAAAC3..." (base64-encoded key)
  • X25519: "x25519 <hex>" (hex-encoded key)
Note
Output format: Depends on key type. Ed25519 uses base64, X25519 uses hex.
Buffer size: Output buffer should be at least 512 bytes for Ed25519 format. X25519 format requires at least 65 bytes (64 hex chars + "x25519 " prefix + null).
Warning
Output buffer must be large enough for formatted key string. Function does not check buffer size (may overflow).

Definition at line 588 of file keys.c.

588 {
589 if (!key || !output || output_size == 0) {
590 return;
591 }
592
593 if (key->type == KEY_TYPE_ED25519) {
594 // Format as SSH Ed25519 public key
595 // Simple base64 encoding for 32 bytes = 43 chars + padding
596 // For now, just use hex encoding
597 char hex_key[65];
598 for (size_t i = 0; i < 32; i++) {
599 char hex_byte[3];
600 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
601 hex_key[i * 2] = hex_byte[0];
602 hex_key[i * 2 + 1] = hex_byte[1];
603 }
604 hex_key[64] = '\0';
605 safe_snprintf(output, output_size, "ssh-ed25519 %s %s", hex_key, key->comment);
606 } else if (key->type == KEY_TYPE_X25519) {
607 // Format as hex X25519 key
608 char hex_key[65];
609 for (size_t i = 0; i < 32; i++) {
610 char hex_byte[3];
611 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
612 hex_key[i * 2] = hex_byte[0];
613 hex_key[i * 2 + 1] = hex_byte[1];
614 }
615 hex_key[64] = '\0';
616 safe_snprintf(output, output_size, "x25519 %s", hex_key);
617 } else {
618 safe_snprintf(output, output_size, "unknown key type: %d", key->type);
619 }
620}
char comment[256]
Definition key_types.h:72

References public_key_t::comment, public_key_t::key, KEY_TYPE_ED25519, KEY_TYPE_X25519, safe_snprintf(), and public_key_t::type.

◆ generate_key_fingerprint()

asciichat_error_t generate_key_fingerprint ( const public_key_t *  key,
uint8_t *  fingerprint_out,
size_t  fingerprint_size 
)

#include <keys_validation.h>

Generate key fingerprint.

Parameters
keyKey to fingerprint (must not be NULL)
fingerprint_outOutput buffer for fingerprint (must not be NULL)
fingerprint_sizeSize of fingerprint buffer (must be >= 32 for SHA-256)
Returns
ASCIICHAT_OK on success, error code on failure

Generates key fingerprint by computing hash of key material. Typically uses SHA-256 for fingerprint generation.

Note
Fingerprint algorithm: Uses SHA-256 hash of key material. Fingerprint is 32 bytes (256 bits) for SHA-256.
Key material: Hashes raw 32-byte key data (not key type or comment). Fingerprint uniquely identifies the key.
Buffer requirements: Fingerprint buffer must be at least 32 bytes for SHA-256. Function may use different hash algorithms in the future.
Warning
Fingerprint buffer must be large enough for hash output. Function validates buffer size but may overflow if buffer is too small.

Definition at line 445 of file keys_validation.c.

445 {
446 if (!key || !fingerprint_out) {
447 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, fingerprint_out=%p", key, fingerprint_out);
448 return ERROR_INVALID_PARAM;
449 }
450
451 if (fingerprint_size < 32) {
452 SET_ERRNO(ERROR_INVALID_PARAM, "Fingerprint buffer too small: %zu (minimum 32)", fingerprint_size);
453 return ERROR_INVALID_PARAM;
454 }
455
456 // Generate SHA-256 fingerprint of the key
457 if (crypto_hash_sha256(fingerprint_out, key->key, 32) != 0) {
458 SET_ERRNO(ERROR_CRYPTO, "Failed to generate key fingerprint");
459 return ERROR_CRYPTO;
460 }
461
462 return ASCIICHAT_OK;
463}

References ASCIICHAT_OK, ERROR_CRYPTO, ERROR_INVALID_PARAM, public_key_t::key, and SET_ERRNO.

Referenced by check_key_fingerprint().

◆ get_gpg_fingerprint()

asciichat_error_t get_gpg_fingerprint ( const char *  gpg_key_text,
uint8_t  fingerprint_out[20] 
)

#include <gpg_keys.h>

Get GPG key fingerprint.

Parameters
gpg_key_textGPG key in armored format (must not be NULL)
fingerprint_outOutput buffer for fingerprint (20 bytes for SHA-1, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Extracts GPG key fingerprint (SHA-1 hash of key material). Used for key identification and verification.

Note
Fingerprint format: SHA-1 hash of key material (20 bytes). Standard GPG fingerprint format.
OpenPGP parsing: Parses OpenPGP packet structure and computes SHA-1 hash. Requires parsing of packet structure.
Warning
NOT YET IMPLEMENTED: This function is not yet implemented. Returns ERROR_CRYPTO_KEY with "GPG fingerprint extraction not yet implemented".
GPG support is currently disabled. Function will not work until implemented.

Definition at line 149 of file gpg_keys.c.

149 {
150 if (!gpg_key_text || !fingerprint_out) {
151 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, fingerprint_out=%p", gpg_key_text,
152 fingerprint_out);
153 return ERROR_INVALID_PARAM;
154 }
155
156 // TODO: Implement GPG fingerprint extraction
157 // This requires parsing the OpenPGP packet structure and computing SHA-1
158
159 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG fingerprint extraction not yet implemented");
160 return ERROR_CRYPTO_KEY;
161}

References ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ get_gpg_key_id()

asciichat_error_t get_gpg_key_id ( const char *  gpg_key_text,
uint8_t  key_id_out[8] 
)

#include <gpg_keys.h>

Get GPG key ID (short fingerprint)

Parameters
gpg_key_textGPG key in armored format (must not be NULL)
key_id_outOutput buffer for key ID (8 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Extracts GPG key ID (last 8 bytes of SHA-1 fingerprint). Used for key identification in short form.

Note
Key ID format: Last 8 bytes of SHA-1 fingerprint (16 hex chars). Example: "EDDAE1DA7360D7F4"
Key ID extraction: Computes full fingerprint and extracts last 8 bytes. Alternatively, can parse from GPG key structure directly.
Warning
NOT YET IMPLEMENTED: This function is not yet implemented. Returns ERROR_CRYPTO_KEY with "GPG key ID extraction not yet implemented".
GPG support is currently disabled. Function will not work until implemented.

Definition at line 163 of file gpg_keys.c.

163 {
164 if (!gpg_key_text || !key_id_out) {
165 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, key_id_out=%p", gpg_key_text, key_id_out);
166 return ERROR_INVALID_PARAM;
167 }
168
169 // TODO: Implement GPG key ID extraction
170 // This is typically the last 8 bytes of the fingerprint
171
172 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG key ID extraction not yet implemented");
173 return ERROR_CRYPTO_KEY;
174}

References ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by format_gpg_key_display().

◆ gpg_to_x25519_public()

asciichat_error_t gpg_to_x25519_public ( const char *  gpg_key_text,
uint8_t  x25519_pk[32] 
)

#include <gpg_keys.h>

Convert GPG key to X25519 for key exchange.

Parameters
gpg_key_textGPG key in armored format (must not be NULL)
x25519_pkOutput buffer for X25519 public key (32 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Converts GPG key to X25519 format for Diffie-Hellman key exchange. Extracts Ed25519 key and converts to X25519.

Note
Conversion flow:
  1. Extract Ed25519 public key from GPG key
  2. Convert Ed25519 to X25519 using libsodium
Key conversion: Uses crypto_sign_ed25519_pk_to_curve25519() from libsodium. Conversion is mathematically safe (same curve, different representation).
Warning
GPG support is currently disabled. Function depends on extract_ed25519_from_gpg() which is not yet implemented. Function will not work until GPG support is re-enabled.

Definition at line 123 of file gpg_keys.c.

123 {
124 if (!gpg_key_text || !x25519_pk) {
125 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, x25519_pk=%p", gpg_key_text, x25519_pk);
126 return ERROR_INVALID_PARAM;
127 }
128
129 // First extract the Ed25519 public key from the GPG key
130 uint8_t ed25519_pk[32];
131 asciichat_error_t extract_result = extract_ed25519_from_gpg(gpg_key_text, ed25519_pk);
132 if (extract_result != ASCIICHAT_OK) {
133 return extract_result;
134 }
135
136 // Convert Ed25519 to X25519
137 if (crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk) != 0) {
138 SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 to X25519");
139 return ERROR_CRYPTO_KEY;
140 }
141
142 return ASCIICHAT_OK;
143}
asciichat_error_t extract_ed25519_from_gpg(const char *gpg_key_id, uint8_t ed25519_pk[32])
Extract Ed25519 public key from GPG key.
Definition gpg_keys.c:107

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, extract_ed25519_from_gpg(), and SET_ERRNO.

◆ hex_decode()

asciichat_error_t hex_decode ( const char *  hex,
uint8_t *  output,
size_t  output_len 
)

#include <keys.h>

Decode hex string to binary (utility function for testing)

Parameters
hexHex string to decode (must not be NULL, must have even length)
outputOutput buffer for binary data (must not be NULL)
output_lenExpected output length in bytes (hex string must be output_len * 2 chars)
Returns
ASCIICHAT_OK on success, error code on failure

Decodes hex string to binary data. Utility function primarily for testing.

Note
Hex format: Must contain only valid hex characters (0-9, a-f, A-F). Must have even length (2 chars per byte).
Length validation: Hex string must be exactly output_len * 2 characters. Returns error if length doesn't match.
Invalid characters: Returns error if hex string contains invalid characters.
Warning
Hex string must have even length. Odd-length strings return error.

Definition at line 626 of file keys.c.

626 {
627 if (!hex || !output) {
628 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for hex_decode");
629 }
630
631 size_t hex_len = strlen(hex);
632 size_t expected_hex_len = output_len * 2;
633
634 if (hex_len != expected_hex_len) {
636 "Hex string length (%zu) doesn't match expected output length (%zu * 2 = %zu)", hex_len,
637 output_len, expected_hex_len);
638 }
639
640 // Decode hex string to binary
641 for (size_t i = 0; i < output_len; i++) {
642 char hex_byte[3] = {hex[i * 2], hex[i * 2 + 1], '\0'};
643 char *endptr;
644 unsigned long byte = strtoul(hex_byte, &endptr, 16);
645
646 // Validate hex character
647 if (*endptr != '\0' || byte > 255) {
648 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid hex character at position %zu: '%c%c'", i * 2, hex[i * 2],
649 hex[i * 2 + 1]);
650 }
651
652 output[i] = (uint8_t)byte;
653 }
654
655 return ASCIICHAT_OK;
656}

References ASCIICHAT_OK, ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by parse_public_key().

◆ parse_gpg_key()

asciichat_error_t parse_gpg_key ( const char *  gpg_key_text,
public_key_t *  key_out 
)

#include <gpg_keys.h>

Parse GPG key from armored text format.

Parse Ed25519 public key from PGP armored format.

Parameters
gpg_key_textGPG key in armored format (--—BEGIN PGP PUBLIC KEY BLOCK--—, must not be NULL)
key_outOutput structure for parsed public key (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses GPG key from armored format and extracts Ed25519 public key. Converts to X25519 for key exchange.

Note
GPG format: Expects PEM-armored GPG key format. Format: "-----BEGIN PGP PUBLIC KEY BLOCK-----" ... "-----END PGP PUBLIC KEY BLOCK-----"
Ed25519 extraction: Extracts Ed25519 public key from GPG key structure. Parses OpenPGP packet structure to find Ed25519 subkey.
Key conversion: Converts extracted Ed25519 key to X25519 for key exchange. Uses libsodium's conversion function.
Key validation: Validates GPG key format before parsing. Returns error if format is invalid.
Warning
GPG support is currently disabled. Many underlying functions are not implemented. This function may not work until GPG support is re-enabled.
Key format: Only Ed25519 GPG keys are supported. RSA/ECDSA GPG keys will return error.
Parameters
gpg_key_textGPG key in armored format (must not be NULL)
key_outOutput structure for parsed public key (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Extracts Ed25519 public key from PGP armored format. Parses GPG key structure and converts to Ed25519/X25519 format.

Note
Key format: GPG keys are parsed from armored format (--—BEGIN PGP PUBLIC KEY BLOCK--—). Extracts Ed25519 subkey and converts to X25519 for key exchange.

Definition at line 25 of file gpg_keys.c.

25 {
26 if (!gpg_key_id || !key_out) {
27 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_id=%p, key_out=%p", gpg_key_id, key_out);
29 }
30
31 // Validate key ID format (must be 8, 16, or 40 hex characters, optionally with 0x prefix)
32 // - 8 chars: short key ID (last 8 chars of fingerprint)
33 // - 16 chars: long key ID (last 16 chars of fingerprint)
34 // - 40 chars: full fingerprint
35 const char *key_id = gpg_key_id;
36 if (strncmp(key_id, "0x", 2) == 0 || strncmp(key_id, "0X", 2) == 0) {
37 key_id += 2; // Skip 0x prefix
38 }
39
40 size_t key_id_len = strlen(key_id);
41 if (key_id_len != 8 && key_id_len != 16 && key_id_len != 40) {
42 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID length: %zu (expected 8, 16, or 40 hex chars)", key_id_len);
43 }
44
45 // Validate hex characters
46 for (size_t i = 0; i < key_id_len; i++) {
47 char c = key_id[i];
48 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
49 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID: contains non-hex character '%c'", c);
50 }
51 }
52
53 // Extract Ed25519 public key from GPG keyring using gpg --list-keys
54 uint8_t ed25519_pk[32];
55 asciichat_error_t extract_result = extract_ed25519_from_gpg(key_id, ed25519_pk);
56 if (extract_result != ASCIICHAT_OK) {
57 return extract_result;
58 }
59
60 // Initialize the public key structure
61 memset(key_out, 0, sizeof(public_key_t));
62 key_out->type = KEY_TYPE_GPG;
63 memcpy(key_out->key, ed25519_pk, 32);
64
65 // Set comment for display
66 safe_snprintf(key_out->comment, sizeof(key_out->comment), "GPG key %s", key_id);
67
68 return ASCIICHAT_OK;
69}

References ASCIICHAT_OK, public_key_t::comment, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, extract_ed25519_from_gpg(), public_key_t::key, KEY_TYPE_GPG, safe_snprintf(), SET_ERRNO, and public_key_t::type.

Referenced by parse_public_key().

◆ parse_gpg_key_binary()

asciichat_error_t parse_gpg_key_binary ( const uint8_t *  gpg_key_binary,
size_t  key_size,
public_key_t *  key_out 
)

#include <gpg_keys.h>

Parse GPG key from binary format.

Parameters
gpg_key_binaryGPG key in binary format (must not be NULL)
key_sizeSize of binary key data (must be > 0)
key_outOutput structure for parsed public key (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses GPG key from binary format (raw OpenPGP packets). Extracts Ed25519 public key and converts to X25519.

Note
Binary format: Accepts raw OpenPGP packet structure. Must parse packet structure to find Ed25519 public key packet.
OpenPGP packets: Parses OpenPGP packet format to extract public key. Requires understanding of OpenPGP packet structure.
Warning
NOT YET IMPLEMENTED: This function is not yet implemented. Returns ERROR_CRYPTO_KEY with "Binary GPG key parsing not yet implemented".
GPG support is currently disabled. Function will not work until implemented.

Definition at line 71 of file gpg_keys.c.

71 {
72 if (!gpg_key_binary || !key_out) {
73 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_binary=%p, key_out=%p", gpg_key_binary, key_out);
75 }
76
77 if (key_size == 0) {
78 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid key size: %zu", key_size);
80 }
81
82 // Treat as PGP armored text - null-terminate for string operations
83 char *armored_text = SAFE_MALLOC(key_size + 1, char *);
84 memcpy(armored_text, gpg_key_binary, key_size);
85 armored_text[key_size] = '\0';
86
87 // Parse OpenPGP armored format and extract Ed25519 public key
88 uint8_t ed25519_pk[32];
89 asciichat_error_t result = openpgp_parse_armored_pubkey(armored_text, ed25519_pk);
90 SAFE_FREE(armored_text);
91
92 if (result != ASCIICHAT_OK) {
93 return result;
94 }
95
96 // Initialize the public key structure
97 memset(key_out, 0, sizeof(public_key_t));
98 key_out->type = KEY_TYPE_GPG;
99 memcpy(key_out->key, ed25519_pk, 32);
100
101 // Set comment for display
102 safe_snprintf(key_out->comment, sizeof(key_out->comment), "GPG Ed25519 key");
103
104 return ASCIICHAT_OK;
105}
#define SAFE_MALLOC(size, cast)
Definition common.h:264
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.
Definition openpgp.c:290

References ASCIICHAT_OK, public_key_t::comment, ERROR_INVALID_PARAM, public_key_t::key, KEY_TYPE_GPG, openpgp_parse_armored_pubkey(), SAFE_FREE, SAFE_MALLOC, safe_snprintf(), SET_ERRNO, and public_key_t::type.

Referenced by parse_public_key().

◆ parse_gpg_keys_from_response()

asciichat_error_t parse_gpg_keys_from_response ( const char *  response_text,
size_t  response_len,
char ***  keys_out,
size_t *  num_keys,
size_t  max_keys 
)

#include <https_keys.h>

Parse GPG keys from HTTPS response text.

Parameters
response_textHTTPS response containing GPG keys (must not be NULL)
response_lenLength of response text
keys_outOutput array of parsed GPG keys (must not be NULL)
num_keysOutput parameter for number of keys parsed (must not be NULL)
max_keysMaximum number of keys to parse (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Parses GPG keys from HTTPS response text (armored format). Extracts GPG key blocks and allocates memory for each key string.

Note
Response format: Expects GPG keys in armored format. Format: "-----BEGIN PGP PUBLIC KEY BLOCK-----" ... "-----END PGP PUBLIC KEY BLOCK-----"
Key extraction: Finds GPG key blocks (BEGIN/END markers) and extracts each block. Allocates memory for each complete GPG key block.
Memory allocation: Allocates memory for each key string. Caller must free each string and the array.
Maximum keys: Stops parsing after max_keys keys are found. Function returns ASCIICHAT_OK even if more keys exist in response.
Warning
Memory leak: Caller must free returned strings and array. Function allocates memory that must be freed.
GPG support: GPG key parsing may not work until GPG support is re-enabled.

Definition at line 396 of file https_keys.c.

397 {
398 (void)max_keys;
399 if (!response_text || !keys_out || !num_keys) {
400 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for GPG key parsing");
401 return ERROR_INVALID_PARAM;
402 }
403
404 *num_keys = 0;
405 *keys_out = NULL;
406
407 // Check if this looks like a GPG key (starts with -----BEGIN PGP)
408 if (strncmp(response_text, "-----BEGIN PGP", 14) != 0) {
409 SET_ERRNO(ERROR_CRYPTO_KEY, "Response does not contain a valid GPG key");
410 return ERROR_CRYPTO_KEY;
411 }
412
413 // Write armored block to temp file
414 char temp_file[PLATFORM_MAX_PATH_LENGTH];
415 int fd = -1;
416 if (platform_create_temp_file(temp_file, sizeof(temp_file), "asc_gpg_import", &fd) != 0) {
417 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to create temp file for GPG import");
418 }
419
420#ifdef _WIN32
421 // On Windows, platform_create_temp_file returns fd=-1, need to open separately
422 fd = platform_open("https_key_temp", temp_file, O_WRONLY | O_BINARY);
423 if (fd < 0) {
424 platform_delete_temp_file(temp_file);
425 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open temp file for writing");
426 }
427#endif
428
429 ssize_t written = write(fd, response_text, response_len);
430 close(fd);
431
432 if (written != (ssize_t)response_len) {
433 platform_unlink(temp_file);
434 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to write GPG key to temp file");
435 }
436
437 // Import the key using gpg --import
438 char import_cmd[BUFFER_SIZE_MEDIUM];
439 safe_snprintf(import_cmd, sizeof(import_cmd), "gpg --import '%s' 2>&1", temp_file);
440 FILE *import_fp = NULL;
441 if (platform_popen("gpg_https_import", import_cmd, "r", &import_fp) != ASCIICHAT_OK || !import_fp) {
442 platform_unlink(temp_file);
443 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to run gpg --import");
444 }
445
446 char import_output[2048];
447 size_t import_len = fread(import_output, 1, sizeof(import_output) - 1, import_fp);
448 import_output[import_len] = '\0';
449 platform_pclose(&import_fp);
450 platform_unlink(temp_file);
451
452 // Extract ALL key IDs from import output (format: "gpg: key KEYID: ...")
453 // GitHub often returns multiple keys in one armored block
454 const char *key_marker = "gpg: key ";
455 char key_ids[16][17]; // Support up to 16 keys
456 size_t key_count = 0;
457
458 log_debug("GPG import output:\n%s", import_output);
459
460 char *search_pos = import_output;
461 while (key_count < 16) {
462 char *key_line = strstr(search_pos, key_marker);
463 if (!key_line)
464 break;
465
466 key_line += strlen(key_marker);
467 int i = 0;
468 while (i < 16 && key_line[i] != ':' && key_line[i] != ' ' && key_line[i] != '\n') {
469 key_ids[key_count][i] = key_line[i];
470 i++;
471 }
472 key_ids[key_count][i] = '\0';
473
474 if (i > 0) {
475 log_debug("Extracted GPG key ID #%zu: %s", key_count, key_ids[key_count]);
476 key_count++;
477 }
478 search_pos = key_line + i;
479 }
480
481 if (key_count == 0) {
482 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to extract any key IDs from GPG import output");
483 }
484
485 log_debug("Total GPG keys extracted from import: %zu", key_count);
486
487 // Allocate array for results
488 *keys_out = SAFE_MALLOC(sizeof(char *) * key_count, char **);
489 if (!*keys_out) {
490 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate GPG keys array");
491 }
492
493 // Process each key ID to get full fingerprint
494 size_t valid_keys = 0;
495 for (size_t k = 0; k < key_count; k++) {
496 // Get full fingerprint from gpg --list-keys output
497 char list_cmd[BUFFER_SIZE_SMALL];
498 safe_snprintf(list_cmd, sizeof(list_cmd),
499 "gpg --list-keys --with-colons --fingerprint '%s' " PLATFORM_SHELL_NULL_REDIRECT, key_ids[k]);
500 FILE *list_fp = NULL;
501 if (platform_popen("gpg_https_list", list_cmd, "r", &list_fp) != ASCIICHAT_OK || !list_fp) {
502 continue; // Skip this key if we can't list it
503 }
504
505 char list_output[4096];
506 size_t list_len = fread(list_output, 1, sizeof(list_output) - 1, list_fp);
507 list_output[list_len] = '\0';
508 platform_pclose(&list_fp);
509
510 // Check if it contains an Ed25519 key (algorithm 22)
511 if (!strstr(list_output, ":22:") && !strstr(list_output, "ed25519")) {
512 continue; // Skip non-Ed25519 keys
513 }
514
515 // Extract full 40-character fingerprint from "fpr:" line
516 // Format: fpr:::::::::FINGERPRINT:
517 // The fingerprint is field 10 (after 9 colons)
518 char fingerprint[41] = {0};
519 const char *fpr_marker = "\nfpr:";
520 char *fpr_line = strstr(list_output, fpr_marker);
521 if (fpr_line) {
522 fpr_line += 1; // Skip the newline
523 // Count 9 colons from the start of the line
524 int colon_count = 0;
525 while (*fpr_line && colon_count < 9) {
526 if (*fpr_line == ':')
527 colon_count++;
528 fpr_line++;
529 }
530 // Now we should be at the start of the fingerprint
531 // Extract up to 40 hex characters
532 int fpr_len = 0;
533 while (fpr_len < 40 && fpr_line[fpr_len] && fpr_line[fpr_len] != ':' && fpr_line[fpr_len] != '\n') {
534 fingerprint[fpr_len] = fpr_line[fpr_len];
535 fpr_len++;
536 }
537 fingerprint[fpr_len] = '\0';
538 }
539
540 // If fingerprint extraction failed, use the short key ID
541 if (strlen(fingerprint) == 0) {
542 log_warn("Failed to extract fingerprint for key %s, using short key ID", key_ids[k]);
543 SAFE_STRNCPY(fingerprint, key_ids[k], sizeof(fingerprint));
544 }
545
546 log_debug("Key %s -> fingerprint: %s (length: %zu)", key_ids[k], fingerprint, strlen(fingerprint));
547
548 // Allocate and store this key in gpg:KEYID format
549 size_t gpg_key_len = strlen("gpg:") + strlen(fingerprint) + 1;
550 (*keys_out)[valid_keys] = SAFE_MALLOC(gpg_key_len, char *);
551 if (!(*keys_out)[valid_keys]) {
552 // Cleanup on allocation failure
553 for (size_t cleanup = 0; cleanup < valid_keys; cleanup++) {
554 SAFE_FREE((*keys_out)[cleanup]);
555 }
556 SAFE_FREE(*keys_out);
557 *keys_out = NULL;
558 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate GPG key string");
559 }
560
561 safe_snprintf((*keys_out)[valid_keys], gpg_key_len, "gpg:%s", fingerprint);
562 log_debug("Added valid Ed25519 key #%zu: %s", valid_keys, (*keys_out)[valid_keys]);
563 valid_keys++;
564 }
565
566 if (valid_keys == 0) {
567 SAFE_FREE(*keys_out);
568 *keys_out = NULL;
569 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid Ed25519 keys found in imported GPG keys");
570 }
571
572 *num_keys = valid_keys;
573 return ASCIICHAT_OK;
574}
@ ERROR_MEMORY
Definition error_codes.h:56
int platform_create_temp_file(char *path_out, size_t path_size, const char *prefix, int *fd)
Create a temporary file with a given prefix.
int platform_unlink(const char *pathname)
Delete/unlink file.
int platform_open(const char *name, const char *pathname, int flags,...)
Safe file open (open replacement)
int platform_delete_temp_file(const char *path)
Delete a temporary file.
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69

References ASCIICHAT_OK, BUFFER_SIZE_MEDIUM, BUFFER_SIZE_SMALL, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, ERROR_MEMORY, log_debug, log_warn, platform_create_temp_file(), platform_delete_temp_file(), PLATFORM_MAX_PATH_LENGTH, platform_open(), platform_pclose(), platform_popen(), PLATFORM_SHELL_NULL_REDIRECT, platform_unlink(), SAFE_FREE, SAFE_MALLOC, safe_snprintf(), SAFE_STRNCPY, and SET_ERRNO.

Referenced by fetch_github_gpg_keys(), and fetch_gitlab_gpg_keys().

◆ parse_keys_from_file()

asciichat_error_t parse_keys_from_file ( const char *  path,
public_key_t *  keys,
size_t *  num_keys,
size_t  max_keys 
)

#include <keys.h>

Parse SSH keys from file (supports authorized_keys and known_hosts formats)

Parameters
pathPath to key file (must not be NULL)
keysOutput array for parsed public keys (must not be NULL)
num_keysOutput parameter for number of keys parsed (must not be NULL)
max_keysMaximum number of keys to parse (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Parses multiple SSH keys from file. Supports:

  • authorized_keys format: One key per line (ssh-ed25519 AAAAC3... comment)
  • **.pub file with multiple entries**: File containing multiple SSH public key entries, one per line (each line in ssh-ed25519 AAAAC3... comment format). This format is similar to authorized_keys and can contain any number of keys.
  • known_hosts format: Multiple keys per line (hostname ssh-ed25519 AAAAC3... comment)

File format examples:

# authorized_keys format or .pub file with multiple entries
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@example.com
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... bob@example.com
Note
Key filtering: Only Ed25519 and X25519 keys are parsed. RSA/ECDSA keys are skipped (not supported).
File format: Automatically detects file format based on content. Supports both authorized_keys format (one key per line) and known_hosts format. Also supports .pub files containing multiple key entries (one per line).
Maximum keys: Stops parsing after max_keys keys are found. Function returns ASCIICHAT_OK even if more keys exist in file.
Warning
File must exist and be readable. Returns error if file cannot be opened.

Definition at line 544 of file keys.c.

544 {
545 if (!path || !keys || !num_keys) {
546 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for key file parsing");
547 }
548
549 *num_keys = 0;
550
551 if (!path_looks_like_path(path)) {
552 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid keys file path: %s", path);
553 }
554
555 char *normalized_path = NULL;
556 asciichat_error_t path_result = path_validate_user_path(path, PATH_ROLE_CLIENT_KEYS, &normalized_path);
557 if (path_result != ASCIICHAT_OK) {
558 SAFE_FREE(normalized_path);
559 return path_result;
560 }
561
562 FILE *f = platform_fopen("file_stream", normalized_path, "r");
563 if (!f) {
564 SAFE_FREE(normalized_path);
565 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open keys file: %s", path);
566 }
567
568 char line[BUFFER_SIZE_LARGE];
569 while (fgets(line, sizeof(line), f) && *num_keys < max_keys) {
570 // Remove newline
571 line[strcspn(line, "\r\n")] = 0;
572
573 // Skip empty lines and comments
574 if (strlen(line) == 0 || line[0] == '#') {
575 continue;
576 }
577
578 if (parse_public_key(line, &keys[*num_keys]) == ASCIICHAT_OK) {
579 (*num_keys)++;
580 }
581 }
582
583 (void)fclose(f);
584 SAFE_FREE(normalized_path);
585 return ASCIICHAT_OK;
586}
FILE * platform_fopen(const char *name, const char *filename, const char *mode)
Safe file open stream (fopen replacement)
bool path_looks_like_path(const char *value)
Determine if a string is likely intended to reference the filesystem.
Definition path.c:809
asciichat_error_t path_validate_user_path(const char *input, path_role_t role, char **normalized_out)
Validate and canonicalize a user-supplied filesystem path.
Definition path.c:1020
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:321

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, parse_public_key(), path_looks_like_path(), PATH_ROLE_CLIENT_KEYS, path_validate_user_path(), platform_fopen(), SAFE_FREE, and SET_ERRNO.

Referenced by parse_public_keys().

◆ parse_private_key()

asciichat_error_t parse_private_key ( const char *  path,
private_key_t *  key_out 
)

#include <keys.h>

Parse SSH private key from file.

Parameters
pathPath to private key file
key_outOutput structure for parsed private key (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses private key from file. Supports:

  • OpenSSH Ed25519: ~/.ssh/id_ed25519 (OpenSSH Ed25519 format)
  • Raw hex file: File containing 64 hex chars for X25519 key
Note
Encrypted keys: If key is encrypted (has password), prompts for password and decrypts it using platform-specific password prompt.
SSH agent detection: If use_ssh_agent is true, key is loaded from SSH agent instead of file. Key stays in agent (not loaded into memory).
File permissions: Validates file permissions before parsing. Warns if file has overly permissive permissions (world-readable).
Key format: Currently only supports Ed25519 keys (OpenSSH format). RSA/ECDSA keys are NOT supported.
Warning
File permissions: Private key files should have restrictive permissions (0600). Function warns but does not fail on overly permissive permissions.

Definition at line 193 of file keys.c.

193 {
194 if (!key_path || !key_out) {
195 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private key parsing");
196 }
197
198 // Clear output structure
199 memset(key_out, 0, sizeof(private_key_t));
200
201 // Check for GPG key format: "gpg:KEYID" where KEYID is 8, 16, or 40 hex chars
202 // - 8 chars: short key ID (last 8 chars of fingerprint)
203 // - 16 chars: long key ID (last 16 chars of fingerprint)
204 // - 40 chars: full fingerprint
205 if (strncmp(key_path, "gpg:", 4) == 0) {
206 const char *key_id = key_path + 4; // Skip "gpg:" prefix
207
208 // Validate key ID format (must be 8, 16, or 40 hex characters)
209 size_t key_id_len = strlen(key_id);
210 if (key_id_len != 8 && key_id_len != 16 && key_id_len != 40) {
211 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID length: %zu (expected 8, 16, or 40 hex chars)",
212 key_id_len);
213 }
214
215 // Validate hex characters
216 for (size_t i = 0; i < key_id_len; i++) {
217 char c = key_id[i];
218 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
219 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID: contains non-hex character '%c'", c);
220 }
221 }
222
223 // Get public key and keygrip from GPG keyring
224 uint8_t public_key[32];
225 char keygrip[64];
226 if (gpg_get_public_key(key_id, public_key, keygrip) != 0) {
227 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to get public key from GPG for key ID: %s", key_id);
228 }
229
230 // Populate private_key_t structure for GPG agent use
231 key_out->type = KEY_TYPE_ED25519;
232 key_out->use_gpg_agent = true;
233 key_out->use_ssh_agent = false;
234
235 // Store public key (for handshake and verification)
236 memcpy(key_out->public_key, public_key, 32);
237
238 // Store keygrip (for GPG agent signing operations)
239 platform_strncpy(key_out->gpg_keygrip, sizeof(key_out->gpg_keygrip), keygrip, sizeof(key_out->gpg_keygrip) - 1);
240
241 // Store comment for display
242 safe_snprintf(key_out->key_comment, sizeof(key_out->key_comment), "GPG key %s", key_id);
243
244 // Clear the key.ed25519 field (we don't have the private key in memory)
245 // The private key stays protected in GPG agent
246 memset(key_out->key.ed25519, 0, 64);
247
248 // Set the public key in the second half of ed25519 key (standard Ed25519 format)
249 // This is for compatibility with code that expects the public key at offset 32
250 memcpy(key_out->key.ed25519 + 32, public_key, 32);
251
252 log_debug("Loaded GPG key %s (keygrip: %.40s) for agent signing", key_id, keygrip);
253 return ASCIICHAT_OK;
254 }
255
256 // Try to load file and detect format
257 char *normalized_path = NULL;
258 asciichat_error_t path_result = path_validate_user_path(key_path, PATH_ROLE_KEY_PRIVATE, &normalized_path);
259 if (path_result != ASCIICHAT_OK) {
260 SAFE_FREE(normalized_path);
261 return path_result;
262 }
263
264 // Read file content to detect format
265 FILE *f = platform_fopen("file_stream", normalized_path, "r");
266 if (!f) {
267 SAFE_FREE(normalized_path);
268 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open private key file: %s", key_path);
269 }
270
271 // Read first 50 bytes to detect format
272 char header[50];
273 size_t bytes_read = fread(header, 1, sizeof(header) - 1, f);
274 header[bytes_read] = '\0';
275 fclose(f);
276
277 log_debug("parse_private_key: Read %zu bytes from %s, header: %.50s", bytes_read, key_path, header);
278
279 // Check for PGP armored secret key format
280 if (strstr(header, "-----BEGIN PGP PRIVATE KEY") != NULL || strstr(header, "-----BEGIN PGP SECRET KEY") != NULL) {
281 log_debug("Detected PGP armored secret key format in %s", key_path);
282 // Load entire file for OpenPGP parsing
283 f = platform_fopen("file_stream", normalized_path, "r");
284 if (!f) {
285 SAFE_FREE(normalized_path);
286 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open GPG key file: %s", key_path);
287 }
288
289 // Get file size
290 fseek(f, 0, SEEK_END);
291 long file_size = ftell(f);
292 fseek(f, 0, SEEK_SET);
293
294 if (file_size <= 0 || file_size > 1024 * 1024) { // Sanity check: max 1MB
295 fclose(f);
296 SAFE_FREE(normalized_path);
297 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key file size: %ld bytes", file_size);
298 }
299
300 char *file_content = SAFE_MALLOC((size_t)file_size + 1, char *);
301 size_t content_read = fread(file_content, 1, (size_t)file_size, f);
302 file_content[content_read] = '\0';
303 fclose(f);
304
305 // Parse OpenPGP armored secret key
306 uint8_t ed25519_pk[32];
307 uint8_t ed25519_sk[32];
308 asciichat_error_t openpgp_result = openpgp_parse_armored_seckey(file_content, ed25519_pk, ed25519_sk);
309 SAFE_FREE(file_content);
310 SAFE_FREE(normalized_path);
311
312 if (openpgp_result != ASCIICHAT_OK) {
313 return openpgp_result;
314 }
315
316 // Populate private_key_t structure
317 key_out->type = KEY_TYPE_ED25519;
318 key_out->use_gpg_agent = false;
319 key_out->use_ssh_agent = false;
320
321 // Store keypair in Ed25519 format (64 bytes: 32-byte seed + 32-byte public key)
322 // libsodium expects the seed in first 32 bytes and public key in second 32 bytes
323 memcpy(key_out->key.ed25519, ed25519_sk, 32); // Secret key (seed)
324 memcpy(key_out->key.ed25519 + 32, ed25519_pk, 32); // Public key
325 memcpy(key_out->public_key, ed25519_pk, 32); // Also store in public_key field
326
327 // Store comment
328 safe_snprintf(key_out->key_comment, sizeof(key_out->key_comment), "GPG Ed25519 key from %s", key_path);
329
330 log_debug("Loaded unencrypted GPG Ed25519 key from %s", key_path);
331 return ASCIICHAT_OK;
332 }
333
334 // Fall back to SSH private key parsing
335 asciichat_error_t result = parse_ssh_private_key(normalized_path, key_out);
336 SAFE_FREE(normalized_path);
337 return result;
338}
asciichat_error_t openpgp_parse_armored_seckey(const char *armored_text, uint8_t ed25519_pk[32], uint8_t ed25519_sk[32])
Parse PGP armored secret key block and extract Ed25519 keypair.
Definition openpgp.c:729
asciichat_error_t parse_ssh_private_key(const char *key_path, private_key_t *key_out)
Parse SSH Ed25519 private key from file.
Definition ssh_keys.c:206
int platform_strncpy(char *dst, size_t dst_size, const char *src, size_t count)
Safe string copy with explicit size bounds (strncpy replacement)
@ PATH_ROLE_KEY_PRIVATE
Definition path.h:319

References ASCIICHAT_OK, private_key_t::ed25519, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, gpg_get_public_key(), private_key_t::gpg_keygrip, private_key_t::key, private_key_t::key_comment, KEY_TYPE_ED25519, log_debug, openpgp_parse_armored_seckey(), parse_ssh_private_key(), PATH_ROLE_KEY_PRIVATE, path_validate_user_path(), platform_fopen(), platform_strncpy(), private_key_t::public_key, SAFE_FREE, SAFE_MALLOC, safe_snprintf(), SET_ERRNO, private_key_t::type, private_key_t::use_gpg_agent, and private_key_t::use_ssh_agent.

Referenced by client_crypto_init().

◆ parse_public_key()

asciichat_error_t parse_public_key ( const char *  input,
public_key_t *  key_out 
)

#include <keys.h>

Parse SSH/GPG public key from any format (returns first key only)

Parameters
inputKey input in various formats (see below)
key_outOutput structure for parsed public key (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses public key from various input formats:

Supported formats:

  • Raw base64 SSH key: "AAAAC3NzaC1lZDI1NTE5AAAAI..." (just the base64 part from .pub files, no prefix)
  • SSH Ed25519: "ssh-ed25519 AAAAC3... comment" (full SSH public key format)
  • HTTPS URL: "https://example.com/key.pub" or "https://example.com/key.gpg" (fetches from URL, parses response)
  • GitHub SSH: "github:username" (fetches from https://github.com/username.keys, returns first Ed25519 key)
  • GitLab SSH: "gitlab:username" (fetches from https://gitlab.com/username.keys, returns first Ed25519 key)
  • GitHub GPG: "github:username.gpg" (fetches GPG key from https://github.com/username.gpg)
  • GitLab GPG: "gitlab:username.gpg" (fetches GPG key from https://gitlab.com/username.gpg)
  • GPG keyring: "gpg:0xKEYID" (shells out to gpg --export KEYID)
  • File path: Path to .pub file or any file containing key (reads first line)
  • Raw hex: 64 hex chars for X25519 key

File support: When a file path is provided, the file is read and the first line is parsed as an SSH public key. Common formats:

  • .pub file: Standard SSH public key file (one key per file, reads first line)
  • Any text file: First line containing SSH key format is used
Note
Key normalization: All keys are converted to X25519 format for key exchange. Ed25519 keys are converted using libsodium conversion function.
GPG support: GPG Ed25519 keys are fully supported. Accepts 8, 16, or 40 character key IDs (short/long/full fingerprint). Requires gpg binary in PATH and gpg-agent running.
HTTPS URLs: Can be any HTTPS URL (e.g., https://discovery.ascii-chat.com/key.pub). Response content is parsed as key format. Uses BearSSL with system CA certificates. Requires network connectivity. Only first key is returned; for multiple keys, store them one per line in the response and use parse_public_keys() instead.
GitHub/GitLab fetching: Uses BearSSL for HTTPS requests. Requires network connectivity. Only first Ed25519 key is returned. For multiple keys, use parse_public_keys() instead.
File path: Reads first line of file and parses as SSH key format. File must exist and be readable. For files with multiple keys (one per line), use parse_keys_from_file() instead.
Warning
Network operations: GitHub/GitLab fetching requires network connectivity. May fail if network is unavailable or endpoints are blocked.

Definition at line 28 of file keys.c.

28 {
29 if (!input || !key_out) {
30 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public key parsing");
31 }
32
33 // Clear output structure
34 memset(key_out, 0, sizeof(public_key_t));
35
36 // Try SSH key parsing first (full format with prefix)
37 if (strncmp(input, "ssh-ed25519", 11) == 0) {
38 key_out->type = KEY_TYPE_ED25519;
39 asciichat_error_t result = parse_ssh_ed25519_line(input, key_out->key);
40 if (result == ASCIICHAT_OK) {
41 platform_strncpy(key_out->comment, sizeof(key_out->comment), "ssh-ed25519", sizeof(key_out->comment) - 1);
42 }
43 return result;
44 }
45
46 // Try raw base64 SSH public key (without "ssh-ed25519" prefix)
47 // SSH Ed25519 public keys in base64 are typically ~68 characters
48 size_t input_len = strlen(input);
49 if (input_len >= 50 && input_len <= 80) {
50 // Check if it looks like base64 (only contains valid base64 chars)
51 bool looks_like_base64 = true;
52 for (size_t i = 0; i < input_len; i++) {
53 char c = input[i];
54 if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '+' || c == '/' ||
55 c == '=')) {
56 looks_like_base64 = false;
57 break;
58 }
59 }
60
61 if (looks_like_base64) {
62 // Try to decode as SSH public key blob
63 uint8_t *blob = SAFE_MALLOC(input_len, uint8_t *);
64 size_t blob_len = 0;
65
66 const char *end = NULL;
67 int decode_result = sodium_base642bin(blob, input_len, input, input_len,
68 NULL, // ignore chars
69 &blob_len, &end, sodium_base64_VARIANT_ORIGINAL);
70
71 if (decode_result == 0 && blob_len >= 47) { // Minimum size: 4+11+4+32 = 51 bytes
72 // Validate SSH key blob structure
73 // Format: [4 bytes: length][11 bytes: "ssh-ed25519"][4 bytes: length][32 bytes: key]
74 if (blob_len >= 4) {
75 uint32_t key_type_len =
76 ((uint32_t)blob[0] << 24) | ((uint32_t)blob[1] << 16) | ((uint32_t)blob[2] << 8) | blob[3];
77 if (key_type_len == 11 && blob_len >= 15 && memcmp(blob + 4, "ssh-ed25519", 11) == 0) {
78 // Valid ssh-ed25519 blob, extract the 32-byte public key
79 if (blob_len >= 19) {
80 uint32_t pubkey_len =
81 ((uint32_t)blob[15] << 24) | ((uint32_t)blob[16] << 16) | ((uint32_t)blob[17] << 8) | blob[18];
82 if (pubkey_len == 32 && blob_len >= 51) {
83 // Extract Ed25519 public key
84 key_out->type = KEY_TYPE_ED25519;
85 memcpy(key_out->key, blob + 19, 32);
86 platform_strncpy(key_out->comment, sizeof(key_out->comment), "raw-base64",
87 sizeof(key_out->comment) - 1);
88 SAFE_FREE(blob);
89 return ASCIICHAT_OK;
90 }
91 }
92 }
93 }
94 }
95 SAFE_FREE(blob);
96 }
97 }
98
99 // Try GPG key parsing
100 if (strncmp(input, "gpg:", 4) == 0) {
101 return parse_gpg_key(input + 4, key_out);
102 }
103
104 // Try HTTPS URLs (validated via production-grade URL regex)
105 if (url_is_valid(input)) {
106 // Parse HTTPS URL to extract hostname and path
107 url_parts_t url_parts = {0};
108 asciichat_error_t parse_result = url_parse(input, &url_parts);
109 if (parse_result != ASCIICHAT_OK) {
110 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to parse HTTPS URL: %s", input);
111 }
112
113 // Fetch the key via HTTPS
114 char *response = https_get(url_parts.host, url_parts.path);
115 url_parts_destroy(&url_parts);
116
117 if (!response) {
118 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch key from HTTPS URL: %s", input);
119 }
120
121 // Parse the fetched content as a public key
122 asciichat_error_t result = parse_public_key(response, key_out);
123 SAFE_FREE(response);
124 return result;
125 }
126
127 // Try HTTPS key fetching (GitHub/GitLab) - delegate to parse_public_keys and return first
128 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
129 public_key_t keys[1];
130 size_t num_keys = 0;
131 asciichat_error_t result = parse_public_keys(input, keys, &num_keys, 1);
132 if (result == ASCIICHAT_OK && num_keys > 0) {
133 *key_out = keys[0];
134 }
135 return result;
136 }
137
138 // Try raw hex key (64 hex chars = 32 bytes)
139 if (strlen(input) == 64) {
140 // Check if it's valid hex
141 bool is_valid_hex = true;
142 for (int i = 0; i < 64; i++) {
143 if (!((input[i] >= '0' && input[i] <= '9') || (input[i] >= 'a' && input[i] <= 'f') ||
144 (input[i] >= 'A' && input[i] <= 'F'))) {
145 is_valid_hex = false;
146 break;
147 }
148 }
149
150 if (is_valid_hex) {
151 // Assume it's a raw X25519 public key in hex (default for raw hex)
152 key_out->type = KEY_TYPE_X25519;
153 // Use hex_decode utility to safely decode hex string to binary
154 if (hex_decode(input, key_out->key, 32) != ASCIICHAT_OK) {
155 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode hex key");
156 }
157 platform_strncpy(key_out->comment, sizeof(key_out->comment), "raw-hex", sizeof(key_out->comment) - 1);
158 return ASCIICHAT_OK;
159 }
160 }
161
162 // Try PGP armored format (-----BEGIN PGP PUBLIC KEY BLOCK-----)
163 if (strstr(input, "-----BEGIN PGP PUBLIC KEY BLOCK-----") != NULL) {
164 return parse_gpg_key_binary((const uint8_t *)input, strlen(input), key_out);
165 }
166
167 if (path_looks_like_path(input)) {
168 char *normalized_path = NULL;
169 asciichat_error_t path_result = path_validate_user_path(input, PATH_ROLE_KEY_PUBLIC, &normalized_path);
170 if (path_result != ASCIICHAT_OK) {
171 SAFE_FREE(normalized_path);
172 return path_result;
173 }
174
175 FILE *f = platform_fopen("file_stream", normalized_path, "r");
176 if (f) {
177 char line[BUFFER_SIZE_LARGE];
178 if (fgets(line, sizeof(line), f)) {
179 (void)fclose(f);
180 SAFE_FREE(normalized_path);
181 // Remove newline
182 line[strcspn(line, "\r\n")] = 0;
183 return parse_public_key(line, key_out);
184 }
185 (void)fclose(f);
186 }
187 SAFE_FREE(normalized_path);
188 }
189
190 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key format: %s", input);
191}
asciichat_error_t parse_ssh_ed25519_line(const char *line, uint8_t ed25519_pk[32])
Parse SSH Ed25519 public key from "ssh-ed25519 AAAAC3..." format.
Definition ssh_keys.c:163
asciichat_error_t parse_public_keys(const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys)
Parse all SSH/GPG public keys from any format (returns all keys)
Definition keys.c:344
asciichat_error_t parse_gpg_key(const char *gpg_key_id, public_key_t *key_out)
Parse GPG key from armored text format.
Definition gpg_keys.c:25
asciichat_error_t hex_decode(const char *hex, uint8_t *output, size_t output_len)
Decode hex string to binary (utility function for testing)
Definition keys.c:626
asciichat_error_t parse_gpg_key_binary(const uint8_t *gpg_key_binary, size_t key_size, public_key_t *key_out)
Parse GPG key from binary format.
Definition gpg_keys.c:71
char * https_get(const char *hostname, const char *path)
Perform HTTPS GET request.
void url_parts_destroy(url_parts_t *parts)
Free parsed URL components.
Definition url.c:281
bool url_is_valid(const char *url)
Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
Definition url.c:81
asciichat_error_t url_parse(const char *url, url_parts_t *parts_out)
Parse HTTP(S), WebSocket, and TCP URL into components.
Definition url.c:166
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:320
Structure containing parsed URL components.
Definition url.h:57
char * path
Extracted path, query, and fragment (allocated if present, may be NULL)
Definition url.h:63
char * host
Extracted hostname or IP address (allocated)
Definition url.h:60

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, public_key_t::comment, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, hex_decode(), url_parts_t::host, https_get(), public_key_t::key, KEY_TYPE_ED25519, KEY_TYPE_X25519, parse_gpg_key(), parse_gpg_key_binary(), parse_public_key(), parse_public_keys(), parse_ssh_ed25519_line(), url_parts_t::path, path_looks_like_path(), PATH_ROLE_KEY_PUBLIC, path_validate_user_path(), platform_fopen(), platform_strncpy(), SAFE_FREE, SAFE_MALLOC, SET_ERRNO, public_key_t::type, url_is_valid(), url_parse(), and url_parts_destroy().

Referenced by check_known_host(), discovery_keys_download_https(), discovery_keys_load_file(), discovery_keys_verify(), parse_keys_from_file(), parse_public_key(), and parse_public_keys().

◆ parse_public_keys()

asciichat_error_t parse_public_keys ( const char *  input,
public_key_t *  keys_out,
size_t *  num_keys,
size_t  max_keys 
)

#include <keys.h>

Parse all SSH/GPG public keys from any format (returns all keys)

Parameters
inputKey input in various formats (see parse_public_key for supported formats)
keys_outOutput array for parsed public keys (must not be NULL)
num_keysOutput parameter for number of keys parsed (must not be NULL)
max_keysMaximum number of keys to parse (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Similar to parse_public_key(), but returns ALL keys for formats that support multiple keys:

Multiple key support:

  • GitHub SSH: "github:username" - returns ALL Ed25519 keys from user's profile
  • GitLab SSH: "gitlab:username" - returns ALL Ed25519 keys from user's profile
  • File path: Returns all keys from file (one per line)
  • Comma-separated list: "github:user1, /path/to/keys.pub, https://example.com/key.pub" - returns all keys from all sources

Single key formats (behaves like parse_public_key):

  • SSH Ed25519: "ssh-ed25519 AAAAC3..." - returns single key
  • Raw hex: 64 hex chars - returns single key
  • GPG formats: Returns single key

Comma-separated support: When input contains commas, each comma-separated specifier is parsed independently. Whitespace around commas is automatically trimmed. If any specifier fails to parse, that specifier is skipped with a warning, but parsing continues for remaining specifiers. Returns ASCIICHAT_OK if at least one specifier parsed successfully.

This function is useful when you need to verify against ANY of a user's keys, such as when a user has multiple SSH keys for different machines on their GitHub/GitLab account, or keys from multiple sources.

Note
GitHub/GitLab: Users often have multiple SSH keys for different machines. This function fetches all keys so you can verify against any of them.
Key filtering: Only Ed25519 keys are returned. RSA/ECDSA keys are skipped.
Warning
Network operations: GitHub/GitLab fetching requires network connectivity.

Definition at line 344 of file keys.c.

344 {
345 if (!input || !keys_out || !num_keys || max_keys == 0) {
346 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for multi-key parsing");
347 }
348
349 *num_keys = 0;
350
351 // Handle comma-separated key specifiers
352 if (strchr(input, ',') != NULL) {
353 size_t input_len = strlen(input);
354 char *input_copy = SAFE_MALLOC(input_len + 1, char *);
355 memcpy(input_copy, input, input_len + 1);
356
357 char *saveptr = NULL;
358 char *specifier = platform_strtok_r(input_copy, ",", &saveptr);
359
360 while (specifier != NULL && *num_keys < max_keys) {
361 // Trim leading/trailing whitespace
362 while (*specifier && isspace((unsigned char)*specifier)) {
363 specifier++;
364 }
365 char *end = specifier + strlen(specifier) - 1;
366 while (end > specifier && isspace((unsigned char)*end)) {
367 *end-- = '\0';
368 }
369
370 // Skip empty specifiers
371 if (strlen(specifier) > 0) {
372 // Recursively parse this single specifier (will not re-enter comma handling)
373 size_t current_num = 0;
374 asciichat_error_t result =
375 parse_public_keys(specifier, &keys_out[*num_keys], &current_num, max_keys - *num_keys);
376 if (result == ASCIICHAT_OK && current_num > 0) {
377 *num_keys += current_num;
378 } else {
379 log_warn("Failed to parse key specifier: %s", specifier);
380 // Continue parsing remaining specifiers even if one fails
381 }
382 }
383
384 specifier = platform_strtok_r(NULL, ",", &saveptr);
385 }
386
387 SAFE_FREE(input_copy);
388
389 if (*num_keys == 0) {
390 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid keys found in comma-separated list: %s", input);
391 }
392 return ASCIICHAT_OK;
393 }
394
395 // Check for direct SSH Ed25519 key format BEFORE checking for file paths
396 // (SSH keys can contain '/' in base64, which would match path_looks_like_path)
397 if (strncmp(input, "ssh-ed25519", 11) == 0) {
398 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
399 if (result == ASCIICHAT_OK) {
400 *num_keys = 1;
401 }
402 return result;
403 }
404
405 // Check if this is a GitHub/GitLab reference - these support multiple keys
406 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
407 const char *username = input + 7; // Skip "github:" or "gitlab:"
408 bool is_github = (strncmp(input, "github:", 7) == 0);
409 bool is_gpg = (strstr(username, ".gpg") != NULL);
410
411 char **keys = NULL;
412 size_t num_fetched_keys = 0;
413 asciichat_error_t result;
414
415 if (is_github) {
416 if (is_gpg) {
417 result = fetch_github_gpg_keys(username, &keys, &num_fetched_keys);
418 } else {
419 result = fetch_github_ssh_keys(username, &keys, &num_fetched_keys);
420 }
421 } else {
422 if (is_gpg) {
423 result = fetch_gitlab_gpg_keys(username, &keys, &num_fetched_keys);
424 } else {
425 result = fetch_gitlab_ssh_keys(username, &keys, &num_fetched_keys);
426 }
427 }
428
429 if (result != ASCIICHAT_OK || num_fetched_keys == 0) {
430 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch keys from %s for user: %s", is_github ? "GitHub" : "GitLab",
431 username);
432 }
433
434 // Parse each fetched key (only Ed25519 keys will succeed)
435 for (size_t i = 0; i < num_fetched_keys && *num_keys < max_keys; i++) {
436 if (parse_public_key(keys[i], &keys_out[*num_keys]) == ASCIICHAT_OK) {
437 (*num_keys)++;
438 }
439 // Non-Ed25519 keys are silently skipped
440 }
441
442 // Free the keys array
443 for (size_t i = 0; i < num_fetched_keys; i++) {
444 SAFE_FREE(keys[i]);
445 }
446 SAFE_FREE(keys);
447
448 if (*num_keys == 0) {
449 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid Ed25519 keys found for %s user: %s", is_github ? "GitHub" : "GitLab",
450 username);
451 }
452
453 log_debug("Parsed %zu Ed25519 key(s) from %s user: %s", *num_keys, is_github ? "GitHub" : "GitLab", username);
454 return ASCIICHAT_OK;
455 }
456
457 // For file paths, delegate to parse_keys_from_file
458 if (path_looks_like_path(input)) {
459 return parse_keys_from_file(input, keys_out, num_keys, max_keys);
460 }
461
462 // For all other formats, use single-key parsing
463 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
464 if (result == ASCIICHAT_OK) {
465 *num_keys = 1;
466 }
467 return result;
468}
char * platform_strtok_r(char *str, const char *delim, char **saveptr)
Thread-safe string tokenization (strtok_r replacement)

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, ERROR_NOT_SUPPORTED, fetch_github_gpg_keys(), fetch_github_ssh_keys(), fetch_gitlab_gpg_keys(), fetch_gitlab_ssh_keys(), log_debug, log_warn, parse_keys_from_file(), parse_public_key(), parse_public_keys(), path_looks_like_path(), platform_strtok_r(), SAFE_FREE, SAFE_MALLOC, and SET_ERRNO.

Referenced by client_crypto_handshake(), crypto_handshake_client_key_exchange(), parse_public_key(), and parse_public_keys().

◆ parse_ssh_ed25519_line()

asciichat_error_t parse_ssh_ed25519_line ( const char *  line,
uint8_t  ed25519_pk[32] 
)

#include <ssh_keys.h>

Parse SSH Ed25519 public key from "ssh-ed25519 AAAAC3..." format.

Parameters
lineSSH key line to parse (must not be NULL)
ed25519_pkOutput buffer for Ed25519 public key (32 bytes, must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses SSH Ed25519 public key from standard SSH public key format. Format: "ssh-ed25519 <base64_key> [comment]"

Note
Format: Expects "ssh-ed25519" prefix followed by base64-encoded public key. Optional comment after key is ignored.
Base64 decoding: Decodes base64 key to extract 32-byte Ed25519 public key. Validates key format and length.
Key size: Ed25519 public key is exactly 32 bytes. Returns error if decoded key is not 32 bytes.
Warning
Key format: Must start with "ssh-ed25519". Other key types (RSA, ECDSA) are NOT supported.

Definition at line 163 of file ssh_keys.c.

163 {
164 if (!line || !ed25519_pk) {
165 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: line=%p, ed25519_pk=%p", line, ed25519_pk);
166 }
167
168 // Use regex to parse and extract SSH public key components
169 char *base64_key = NULL, *comment = NULL;
170 if (!crypto_regex_match_public_key(line, &base64_key, &comment)) {
171 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key line does not match expected format");
172 }
173
174 // Base64 decode
175 uint8_t *blob;
176 size_t blob_len;
177 if (base64_decode_ssh_key(base64_key, strlen(base64_key), &blob, &blob_len) != 0) {
178 SAFE_FREE(base64_key);
179 SAFE_FREE(comment);
180 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode SSH key base64 data");
181 }
182
183 // Clean up regex-extracted strings
184 SAFE_FREE(base64_key);
185 SAFE_FREE(comment);
186
187 // Parse SSH key blob structure:
188 // [4 bytes: length of "ssh-ed25519"]
189 // [11 bytes: "ssh-ed25519"]
190 // [4 bytes: length of public key (32)]
191 // [32 bytes: Ed25519 public key]
192
193 if (blob_len < SSH_KEY_HEADER_SIZE) {
194 SAFE_FREE(blob);
195 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key blob too small: %zu bytes (expected at least %d)", blob_len,
197 }
198
199 // Extract Ed25519 public key (last 32 bytes)
200 memcpy(ed25519_pk, blob + blob_len - ED25519_PUBLIC_KEY_SIZE, ED25519_PUBLIC_KEY_SIZE);
201 SAFE_FREE(blob);
202
203 return ASCIICHAT_OK;
204}
#define ED25519_PUBLIC_KEY_SIZE
Ed25519 public key size in bytes.
#define SSH_KEY_HEADER_SIZE
bool crypto_regex_match_public_key(const char *line, char **base64_key_out, char **comment_out)
Definition regex.c:184

References ASCIICHAT_OK, crypto_regex_match_public_key(), ED25519_PUBLIC_KEY_SIZE, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, SAFE_FREE, SET_ERRNO, and SSH_KEY_HEADER_SIZE.

Referenced by parse_public_key(), and parse_ssh_private_key().

◆ parse_ssh_keys_from_response()

asciichat_error_t parse_ssh_keys_from_response ( const char *  response_text,
size_t  response_len,
char ***  keys_out,
size_t *  num_keys,
size_t  max_keys 
)

#include <https_keys.h>

Parse SSH keys from HTTPS response text.

Parameters
response_textHTTPS response containing SSH keys (must not be NULL)
response_lenLength of response text
keys_outOutput array of parsed SSH keys (must not be NULL)
num_keysOutput parameter for number of keys parsed (must not be NULL)
max_keysMaximum number of keys to parse (must be > 0)
Returns
ASCIICHAT_OK on success, error code on failure

Parses SSH keys from HTTPS response text (one key per line). Extracts Ed25519 keys and allocates memory for each key string.

Note
Response format: Expects one SSH key per line. Format: "ssh-ed25519 AAAAC3... comment" (one key per line)
Key filtering: Only Ed25519 SSH keys are parsed. Other key types (RSA, ECDSA) are skipped.
Memory allocation: Allocates memory for each key string. Caller must free each string and the array.
Maximum keys: Stops parsing after max_keys keys are found. Function returns ASCIICHAT_OK even if more keys exist in response.
Warning
Memory leak: Caller must free returned strings and array. Function allocates memory that must be freed.

Definition at line 298 of file https_keys.c.

299 {
300 if (!response_text || !keys_out || !num_keys) {
301 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for SSH key parsing");
302 return ERROR_INVALID_PARAM;
303 }
304
305 *num_keys = 0;
306 *keys_out = NULL;
307
308 // Count the number of SSH keys in the response
309 size_t key_count = 0;
310 const char *line_start = response_text;
311 const char *line_end;
312
313 while ((line_end = strchr(line_start, '\n')) != NULL) {
314 size_t line_len = line_end - line_start;
315 if (line_len > 0 && line_start[0] != '\r' && line_start[0] != '\n') {
316 key_count++;
317 }
318 line_start = line_end + 1;
319 }
320
321 // Handle last line if it doesn't end with newline
322 if (line_start < response_text + response_len) {
323 key_count++;
324 }
325
326 if (key_count == 0) {
327 SET_ERRNO(ERROR_CRYPTO_KEY, "No SSH keys found in response");
328 return ERROR_CRYPTO_KEY;
329 }
330
331 if (key_count > max_keys) {
332 key_count = max_keys;
333 }
334
335 // Allocate array for key strings
336 *keys_out = SAFE_MALLOC(sizeof(char *) * key_count, char **);
337 if (!*keys_out) {
338 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SSH keys array");
339 }
340
341 // Parse each SSH key line
342 line_start = response_text;
343 size_t parsed_keys = 0;
344
345 while (parsed_keys < key_count && (line_end = strchr(line_start, '\n')) != NULL) {
346 size_t line_len = line_end - line_start;
347
348 // Skip empty lines
349 if (line_len > 0 && line_start[0] != '\r' && line_start[0] != '\n') {
350 // Allocate space for this key line
351 (*keys_out)[parsed_keys] = SAFE_MALLOC(line_len + 1, char *);
352 if (!(*keys_out)[parsed_keys]) {
353 // Cleanup previously allocated keys
354 for (size_t i = 0; i < parsed_keys; i++) {
355 SAFE_FREE((*keys_out)[i]);
356 }
357 SAFE_FREE(*keys_out);
358 *keys_out = NULL;
359 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SSH key string");
360 }
361
362 // Copy the key line
363 memcpy((*keys_out)[parsed_keys], line_start, line_len);
364 (*keys_out)[parsed_keys][line_len] = '\0';
365
366 parsed_keys++;
367 }
368
369 line_start = line_end + 1;
370 }
371
372 // Handle last line if it doesn't end with newline
373 if (parsed_keys < key_count && line_start < response_text + response_len) {
374 size_t line_len = (response_text + response_len) - line_start;
375 if (line_len > 0) {
376 (*keys_out)[parsed_keys] = SAFE_MALLOC(line_len + 1, char *);
377 if (!(*keys_out)[parsed_keys]) {
378 // Cleanup previously allocated keys
379 for (size_t i = 0; i < parsed_keys; i++) {
380 SAFE_FREE((*keys_out)[i]);
381 }
382 SAFE_FREE(*keys_out);
383 *keys_out = NULL;
384 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SSH key string");
385 }
386 memcpy((*keys_out)[parsed_keys], line_start, line_len);
387 (*keys_out)[parsed_keys][line_len] = '\0';
388 parsed_keys++;
389 }
390 }
391
392 *num_keys = parsed_keys;
393 return ASCIICHAT_OK;
394}

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

Referenced by fetch_github_ssh_keys(), and fetch_gitlab_ssh_keys().

◆ parse_ssh_private_key()

asciichat_error_t parse_ssh_private_key ( const char *  key_path,
private_key_t *  key_out 
)

#include <ssh_keys.h>

Parse SSH Ed25519 private key from file.

Parameters
key_pathPath to SSH private key file (must not be NULL)
key_outOutput structure for parsed private key (must not be NULL)
Returns
ASCIICHAT_OK on success, error code on failure

Parses OpenSSH Ed25519 private key from file. Supports both encrypted and unencrypted keys.

Note
Key format: Supports OpenSSH private key format (openssh-key-v1). Format: "-----BEGIN OPENSSH PRIVATE KEY-----" ... "-----END OPENSSH PRIVATE KEY-----"
Encrypted keys: If key is encrypted, prompts for password and decrypts it natively. Uses bcrypt_pbkdf (from libsodium-bcrypt-pbkdf) + BearSSL AES (aes256-ctr/aes256-cbc) for OpenSSH format decryption. No external tools (ssh-keygen) required.
Key structure: Parses OpenSSH key structure:
  • Magic: "openssh-key-v1\0"
  • Ciphername: "none" for unencrypted, cipher name for encrypted
  • KDF name and options (for encrypted keys)
  • Number of keys (must be 1)
  • Public key (for verification)
  • Private key blob (decrypted if encrypted)
Ed25519 extraction: Extracts Ed25519 seed (32 bytes) and public key (32 bytes) from private key blob. Stores as 64-byte Ed25519 key (seed + public).
File validation: Validates file permissions before parsing. Warns if file has overly permissive permissions (world-readable).
Warning
Key format: Only Ed25519 keys are supported. RSA/ECDSA keys will return error.
File permissions: Private key files should have restrictive permissions (0600). Function warns but does not fail on overly permissive permissions.
Password: Encrypted keys require password for native decryption. Decryption is done using bcrypt_pbkdf + BearSSL AES (supports aes256-ctr and aes256-cbc). May fail if password is incorrect, KDF parameters are unsupported, or unsupported encryption cipher.

Definition at line 206 of file ssh_keys.c.

206 {
207 if (!key_path || !key_out) {
208 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p, key_out=%p", key_path, key_out);
209 }
210
211 // First, check if we can get the key from ssh-agent (password-free)
212 // This requires reading the public key from the .pub file
213 char pub_key_path[BUFFER_SIZE_LARGE];
214 safe_snprintf(pub_key_path, sizeof(pub_key_path), "%s.pub", key_path);
215
216 FILE *pub_f = platform_fopen("file_stream", pub_key_path, "r");
217 if (pub_f) {
218 char pub_line[BUFFER_SIZE_LARGE];
219 if (fgets(pub_line, sizeof(pub_line), pub_f)) {
220 public_key_t pub_key = {0};
221 pub_key.type = KEY_TYPE_ED25519;
222
223 if (parse_ssh_ed25519_line(pub_line, pub_key.key) == ASCIICHAT_OK) {
224 // Check if this key is in ssh-agent
225 if (ssh_agent_has_key(&pub_key)) {
226 fclose(pub_f);
227 log_dev("Key found in ssh-agent - using cached key (no password required)");
228 // Key is in agent, we can use it
229 key_out->type = KEY_TYPE_ED25519;
230 key_out->use_ssh_agent = true;
231 memcpy(key_out->key.ed25519 + 32, pub_key.key, 32); // Copy public key to second half
232 memcpy(key_out->public_key, pub_key.key, 32); // Also store in public_key field
233 // Note: We don't have the private key seed, but ssh-agent will sign for us
234 return ASCIICHAT_OK;
235 } else {
236 log_debug("Key not found in ssh-agent - will decrypt from file");
237 }
238 }
239 }
240 fclose(pub_f);
241 }
242
243 // Validate the SSH key file first
244 asciichat_error_t validation_result = validate_ssh_key_file(key_path);
245 if (validation_result != ASCIICHAT_OK) {
246 return validation_result;
247 }
248
249 // Read the private key file
250 FILE *f = platform_fopen("file_stream", key_path, "r");
251 if (!f) {
252 return SET_ERRNO(ERROR_CRYPTO_KEY, "Cannot read private key file: %s", key_path);
253 }
254
255 // Read the entire file
256 char *file_content = NULL;
257 size_t file_size = 0;
258 char buffer[BUFFER_SIZE_XXLARGE];
259 size_t bytes_read;
260
261 while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) {
262 file_content = SAFE_REALLOC(file_content, file_size + bytes_read + 1, char *);
263 if (!file_content) {
264 (void)fclose(f);
265 return SET_ERRNO(ERROR_CRYPTO_KEY, "Out of memory reading private key file");
266 }
267 memcpy(file_content + file_size, buffer, bytes_read);
268 file_size += bytes_read;
269 }
270 (void)fclose(f);
271
272 if (file_size == 0) {
273 SAFE_FREE(file_content);
274 return SET_ERRNO(ERROR_CRYPTO_KEY, "Private key file is empty: %s", key_path);
275 }
276
277 file_content[file_size] = '\0';
278
279 // Use regex to extract base64 data from OpenSSH PEM format
280 char *pem_base64_data = NULL;
281 if (!crypto_regex_extract_pem_base64(file_content, &pem_base64_data)) {
282 SAFE_FREE(file_content);
283 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid OpenSSH private key format: %s", key_path);
284 }
285
286 // Remove any whitespace/newlines from the base64 data
287 char *clean_base64 = SAFE_MALLOC(strlen(pem_base64_data) + 1, char *);
288 char *clean_ptr = clean_base64;
289 for (const char *p = pem_base64_data; *p; p++) {
290 if (*p != '\n' && *p != '\r' && *p != ' ' && *p != '\t') {
291 *clean_ptr++ = *p;
292 }
293 }
294 *clean_ptr = '\0';
295 SAFE_FREE(pem_base64_data);
296
297 // Decode the base64 data
298 uint8_t *key_blob;
299 size_t key_blob_len;
300 asciichat_error_t decode_result = base64_decode_ssh_key(clean_base64, strlen(clean_base64), &key_blob, &key_blob_len);
301 SAFE_FREE(clean_base64);
302
303 if (decode_result != ASCIICHAT_OK) {
304 SAFE_FREE(file_content);
305 return decode_result;
306 }
307
308 // Parse the OpenSSH private key structure
309 // Format: [4 bytes: magic] [4 bytes: ciphername length] [ciphername] [4 bytes: kdfname length] [kdfname]
310 // [4 bytes: kdfoptions length] [kdfoptions] [4 bytes: num keys] [4 bytes: pubkey length] [pubkey]
311 // [4 bytes: privkey length] [privkey]
312
313 if (key_blob_len < 4) {
314 SAFE_FREE(key_blob);
315 SAFE_FREE(file_content);
316 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key blob too small: %s", key_path);
317 }
318
319 // Check magic number (should be "openssh-key-v1\0")
320 if (memcmp(key_blob, "openssh-key-v1\0", 15) != 0) {
321 SAFE_FREE(key_blob);
322 SAFE_FREE(file_content);
323 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid OpenSSH private key magic: %s", key_path);
324 }
325
326 size_t offset = 15; // Skip magic
327
328 // Read ciphername
329 if (offset + 4 > key_blob_len) {
330 SAFE_FREE(key_blob);
331 SAFE_FREE(file_content);
332 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at ciphername: %s", key_path);
333 }
334
335 uint32_t ciphername_len = read_u32_be(&key_blob[offset]);
336 offset += 4;
337
338 if (offset + ciphername_len > key_blob_len) {
339 SAFE_FREE(key_blob);
340 SAFE_FREE(file_content);
341 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at ciphername data: %s", key_path);
342 }
343
344 // Store the position of ciphername for later use
345 size_t ciphername_pos = offset;
346
347 // Check if key is encrypted
348 bool is_encrypted = (ciphername_len > 0 && memcmp(key_blob + offset, "none", 4) != 0);
349
350 offset += ciphername_len;
351
352 // Skip kdfname and kdfoptions (we don't support encryption)
353 if (offset + 4 > key_blob_len) {
354 SAFE_FREE(key_blob);
355 SAFE_FREE(file_content);
356 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at kdfname: %s", key_path);
357 }
358
359 uint32_t kdfname_len = read_u32_be(&key_blob[offset]);
360 offset += 4;
361
362 // Store the position of kdfname for later use
363 size_t kdfname_pos = offset;
364
365 offset += kdfname_len;
366
367 if (offset + 4 > key_blob_len) {
368 SAFE_FREE(key_blob);
369 SAFE_FREE(file_content);
370 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at kdfoptions: %s", key_path);
371 }
372
373 uint32_t kdfoptions_len = read_u32_be(&key_blob[offset]);
374 offset += 4 + kdfoptions_len;
375
376 // Handle encrypted keys
377 if (is_encrypted) {
378 // Parse the cipher name from the stored position
379 char ciphername[32] = {0};
380 if (ciphername_len > 0 && ciphername_len < sizeof(ciphername)) {
381 memcpy(ciphername, key_blob + ciphername_pos, ciphername_len);
382 }
383
384 // Parse the KDF name from the stored position
385 char kdfname[32] = {0};
386 if (kdfname_len > 0 && kdfname_len < sizeof(kdfname)) {
387 memcpy(kdfname, key_blob + kdfname_pos, kdfname_len);
388 }
389
390 log_debug("Cipher: %s, KDF: %s", ciphername, kdfname);
391
392 // Check if we support this encryption method
393 if (strcmp(ciphername, "aes256-ctr") != 0 && strcmp(ciphername, "aes256-cbc") != 0) {
394 SAFE_FREE(key_blob);
395 SAFE_FREE(file_content);
397 "Unsupported cipher '%s' for encrypted SSH key: %s\n"
398 "Supported ciphers: aes256-ctr, aes256-cbc",
399 ciphername, key_path);
400 }
401
402 if (strcmp(kdfname, "bcrypt") != 0) {
403 SAFE_FREE(key_blob);
404 SAFE_FREE(file_content);
406 "Unsupported KDF '%s' for encrypted SSH key: %s\n"
407 "Only bcrypt KDF is supported",
408 kdfname, key_path);
409 }
410
411 // Parse KDF options (bcrypt salt and rounds)
412 if (kdfoptions_len < 8) {
413 SAFE_FREE(key_blob);
414 SAFE_FREE(file_content);
415 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid KDF options length: %s", key_path);
416 }
417
418 // Parse KDF options: [salt_length:4][salt:N][rounds:4]
419 size_t kdf_opt_offset = offset - kdfoptions_len;
420
421 // Read salt length
422 if (kdf_opt_offset + 4 > key_blob_len) {
423 SAFE_FREE(key_blob);
424 SAFE_FREE(file_content);
425 return SET_ERRNO(ERROR_CRYPTO_KEY, "KDF options truncated at salt length: %s", key_path);
426 }
427 uint32_t salt_len = read_u32_be(&key_blob[kdf_opt_offset]);
428 kdf_opt_offset += 4;
429
430 // Read salt
431 if (salt_len != 16) {
432 SAFE_FREE(key_blob);
433 SAFE_FREE(file_content);
434 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unexpected bcrypt salt length %u (expected 16): %s", salt_len, key_path);
435 }
436 if (kdf_opt_offset + salt_len > key_blob_len) {
437 SAFE_FREE(key_blob);
438 SAFE_FREE(file_content);
439 return SET_ERRNO(ERROR_CRYPTO_KEY, "KDF options truncated at salt data: %s", key_path);
440 }
441 uint8_t bcrypt_salt[16];
442 memcpy(bcrypt_salt, key_blob + kdf_opt_offset, salt_len);
443 kdf_opt_offset += salt_len;
444
445 // Read rounds
446 if (kdf_opt_offset + 4 > key_blob_len) {
447 SAFE_FREE(key_blob);
448 SAFE_FREE(file_content);
449 return SET_ERRNO(ERROR_CRYPTO_KEY, "KDF options truncated at rounds: %s", key_path);
450 }
451 uint32_t bcrypt_rounds = read_u32_be(&key_blob[kdf_opt_offset]);
452
453 // Check for password in environment variable first
454 const char *env_password = platform_getenv("ASCII_CHAT_KEY_PASSWORD");
455 char *password = NULL;
456 if (env_password && strlen(env_password) > 0) {
457 // Use password from environment variable
458 password = platform_strdup(env_password);
459 if (!password) {
460 SAFE_FREE(key_blob);
461 SAFE_FREE(file_content);
462 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for password");
463 }
464 } else {
465 // Prompt for password interactively - allocate buffer for input
466 password = SAFE_MALLOC(1024, char *);
467 if (!password) {
468 SAFE_FREE(key_blob);
469 SAFE_FREE(file_content);
470 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for password");
471 }
472 if (prompt_password_simple("Encrypted SSH key - enter passphrase", password, 1024) != 0) {
473 SAFE_FREE(key_blob);
474 SAFE_FREE(file_content);
475 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to read passphrase for encrypted key: %s", key_path);
476 }
477 }
478
479 // Native OpenSSH key decryption using bcrypt_pbkdf + BearSSL AES
480
481 // Skip past the unencrypted public key section to get to encrypted private keys
482 // Format: [num_keys:4][pubkey_len:4][pubkey:N]...[encrypted_len:4][encrypted:N]
483
484 // Read num_keys
485 if (offset + 4 > key_blob_len) {
486 sodium_memzero(password, strlen(password));
487 SAFE_FREE(password);
488 SAFE_FREE(key_blob);
489 SAFE_FREE(file_content);
490 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at num_keys: %s", key_path);
491 }
492 uint32_t num_keys = read_u32_be(&key_blob[offset]);
493 offset += 4;
494 log_debug("num_keys=%u", num_keys);
495
496 // Skip all public keys
497 for (uint32_t i = 0; i < num_keys; i++) {
498 if (offset + 4 > key_blob_len) {
499 sodium_memzero(password, strlen(password));
500 SAFE_FREE(password);
501 SAFE_FREE(key_blob);
502 SAFE_FREE(file_content);
503 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at pubkey %u length: %s", i, key_path);
504 }
505 uint32_t pubkey_len = read_u32_be(&key_blob[offset]);
506 offset += 4;
507 log_debug("Skipping public key %u: %u bytes", i, pubkey_len);
508
509 if (offset + pubkey_len > key_blob_len) {
510 sodium_memzero(password, strlen(password));
511 SAFE_FREE(password);
512 SAFE_FREE(key_blob);
513 SAFE_FREE(file_content);
514 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at pubkey %u data: %s", i, key_path);
515 }
516 offset += pubkey_len;
517 }
518
519 // Read encrypted private keys length
520 if (offset + 4 > key_blob_len) {
521 sodium_memzero(password, strlen(password));
522 SAFE_FREE(password);
523 SAFE_FREE(key_blob);
524 SAFE_FREE(file_content);
525 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted key truncated at encrypted_len: %s", key_path);
526 }
527 uint32_t encrypted_len = read_u32_be(&key_blob[offset]);
528 offset += 4;
529
530 // Now offset points to the actual encrypted data
531 size_t encrypted_data_start = offset;
532 size_t encrypted_data_len = encrypted_len;
533
534 if (encrypted_data_len < 16) {
535 sodium_memzero(password, strlen(password));
536 SAFE_FREE(password);
537 SAFE_FREE(key_blob);
538 SAFE_FREE(file_content);
539 return SET_ERRNO(ERROR_CRYPTO_KEY, "Encrypted data too small: %s", key_path);
540 }
541
542 // Extract encrypted blob (includes everything from offset onwards)
543 const uint8_t *encrypted_blob = key_blob + encrypted_data_start;
544
545 // Call native decryption function
546 uint8_t *decrypted_blob = NULL;
547 size_t decrypted_blob_len = 0;
548
549 // Note: decrypt_openssh_private_key derives IV from bcrypt_pbkdf, not from encrypted data
550 asciichat_error_t decrypt_result =
551 decrypt_openssh_private_key(encrypted_blob, encrypted_data_len, password, bcrypt_salt, salt_len, bcrypt_rounds,
552 ciphername, &decrypted_blob, &decrypted_blob_len);
553
554 // Clean up password immediately after use
555 sodium_memzero(password, strlen(password));
556 SAFE_FREE(password);
557
558 if (decrypt_result != ASCIICHAT_OK) {
559 SAFE_FREE(key_blob);
560 SAFE_FREE(file_content);
561 return decrypt_result;
562 }
563
564 // Parse the decrypted private key structure
565 // OpenSSH format (decrypted):
566 // [checkint1:4][checkint2:4][keytype:string][pubkey:string][privkey:string][comment:string][padding:N]
567
568 if (decrypted_blob_len < 8) {
569 SAFE_FREE(decrypted_blob);
570 SAFE_FREE(key_blob);
571 SAFE_FREE(file_content);
572 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted data too small (no checkints): %s", key_path);
573 }
574
575 // Verify checkints (first 8 bytes should be two identical 32-bit values)
576 uint32_t checkint1 = read_u32_be(&decrypted_blob[0]);
577 uint32_t checkint2 = read_u32_be(&decrypted_blob[4]);
578 if (checkint1 != checkint2) {
579 SAFE_FREE(decrypted_blob);
580 SAFE_FREE(key_blob);
581 SAFE_FREE(file_content);
583 "Incorrect passphrase or corrupted key (checkint mismatch): %s\n"
584 "Expected matching checkints, got 0x%08x != 0x%08x",
585 key_path, checkint1, checkint2);
586 }
587
588 // Parse the decrypted private key structure manually
589 // Format after checkints: [keytype:string][pubkey:string][privkey:string][comment:string][padding:N]
590 size_t dec_offset = 8; // Skip checkints
591
592 // Read keytype length
593 if (dec_offset + 4 > decrypted_blob_len) {
594 SAFE_FREE(decrypted_blob);
595 SAFE_FREE(key_blob);
596 SAFE_FREE(file_content);
597 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at keytype length: %s", key_path);
598 }
599 uint32_t keytype_len = read_u32_be(&decrypted_blob[dec_offset]);
600 dec_offset += 4;
601
602 // Read keytype
603 if (dec_offset + keytype_len > decrypted_blob_len) {
604 SAFE_FREE(decrypted_blob);
605 SAFE_FREE(key_blob);
606 SAFE_FREE(file_content);
607 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at keytype data: %s", key_path);
608 }
609 char keytype[BUFFER_SIZE_SMALL / 4] = {0}; // SSH key type strings are short
610 if (keytype_len > 0 && keytype_len < sizeof(keytype)) {
611 memcpy(keytype, decrypted_blob + dec_offset, keytype_len);
612 }
613 dec_offset += keytype_len;
614
615 // Check if it's Ed25519
616 if (strcmp(keytype, "ssh-ed25519") != 0) {
617 SAFE_FREE(decrypted_blob);
618 SAFE_FREE(key_blob);
619 SAFE_FREE(file_content);
620 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type after decryption: '%s'", keytype);
621 }
622
623 // Read public key length
624 if (dec_offset + 4 > decrypted_blob_len) {
625 SAFE_FREE(decrypted_blob);
626 SAFE_FREE(key_blob);
627 SAFE_FREE(file_content);
628 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at pubkey length: %s", key_path);
629 }
630 uint32_t pubkey_data_len = read_u32_be(&decrypted_blob[dec_offset]);
631 dec_offset += 4;
632
633 // Read public key (32 bytes for Ed25519)
634 if (pubkey_data_len != 32) {
635 SAFE_FREE(decrypted_blob);
636 SAFE_FREE(key_blob);
637 SAFE_FREE(file_content);
638 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid Ed25519 public key length: %u (expected 32)", pubkey_data_len);
639 }
640 if (dec_offset + pubkey_data_len > decrypted_blob_len) {
641 SAFE_FREE(decrypted_blob);
642 SAFE_FREE(key_blob);
643 SAFE_FREE(file_content);
644 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at pubkey data: %s", key_path);
645 }
646 uint8_t ed25519_pk[32];
647 memcpy(ed25519_pk, decrypted_blob + dec_offset, 32);
648 dec_offset += 32;
649
650 // Read private key length
651 if (dec_offset + 4 > decrypted_blob_len) {
652 SAFE_FREE(decrypted_blob);
653 SAFE_FREE(key_blob);
654 SAFE_FREE(file_content);
655 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at privkey length: %s", key_path);
656 }
657 uint32_t privkey_data_len = read_u32_be(&decrypted_blob[dec_offset]);
658 dec_offset += 4;
659
660 // Read private key (64 bytes for Ed25519: 32-byte seed + 32-byte public key)
661 if (privkey_data_len != 64) {
662 SAFE_FREE(decrypted_blob);
663 SAFE_FREE(key_blob);
664 SAFE_FREE(file_content);
665 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid Ed25519 private key length: %u (expected 64)", privkey_data_len);
666 }
667 if (dec_offset + privkey_data_len > decrypted_blob_len) {
668 SAFE_FREE(decrypted_blob);
669 SAFE_FREE(key_blob);
670 SAFE_FREE(file_content);
671 return SET_ERRNO(ERROR_CRYPTO_KEY, "Decrypted key truncated at privkey data: %s", key_path);
672 }
673 uint8_t ed25519_sk[64];
674 memcpy(ed25519_sk, decrypted_blob + dec_offset, 64);
675 dec_offset += 64;
676
677 // Populate key_out (ed25519_sk contains: 32-byte seed + 32-byte public key)
678 key_out->type = KEY_TYPE_ED25519;
679 memcpy(key_out->key.ed25519, ed25519_sk, 32); // Seed (first 32 bytes)
680 memcpy(key_out->key.ed25519 + 32, ed25519_sk + 32, 32); // Public key (next 32 bytes)
681
682 // Clean up decrypted data (sensitive!)
683 sodium_memzero(decrypted_blob, decrypted_blob_len);
684 SAFE_FREE(decrypted_blob);
685 SAFE_FREE(key_blob);
686 SAFE_FREE(file_content);
687
688 log_debug("Successfully parsed decrypted Ed25519 key");
689
690 // Attempt to add the decrypted key to ssh-agent for future password-free use
691 log_debug("Attempting to add decrypted key to ssh-agent");
692 asciichat_error_t agent_result = ssh_agent_add_key(key_out, key_path);
693 if (agent_result == ASCIICHAT_OK) {
694 log_debug("Successfully added key to ssh-agent - password will not be required on next run");
695 } else {
696 // Non-fatal: key is already decrypted and loaded, just won't be cached in agent
697 log_warn("Failed to add key to ssh-agent (non-fatal): %s", asciichat_error_string(agent_result));
698 log_warn("You can manually add it with: ssh-add %s", key_path);
699 }
700
701 return ASCIICHAT_OK;
702 }
703
704 // Read number of keys
705 if (offset + 4 > key_blob_len) {
706 SAFE_FREE(key_blob);
707 SAFE_FREE(file_content);
708 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at num keys: %s", key_path);
709 }
710
711 uint32_t num_keys = read_u32_be(&key_blob[offset]);
712 offset += 4;
713
714 log_debug("num_keys=%u, offset=%zu, key_blob_len=%zu", num_keys, offset, key_blob_len);
715 log_debug("Raw bytes at offset %zu: %02x %02x %02x %02x", offset, key_blob[offset], key_blob[offset + 1],
716 key_blob[offset + 2], key_blob[offset + 3]);
717 log_debug("After num_keys, offset=%zu", offset);
718
719 if (num_keys != 1) {
720 SAFE_FREE(key_blob);
721 SAFE_FREE(file_content);
722 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key contains %u keys (expected 1): %s", num_keys, key_path);
723 }
724
725 // Read public key
726 if (offset + 4 > key_blob_len) {
727 SAFE_FREE(key_blob);
728 SAFE_FREE(file_content);
729 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at pubkey length: %s", key_path);
730 }
731
732 log_debug("About to read pubkey_len at offset=%zu, bytes: %02x %02x %02x %02x", offset, key_blob[offset],
733 key_blob[offset + 1], key_blob[offset + 2], key_blob[offset + 3]);
734
735 uint32_t pubkey_len = read_u32_be(&key_blob[offset]);
736 offset += 4;
737
738 log_debug("pubkey_len=%u, offset=%zu", pubkey_len, offset);
739
740 if (offset + pubkey_len > key_blob_len) {
741 SAFE_FREE(key_blob);
742 SAFE_FREE(file_content);
743 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at pubkey data: %s", key_path);
744 }
745
746 // Parse the public key to extract the Ed25519 public key for validation
747 if (pubkey_len < 4) {
748 SAFE_FREE(key_blob);
749 SAFE_FREE(file_content);
750 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH public key too small: %s", key_path);
751 }
752
753 uint32_t key_type_len = read_u32_be(&key_blob[offset]);
754 offset += 4;
755
756 // Check if it's an Ed25519 key
757 if (key_type_len != 11 || memcmp(key_blob + offset, "ssh-ed25519", 11) != 0) {
758 SAFE_FREE(key_blob);
759 SAFE_FREE(file_content);
760 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key is not Ed25519: %s", key_path);
761 }
762
763 offset += key_type_len; // Skip the key type string
764 log_debug("After skipping key type, offset=%zu", offset);
765
766 // Read the public key length
767 if (offset + 4 > key_blob_len) {
768 SAFE_FREE(key_blob);
769 SAFE_FREE(file_content);
770 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at public key length: %s", key_path);
771 }
772
773 uint32_t pubkey_data_len = read_u32_be(&key_blob[offset]);
774 offset += 4;
775 log_debug("Public key data length: %u, offset=%zu", pubkey_data_len, offset);
776
777 log_debug("Public key data length: %u (expected 32 for Ed25519)", pubkey_data_len);
778
779 // For Ed25519, the public key should be 32 bytes, but let's be more flexible
780 if (pubkey_data_len < 32) {
781 SAFE_FREE(key_blob);
782 SAFE_FREE(file_content);
783 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH public key data too small (%u bytes, expected at least 32): %s",
784 pubkey_data_len, key_path);
785 }
786
787 // Read Ed25519 public key (first 32 bytes) for validation
788 if (offset + 32 > key_blob_len) {
789 SAFE_FREE(key_blob);
790 SAFE_FREE(file_content);
791 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at public key: %s", key_path);
792 }
793
794 uint8_t ed25519_pubkey[32];
795 memcpy(ed25519_pubkey, key_blob + offset, 32);
796 offset += pubkey_data_len; // Skip the entire public key data
797
798 // Skip the rest of the public key data to get to the private key
799 // We've already parsed: 4 (key_type_len) + 11 (ssh-ed25519) + 4 (pubkey_data_len) + 32 (key) = 51 bytes
800 // So we need to skip the remaining pubkey_len - 51 bytes
801 // Check for underflow before subtraction
802 if (pubkey_len >= 51) {
803 size_t remaining_pubkey = pubkey_len - 51;
804 if (remaining_pubkey > 0) {
805 offset += remaining_pubkey;
806 }
807 }
808
809 // Read private key
810 if (offset + 4 > key_blob_len) {
811 SAFE_FREE(key_blob);
812 SAFE_FREE(file_content);
813 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey length: %s", key_path);
814 }
815
816 uint32_t privkey_len = read_u32_be(&key_blob[offset]);
817 offset += 4;
818
819 if (offset + privkey_len > key_blob_len) {
820 SAFE_FREE(key_blob);
821 SAFE_FREE(file_content);
822 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey data: %s", key_path);
823 }
824
825 // Parse the private key structure
826 // Format: [4 bytes: checkint1] [4 bytes: checkint2] [4 bytes: key type length] [key type]
827 // [4 bytes: public key length] [public key] [4 bytes: private key length] [private key]
828 // [4 bytes: comment length] [comment]
829
830 if (privkey_len < 8) {
831 SAFE_FREE(key_blob);
832 SAFE_FREE(file_content);
833 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key data too small: %s", key_path);
834 }
835
836 // Verify checkints (should be equal)
837 uint32_t checkint1 = read_u32_be(&key_blob[offset]);
838 uint32_t checkint2 = read_u32_be(&key_blob[offset + 4]);
839 offset += 8;
840
841 if (checkint1 != checkint2) {
842 SAFE_FREE(key_blob);
843 SAFE_FREE(file_content);
844 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key checkints don't match: %s", key_path);
845 }
846
847 // Skip key type
848 if (offset + 4 > key_blob_len) {
849 SAFE_FREE(key_blob);
850 SAFE_FREE(file_content);
851 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at key type length: %s", key_path);
852 }
853
854 uint32_t key_type_len_priv = read_u32_be(&key_blob[offset]);
855 // Check for integer overflow before adding to offset
856 if (key_type_len_priv > key_blob_len || offset + 4 + key_type_len_priv > key_blob_len) {
857 SAFE_FREE(key_blob);
858 SAFE_FREE(file_content);
859 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key type length overflow: %u", key_type_len_priv);
860 }
861 offset += 4 + key_type_len_priv;
862
863 // Skip public key
864 if (offset + 4 > key_blob_len) {
865 SAFE_FREE(key_blob);
866 SAFE_FREE(file_content);
867 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at pubkey length: %s", key_path);
868 }
869
870 uint32_t pubkey_len_priv = read_u32_be(&key_blob[offset]);
871 // Check for integer overflow before adding to offset
872 if (pubkey_len_priv > key_blob_len || offset + 4 + pubkey_len_priv > key_blob_len) {
873 SAFE_FREE(key_blob);
874 SAFE_FREE(file_content);
875 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key public key length overflow: %u", pubkey_len_priv);
876 }
877 offset += 4 + pubkey_len_priv;
878
879 // Read private key
880 if (offset + 4 > key_blob_len) {
881 SAFE_FREE(key_blob);
882 SAFE_FREE(file_content);
883 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey length: %s", key_path);
884 }
885
886 uint32_t privkey_data_len = read_u32_be(&key_blob[offset]);
887 offset += 4;
888
889 if (offset + privkey_data_len > key_blob_len) {
890 SAFE_FREE(key_blob);
891 SAFE_FREE(file_content);
892 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key truncated at privkey data: %s", key_path);
893 }
894
895 // The private key data should be at least 64 bytes (32 bytes private key + 32 bytes public key)
896 // But OpenSSH format may have additional data
897 if (privkey_data_len < 64) {
898 SAFE_FREE(key_blob);
899 SAFE_FREE(file_content);
900 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key data length is %u (expected at least 64): %s",
901 privkey_data_len, key_path);
902 }
903
904 // Extract the Ed25519 private key (first 32 bytes)
905 uint8_t ed25519_privkey[32];
906 memcpy(ed25519_privkey, key_blob + offset, 32);
907
908 // Verify the public key matches
909 // The public key in the privkey section is raw Ed25519, while the one in pubkey section is SSH format
910 // We need to compare the raw public key from privkey with the raw public key extracted from pubkey
911 // Use constant-time comparison for cryptographic material
912 if (sodium_memcmp(key_blob + offset + 32, ed25519_pubkey, 32) != 0) {
913 SAFE_FREE(key_blob);
914 SAFE_FREE(file_content);
915 return SET_ERRNO(ERROR_CRYPTO_KEY, "OpenSSH private key public key mismatch: %s", key_path);
916 }
917
918 // Initialize the private key structure
919 memset(key_out, 0, sizeof(private_key_t));
920 key_out->type = KEY_TYPE_ED25519;
921
922 // Store the actual private key (seed + public key = 64 bytes)
923 // Ed25519 private key format: [32 bytes seed][32 bytes public key]
924 memcpy(key_out->key.ed25519, ed25519_privkey, 32); // Store the seed (first 32 bytes)
925 memcpy(key_out->key.ed25519 + 32, ed25519_pubkey, 32); // Store the public key (next 32 bytes)
926
927 // Also store the public key in the public_key field for easy access
928 memcpy(key_out->public_key, ed25519_pubkey, 32);
929
930 // Set a comment
931 SAFE_STRNCPY(key_out->key_comment, "ssh-ed25519", sizeof(key_out->key_comment) - 1);
932
933 SAFE_FREE(key_blob);
934 SAFE_FREE(file_content);
935
936 return ASCIICHAT_OK;
937}
#define BUFFER_SIZE_XXLARGE
Extra extra large buffer size (4096 bytes)
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:284
bool ssh_agent_has_key(const public_key_t *public_key)
Check if a public key is already in ssh-agent.
Definition ssh_agent.c:50
asciichat_error_t ssh_agent_add_key(const private_key_t *private_key, const char *key_path)
Add a private key to ssh-agent.
Definition ssh_agent.c:145
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
int prompt_password_simple(const char *prompt, char *password, size_t max_len)
Prompt the user for a password with simple formatting.
Definition password.c:70
bool crypto_regex_extract_pem_base64(const char *file_content, char **base64_data_out)
Definition regex.c:227

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, BUFFER_SIZE_SMALL, BUFFER_SIZE_XXLARGE, crypto_regex_extract_pem_base64(), private_key_t::ed25519, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, ERROR_MEMORY, public_key_t::key, private_key_t::key, private_key_t::key_comment, KEY_TYPE_ED25519, log_debug, log_dev, log_warn, parse_ssh_ed25519_line(), platform_fopen(), platform_getenv(), platform_strdup(), prompt_password_simple(), private_key_t::public_key, SAFE_FREE, SAFE_MALLOC, SAFE_REALLOC, safe_snprintf(), SAFE_STRNCPY, SET_ERRNO, ssh_agent_add_key(), ssh_agent_has_key(), public_key_t::type, private_key_t::type, private_key_t::use_ssh_agent, and validate_ssh_key_file().

Referenced by parse_private_key().

◆ private_key_to_x25519()

asciichat_error_t private_key_to_x25519 ( const private_key_t *  key,
uint8_t  x25519_sk[32] 
)

#include <keys.h>

Convert private key to X25519 for Diffie-Hellman key exchange.

Parameters
keyPrivate key to convert (must not be NULL)
x25519_skOutput buffer for X25519 private key (32 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Converts private key to X25519 format for key exchange:

  • Ed25519 → X25519: Uses libsodium conversion function (crypto_sign_ed25519_sk_to_curve25519)
  • X25519: Passthrough (key is already in X25519 format)
Note
Conversion: Ed25519 keys are converted using libsodium's conversion function. Conversion is mathematically safe (same curve, different representation).
Ed25519 keys: Extracts 32-byte seed from Ed25519 key (first 32 bytes of ed25519 union). Converts seed to X25519 scalar using libsodium.
X25519 passthrough: If key is already X25519, key is copied directly.
Warning
All keys must be 32 bytes. Function validates key size before conversion.

Definition at line 493 of file keys.c.

493 {
494 if (!key || !x25519_sk) {
495 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private_key_to_x25519");
496 }
497
498 if (key->type == KEY_TYPE_X25519) {
499 // Passthrough for X25519 keys
500 memcpy(x25519_sk, key->key.x25519, 32);
501 return ASCIICHAT_OK;
502 }
503
504 if (key->type == KEY_TYPE_ED25519) {
505 // Convert Ed25519 to X25519 (Ed25519 private key is 64 bytes: seed + public)
506 return ed25519_to_x25519_private(key->key.ed25519, x25519_sk);
507 }
508
509 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
510}
uint8_t x25519[32]
Definition key_types.h:95

References ASCIICHAT_OK, private_key_t::ed25519, ed25519_to_x25519_private(), ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, private_key_t::key, KEY_TYPE_ED25519, KEY_TYPE_X25519, SET_ERRNO, private_key_t::type, and private_key_t::x25519.

◆ public_key_to_x25519()

asciichat_error_t public_key_to_x25519 ( const public_key_t *  key,
uint8_t  x25519_pk[32] 
)

#include <keys.h>

Convert public key to X25519 for Diffie-Hellman key exchange.

Parameters
keyPublic key to convert (must not be NULL)
x25519_pkOutput buffer for X25519 public key (32 bytes)
Returns
ASCIICHAT_OK on success, error code on failure

Converts public key to X25519 format for key exchange:

  • Ed25519 → X25519: Uses libsodium conversion function (crypto_sign_ed25519_pk_to_curve25519)
  • X25519: Passthrough (key is already in X25519 format)
  • GPG: Already derived to X25519 during GPG parsing
Note
Conversion: Ed25519 keys are converted using libsodium's conversion function. Conversion is mathematically safe (same curve, different representation).
X25519 passthrough: If key is already X25519, key is copied directly.
GPG keys: GPG keys are already converted to X25519 during parsing. No additional conversion needed.
Warning
All keys must be 32 bytes. Function validates key size before conversion.

Definition at line 474 of file keys.c.

474 {
475 if (!key || !x25519_pk) {
476 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public_key_to_x25519");
477 }
478
479 if (key->type == KEY_TYPE_X25519) {
480 // Passthrough for X25519 keys
481 memcpy(x25519_pk, key->key, 32);
482 return ASCIICHAT_OK;
483 }
484
485 if (key->type == KEY_TYPE_ED25519) {
486 // Convert Ed25519 to X25519
487 return ed25519_to_x25519_public(key->key, x25519_pk);
488 }
489
490 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
491}

References ASCIICHAT_OK, ed25519_to_x25519_public(), ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, public_key_t::key, KEY_TYPE_ED25519, KEY_TYPE_X25519, SET_ERRNO, and public_key_t::type.

◆ validate_gpg_key_format()

asciichat_error_t validate_gpg_key_format ( const char *  gpg_key_text)

#include <gpg_keys.h>

Validate GPG key format and structure.

Validate GPG key format.

Parameters
gpg_key_textGPG key text to validate (must not be NULL)
Returns
ASCIICHAT_OK if valid, error code on failure

Validates GPG key format before parsing. Checks for correct armored format and basic structure.

Note
Format validation: Checks for correct armored format markers. Validates basic GPG key structure.
Use case: Called before parsing to ensure key format is valid. Returns error early if format is invalid.
Warning
GPG support is currently disabled. Function may not work until GPG support is re-enabled.
Parameters
key_textGPG key text to validate (must not be NULL)
Returns
ASCIICHAT_OK if valid, error code on failure

Validates GPG key format by checking for correct armored format markers. Expects "-----BEGIN PGP PUBLIC KEY BLOCK-----" and "-----END PGP PUBLIC KEY BLOCK-----".

Note
Format validation: Checks for correct GPG armored format markers. Validates basic GPG key structure.
GPG format: Expects PEM-armored GPG key format. Format: "-----BEGIN PGP PUBLIC KEY BLOCK-----" ... "-----END PGP PUBLIC KEY BLOCK-----"
Warning
GPG support: GPG key validation may not work until GPG support is re-enabled.

Definition at line 184 of file keys_validation.c.

184 {
185 if (!key_text) {
186 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_text=%p", key_text);
187 return ERROR_INVALID_PARAM;
188 }
189
190 // Check for GPG armor header
191 if (strncmp(key_text, "-----BEGIN PGP", 14) != 0) {
192 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG key does not start with armor header");
193 return ERROR_CRYPTO_KEY;
194 }
195
196 // Check for GPG armor footer
197 if (strstr(key_text, "-----END PGP") == NULL) {
198 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG key does not contain armor footer");
199 return ERROR_CRYPTO_KEY;
200 }
201
202 // Additional GPG key validation
203 // Validate armor format (should have valid armor headers)
204 bool has_version_header = (strstr(key_text, "Version:") != NULL);
205 bool has_charset_header = (strstr(key_text, "Charset:") != NULL || strstr(key_text, "Version:") != NULL);
206
207 if (!has_version_header && !has_charset_header) {
208 // Missing common armor headers - might be incomplete
209 log_warn("GPG armor missing version/charset headers - key might be incomplete");
210 }
211
212 // Validate base64 content (keys must have non-empty body between headers and footer)
213 const char *start_pos = strstr(key_text, "\n\n");
214 const char *end_pos = strstr(key_text, "\n-----END");
215 if (start_pos && end_pos && start_pos < end_pos) {
216 // -2 for \n\n at start_pos; check for underflow before casting to size_t
217 ptrdiff_t diff = end_pos - start_pos - 2;
218 if (diff <= 0) {
219 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG key has empty or malformed body content");
220 return ERROR_CRYPTO_KEY;
221 }
222 }
223
224 return ASCIICHAT_OK;
225}

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

◆ validate_key_security()

asciichat_error_t validate_key_security ( const char *  key_path)

#include <keys_validation.h>

Validate key permissions and security.

Parameters
key_pathPath to key file (must not be NULL)
Returns
ASCIICHAT_OK if secure, error code on failure

Validates key file permissions and performs security checks. Combines permission validation with additional security checks.

Note
Permission checking: Validates file permissions using validate_key_permissions(). Checks that file has restrictive permissions (0600).
Security checks: Additional security checks would include:
  • Weak key pattern detection
  • Key strength validation
  • Known weak key detection These are not yet fully implemented (TODOs in implementation).
Warning
Security checks incomplete: Additional security checks (weak keys, etc.) are not yet fully implemented. Function currently only checks permissions.

Definition at line 131 of file keys_validation.c.

131 {
132 if (!key_path) {
133 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p", key_path);
134 return ERROR_INVALID_PARAM;
135 }
136
137 // Check file permissions
139 if (perm_result != ASCIICHAT_OK) {
140 return perm_result;
141 }
142
143 // Additional security checks have been implemented in check_key_strength()
144 // and check_key_patterns() functions. File permissions check is the critical one.
145 // Known weak keys database would require periodic updates and is beyond scope.
146
147 return ASCIICHAT_OK;
148}
asciichat_error_t platform_validate_key_file_permissions(const char *key_path)
Validate that a cryptographic key file has appropriate permissions.

References ASCIICHAT_OK, ERROR_INVALID_PARAM, platform_validate_key_file_permissions(), and SET_ERRNO.

◆ validate_private_key()

asciichat_error_t validate_private_key ( const private_key_t *  key)

#include <keys_validation.h>

Validate a private key structure.

Parameters
keyPrivate key to validate (must not be NULL)
Returns
ASCIICHAT_OK if valid, error code on failure

Validates private key structure by checking:

  • Key type is valid (Ed25519 or X25519)
  • Key data is not all zeros
  • Comment length is within limits
Note
Key type validation: Checks that key type is one of: KEY_TYPE_ED25519 or KEY_TYPE_X25519 GPG keys are NOT supported for private keys (only public).
Key size: Ed25519 private keys are 64 bytes (seed + public). X25519 private keys are 32 bytes. Function validates key size based on key type.
Zero key check: Rejects keys with all-zero key data. Zero keys are invalid and should not be used.
Comment validation: Checks that comment length is less than MAX_COMMENT_LEN.
Warning
Zero keys: Keys with all-zero data are rejected as invalid.

Definition at line 69 of file keys_validation.c.

69 {
70 if (!key) {
71 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p", key);
73 }
74
75 // Check key type
76 if (key->type == KEY_TYPE_UNKNOWN) {
77 SET_ERRNO(ERROR_CRYPTO_KEY, "Private key type is unknown");
78 return ERROR_CRYPTO_KEY;
79 }
80
81 if (key->type != KEY_TYPE_ED25519 && key->type != KEY_TYPE_X25519) {
82 SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported private key type: %d", key->type);
83 return ERROR_CRYPTO_KEY;
84 }
85
86 // Check key data is not all zeros
87 bool is_zero = true;
88 size_t key_size = (key->type == KEY_TYPE_ED25519) ? 64 : 32;
89
90 for (size_t i = 0; i < key_size; i++) {
91 if (key->key.ed25519[i] != 0) {
92 is_zero = false;
93 break;
94 }
95 }
96
97 if (is_zero) {
98 SET_ERRNO(ERROR_CRYPTO_KEY, "Private key data is all zeros");
99 return ERROR_CRYPTO_KEY;
100 }
101
102 // Check comment length
103 if (strlen(key->key_comment) >= MAX_COMMENT_LEN) {
104 SET_ERRNO(ERROR_CRYPTO_KEY, "Private key comment too long: %zu (maximum %d)", strlen(key->key_comment),
105 MAX_COMMENT_LEN - 1);
106 return ERROR_CRYPTO_KEY;
107 }
108
109 // Validate comment is valid UTF-8 (user-provided text in key files)
110 if (key->key_comment[0] != '\0' && !utf8_is_valid(key->key_comment)) {
111 SET_ERRNO(ERROR_CRYPTO_KEY, "Private key comment contains invalid UTF-8 sequence");
112 return ERROR_CRYPTO_KEY;
113 }
114
115 return ASCIICHAT_OK;
116}
bool utf8_is_valid(const char *str)
Check if a string is valid UTF-8.
Definition utf8.c:156

References ASCIICHAT_OK, private_key_t::ed25519, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, private_key_t::key, private_key_t::key_comment, KEY_TYPE_ED25519, KEY_TYPE_UNKNOWN, KEY_TYPE_X25519, MAX_COMMENT_LEN, SET_ERRNO, private_key_t::type, and utf8_is_valid().

◆ validate_public_key()

asciichat_error_t validate_public_key ( const public_key_t *  key)

#include <keys_validation.h>

Validate a public key structure.

Parameters
keyPublic key to validate (must not be NULL)
Returns
ASCIICHAT_OK if valid, error code on failure

Validates public key structure by checking:

  • Key type is valid (Ed25519, X25519, or GPG)
  • Key data is not all zeros
  • Comment length is within limits
Note
Key type validation: Checks that key type is one of: KEY_TYPE_ED25519, KEY_TYPE_X25519, or KEY_TYPE_GPG
Zero key check: Rejects keys with all-zero key data. Zero keys are invalid and should not be used.
Comment validation: Checks that comment length is less than MAX_COMMENT_LEN. Comments can be up to 255 characters (including null terminator).
Warning
Zero keys: Keys with all-zero data are rejected as invalid.

Definition at line 23 of file keys_validation.c.

23 {
24 if (!key) {
25 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p", key);
27 }
28
29 // Check key type
30 if (key->type == KEY_TYPE_UNKNOWN) {
31 SET_ERRNO(ERROR_CRYPTO_KEY, "Key type is unknown");
32 return ERROR_CRYPTO_KEY;
33 }
34
35 if (key->type != KEY_TYPE_ED25519 && key->type != KEY_TYPE_X25519 && key->type != KEY_TYPE_GPG) {
36 SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type: %d", key->type);
37 return ERROR_CRYPTO_KEY;
38 }
39
40 // Check key data is not all zeros
41 bool is_zero = true;
42 for (int i = 0; i < 32; i++) {
43 if (key->key[i] != 0) {
44 is_zero = false;
45 break;
46 }
47 }
48
49 if (is_zero) {
50 SET_ERRNO(ERROR_CRYPTO_KEY, "Key data is all zeros");
51 return ERROR_CRYPTO_KEY;
52 }
53
54 // Check comment length
55 if (strlen(key->comment) >= MAX_COMMENT_LEN) {
56 SET_ERRNO(ERROR_CRYPTO_KEY, "Key comment too long: %zu (maximum %d)", strlen(key->comment), MAX_COMMENT_LEN - 1);
57 return ERROR_CRYPTO_KEY;
58 }
59
60 // Validate comment is valid UTF-8 (user-provided text in key files)
61 if (key->comment[0] != '\0' && !utf8_is_valid(key->comment)) {
62 SET_ERRNO(ERROR_CRYPTO_KEY, "Key comment contains invalid UTF-8 sequence");
63 return ERROR_CRYPTO_KEY;
64 }
65
66 return ASCIICHAT_OK;
67}

References ASCIICHAT_OK, public_key_t::comment, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, public_key_t::key, KEY_TYPE_ED25519, KEY_TYPE_GPG, KEY_TYPE_UNKNOWN, KEY_TYPE_X25519, MAX_COMMENT_LEN, SET_ERRNO, public_key_t::type, and utf8_is_valid().

◆ validate_ssh_key_file()

asciichat_error_t validate_ssh_key_file ( const char *  key_path)

#include <keys.h>

Validate SSH key file before parsing.

Validate SSH key file permissions and format.

Parameters
key_pathPath to SSH key file (must not be NULL)
Returns
ASCIICHAT_OK if valid, error code on failure

Validates SSH key file before parsing. Checks:

  • File exists and is readable
  • File has valid SSH private key header ("-----BEGIN OPENSSH PRIVATE KEY-----")
  • File permissions are appropriate (warns if overly permissive)
Note
Permission checking: Checks file permissions on Unix systems. Warns if file has world-readable permissions but does not fail validation.
File header: Validates that file starts with correct SSH private key header. Returns error if header is missing or invalid.
Platform-specific: Permission checking is Unix-specific. Windows does not have Unix-style permissions.
Warning
File permissions: Private key files should have restrictive permissions (0600). Function warns but does not fail on overly permissive permissions.

Definition at line 939 of file ssh_keys.c.

939 {
940 if (!key_path) {
941 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p", key_path);
942 }
943
944 if (!path_looks_like_path(key_path)) {
945 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid SSH key path: %s", key_path);
946 }
947
948 char *normalized_path = NULL;
949 asciichat_error_t path_result = path_validate_user_path(key_path, PATH_ROLE_KEY_PRIVATE, &normalized_path);
950 if (path_result != ASCIICHAT_OK) {
951 SAFE_FREE(normalized_path);
952 return path_result;
953 }
954
955 // Check if file exists and is readable
956 FILE *test_file = platform_fopen("file_stream", normalized_path, "r");
957 if (test_file == NULL) {
958 SAFE_FREE(normalized_path);
959 return SET_ERRNO(ERROR_CRYPTO_KEY, "Cannot read key file: %s", key_path);
960 }
961
962 // Check if this is an SSH key file or GPG armored key by looking for the header
963 char header[BUFFER_SIZE_SMALL];
964 bool is_valid_key_file = false;
965 if (fgets(header, sizeof(header), test_file) != NULL) {
966 if (strstr(header, "BEGIN OPENSSH PRIVATE KEY") != NULL || strstr(header, "BEGIN RSA PRIVATE KEY") != NULL ||
967 strstr(header, "BEGIN EC PRIVATE KEY") != NULL || strstr(header, "BEGIN PGP PRIVATE KEY") != NULL ||
968 strstr(header, "BEGIN PGP SECRET KEY") != NULL) {
969 is_valid_key_file = true;
970 }
971 }
972 (void)fclose(test_file);
973
974 if (!is_valid_key_file) {
975 SAFE_FREE(normalized_path);
976 return SET_ERRNO(ERROR_CRYPTO_KEY, "File is not a valid SSH or GPG key: %s", key_path);
977 }
978
979 // Check permissions for SSH key files (should be 600 or 400)
980#ifndef _WIN32
981 struct stat st;
982 if (stat(normalized_path, &st) == 0) {
983 if ((st.st_mode & SSH_KEY_PERMISSIONS_MASK) != 0) {
984 log_error("SSH key file %s has overly permissive permissions: %o", key_path, st.st_mode & 0777);
985 log_error("Run 'chmod 600 %s' to fix this", key_path);
986 SAFE_FREE(normalized_path);
987 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key file has overly permissive permissions: %s", key_path);
988 }
989 }
990#endif
991
992 SAFE_FREE(normalized_path);
993 return ASCIICHAT_OK;
994}
#define SSH_KEY_PERMISSIONS_MASK

References ASCIICHAT_OK, BUFFER_SIZE_SMALL, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, log_error, path_looks_like_path(), PATH_ROLE_KEY_PRIVATE, path_validate_user_path(), platform_fopen(), SAFE_FREE, SET_ERRNO, and SSH_KEY_PERMISSIONS_MASK.

Referenced by client_crypto_init(), and parse_ssh_private_key().

◆ validate_ssh_key_format()

asciichat_error_t validate_ssh_key_format ( const char *  key_text)

#include <keys_validation.h>

Validate SSH key format.

Parameters
key_textSSH key text to validate (must not be NULL)
Returns
ASCIICHAT_OK if valid, error code on failure

Validates SSH key format by checking for correct format markers. Expects "ssh-ed25519 " prefix followed by base64-encoded key.

Note
Format validation: Checks that key text starts with "ssh-ed25519 ". Validates basic SSH key format structure.
Key type: Only Ed25519 SSH keys are validated. Other key types (RSA, ECDSA) will fail validation.
Warning
Key format: Must start with "ssh-ed25519 ". Other SSH key types (RSA, ECDSA) are NOT supported.

Definition at line 154 of file keys_validation.c.

154 {
155 if (!key_text) {
156 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_text=%p", key_text);
157 return ERROR_INVALID_PARAM;
158 }
159
160 // Check for SSH key format
161 if (strncmp(key_text, "ssh-ed25519 ", 12) != 0) {
162 SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key does not start with 'ssh-ed25519 '");
163 return ERROR_CRYPTO_KEY;
164 }
165
166 // Check for base64 data
167 const char *base64_start = key_text + 12;
168 while (*base64_start == ' ' || *base64_start == '\t') {
169 base64_start++;
170 }
171
172 if (*base64_start == '\0' || *base64_start == '\n' || *base64_start == '\r') {
173 SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key has no base64 data");
174 return ERROR_CRYPTO_KEY;
175 }
176
177 // Additional comprehensive SSH key validation (base64, blob structure, etc.)
178 // would require full OpenSSH format parsing. Basic format check above is sufficient
179 // to catch malformed keys. Full validation happens during key loading.
180
181 return ASCIICHAT_OK;
182}

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ validate_x25519_key_format()

asciichat_error_t validate_x25519_key_format ( const char *  key_hex)

#include <keys_validation.h>

Validate X25519 key format.

Parameters
key_hexX25519 key in hex format (must not be NULL)
Returns
ASCIICHAT_OK if valid, error code on failure

Validates X25519 key format by checking hex string format. Expects 64 hex characters (32 bytes).

Note
Format validation: Checks that key is 64 hex characters (32 bytes). Validates hex character format (0-9, a-f, A-F).
Key size: X25519 keys must be exactly 64 hex characters (32 bytes). Returns error if length doesn't match or contains invalid characters.
Warning
Key format: Must be exactly 64 hex characters. Invalid characters or wrong length will fail validation.

Definition at line 227 of file keys_validation.c.

227 {
228 if (!key_hex) {
229 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_hex=%p", key_hex);
230 return ERROR_INVALID_PARAM;
231 }
232
233 // Check hex string length (32 bytes = 64 hex characters)
234 size_t hex_len = strlen(key_hex);
235 if (hex_len != 64) {
236 SET_ERRNO(ERROR_CRYPTO_KEY, "X25519 key has invalid length: %zu (expected 64)", hex_len);
237 return ERROR_CRYPTO_KEY;
238 }
239
240 // Check that all characters are valid hex
241 for (size_t i = 0; i < hex_len; i++) {
242 if (!isxdigit(key_hex[i])) {
243 SET_ERRNO(ERROR_CRYPTO_KEY, "X25519 key contains invalid hex character at position %zu: '%c'", i, key_hex[i]);
244 return ERROR_CRYPTO_KEY;
245 }
246 }
247
248 return ASCIICHAT_OK;
249}

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

Variable Documentation

◆ comment

char public_key_t::comment[256]

◆ ed25519 [1/2]

◆ [] [2/2]

uint8_t { ... } ::ed25519[64]

Ed25519 seed (32) + public key (32) = 64 bytes

Definition at line 94 of file key_types.h.

◆ gpg_keygrip

char private_key_t::gpg_keygrip[64]

GPG keygrip (40 hex chars + null) for gpg-agent signing

Definition at line 101 of file key_types.h.

Referenced by ed25519_sign_message(), and parse_private_key().

◆ key [1/2]

◆ [union] [2/2]

◆ key_comment

char private_key_t::key_comment[256]

SSH key comment (for agent identification)

Definition at line 100 of file key_types.h.

Referenced by client_crypto_init(), ed25519_sign_message(), parse_private_key(), parse_ssh_private_key(), and validate_private_key().

◆ public_key

uint8_t private_key_t::public_key[32]

◆ type [1/2]

◆ type [2/2]

◆ use_gpg_agent

bool private_key_t::use_gpg_agent

If true, use GPG agent for signing (currently disabled)

Definition at line 98 of file key_types.h.

Referenced by ed25519_sign_message(), and parse_private_key().

◆ use_ssh_agent

bool private_key_t::use_ssh_agent

If true, use SSH agent for signing (key stays in agent)

Definition at line 97 of file key_types.h.

Referenced by ed25519_sign_message(), parse_private_key(), and parse_ssh_private_key().

◆ x25519 [1/2]

uint8_t private_key_t::x25519[32]

X25519 private key (32 bytes)

Definition at line 95 of file key_types.h.

Referenced by private_key_to_x25519().

◆ [] [2/2]

uint8_t { ... } ::x25519[32]

X25519 private key (32 bytes)

Definition at line 95 of file key_types.h.