ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
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 validate_key_permissions (const char *key_path)
 Validate key file permissions.
 
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:46
asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out)
Parse SSH private key from file.
Definition keys.c:108
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
Parse SSH/GPG public key from any format (returns first key only)
Definition keys.c:24
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:343

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:48
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:178

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

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:274
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:210

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.

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

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

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

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:1050
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:1037

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

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:1163
#define log_info(...)
Log an INFO message.

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:

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

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 526 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 116 of file https_keys.c.

116 {
117 if (!username || !url_out) {
118 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
119 return ERROR_INVALID_PARAM;
120 }
121
122 if (url_size < 64) {
123 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
124 return ERROR_INVALID_PARAM;
125 }
126
127 // Strip .gpg suffix if user already included it (e.g., "github:zfogg.gpg")
128 char clean_username[256];
129 SAFE_STRNCPY(clean_username, username, sizeof(clean_username) - 1);
130 size_t len = strlen(clean_username);
131 if (len > 4 && strcmp(clean_username + len - 4, ".gpg") == 0) {
132 clean_username[len - 4] = '\0'; // Remove .gpg suffix
133 }
134
135 // Construct GitHub GPG keys URL: https://github.com/username.gpg
136 int result = safe_snprintf(url_out, url_size, "https://github.com/%s.gpg", clean_username);
137 if (result < 0 || result >= (int)url_size) {
138 SET_ERRNO(ERROR_STRING, "Failed to construct GitHub GPG URL");
139 return ERROR_STRING;
140 }
141
142 return ASCIICHAT_OK;
143}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
@ ERROR_STRING
@ ERROR_INVALID_PARAM
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.

References ASCIICHAT_OK, 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 74 of file https_keys.c.

74 {
75 if (!username || !url_out) {
76 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
78 }
79
80 if (url_size < 64) {
81 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
83 }
84
85 // Construct GitHub SSH keys URL: https://github.com/username.keys
86 int result = safe_snprintf(url_out, url_size, "https://github.com/%s.keys", username);
87 if (result < 0 || result >= (int)url_size) {
88 SET_ERRNO(ERROR_STRING, "Failed to construct GitHub SSH URL");
89 return ERROR_STRING;
90 }
91
92 return ASCIICHAT_OK;
93}

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

145 {
146 if (!username || !url_out) {
147 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
148 return ERROR_INVALID_PARAM;
149 }
150
151 if (url_size < 64) {
152 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
153 return ERROR_INVALID_PARAM;
154 }
155
156 // Strip .gpg suffix if user already included it (e.g., "gitlab:zfogg.gpg")
157 char clean_username[256];
158 SAFE_STRNCPY(clean_username, username, sizeof(clean_username) - 1);
159 size_t len = strlen(clean_username);
160 if (len > 4 && strcmp(clean_username + len - 4, ".gpg") == 0) {
161 clean_username[len - 4] = '\0'; // Remove .gpg suffix
162 }
163
164 // Construct GitLab GPG keys URL: https://gitlab.com/username.gpg
165 int result = safe_snprintf(url_out, url_size, "https://gitlab.com/%s.gpg", clean_username);
166 if (result < 0 || result >= (int)url_size) {
167 SET_ERRNO(ERROR_STRING, "Failed to construct GitLab GPG URL");
168 return ERROR_STRING;
169 }
170
171 return ASCIICHAT_OK;
172}

References ASCIICHAT_OK, 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 95 of file https_keys.c.

95 {
96 if (!username || !url_out) {
97 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, url_out=%p", username, url_out);
99 }
100
101 if (url_size < 64) {
102 SET_ERRNO(ERROR_INVALID_PARAM, "URL buffer too small: %zu (minimum 64)", url_size);
103 return ERROR_INVALID_PARAM;
104 }
105
106 // Construct GitLab SSH keys URL: https://gitlab.com/username.keys
107 int result = safe_snprintf(url_out, url_size, "https://gitlab.com/%s.keys", username);
108 if (result < 0 || result >= (int)url_size) {
109 SET_ERRNO(ERROR_STRING, "Failed to construct GitLab SSH URL");
110 return ERROR_STRING;
111 }
112
113 return ASCIICHAT_OK;
114}

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 164 of file gpg_keys.c.

164 {
165 if (!gpg_key_text || !is_expired) {
166 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, is_expired=%p", gpg_key_text, is_expired);
167 return ERROR_INVALID_PARAM;
168 }
169
170 // For GPG key expiry checking, we need a key ID
171 // If gpg_key_text is a key ID (8, 16, or 40 hex chars), use it directly
172 // Otherwise, we'd need to parse the GPG armored text (complex)
173 size_t key_len = strlen(gpg_key_text);
174 const char *key_id = gpg_key_text;
175
176 // Check if it looks like a key ID (hex characters only)
177 bool is_hex = true;
178 for (size_t i = 0; i < key_len; i++) {
179 if (!((gpg_key_text[i] >= '0' && gpg_key_text[i] <= '9') || (gpg_key_text[i] >= 'A' && gpg_key_text[i] <= 'F') ||
180 (gpg_key_text[i] >= 'a' && gpg_key_text[i] <= 'f'))) {
181 is_hex = false;
182 break;
183 }
184 }
185
186 // Only support key ID format (8, 16, or 40 hex chars)
187 if (!is_hex || (key_len != 8 && key_len != 16 && key_len != 40)) {
188 log_warn("check_gpg_key_expiry: Input is not a key ID format (expected 8/16/40 hex chars)");
189 *is_expired = false; // Assume not expired if we can't check
190 return ASCIICHAT_OK;
191 }
192
193 // Use gpg --list-keys with colon-separated output to check expiry
194 char cmd[512];
195 safe_snprintf(cmd, sizeof(cmd), "gpg --list-keys --with-colons %s 2>/dev/null", key_id);
196
197 FILE *fp = SAFE_POPEN(cmd, "r");
198 if (!fp) {
199 log_error("Failed to run gpg --list-keys for key %s", key_id);
200 *is_expired = false; // Assume not expired if we can't check
201 return ASCIICHAT_OK;
202 }
203
204 char line[1024];
205 bool found_pub = false;
206 *is_expired = false;
207
208 // Parse colon-separated output
209 // Format: pub:trust:keylen:algo:keyid:creation:expiry:...
210 // Field 6 is expiry timestamp (seconds since epoch), or empty if no expiry
211 while (fgets(line, sizeof(line), fp)) {
212 if (strncmp(line, "pub:", 4) == 0) {
213 found_pub = true;
214
215 // Split line by colons
216 char *fields[12];
217 int field_count = 0;
218 char *ptr = line;
219 char *field_start = ptr;
220
221 while (*ptr && field_count < 12) {
222 if (*ptr == ':' || *ptr == '\n') {
223 *ptr = '\0';
224 fields[field_count++] = field_start;
225 field_start = ptr + 1;
226 }
227 ptr++;
228 }
229
230 // Field 6 is expiry date (index 6)
231 if (field_count >= 7 && strlen(fields[6]) > 0) {
232 // Parse expiry timestamp
233 long expiry_timestamp = atol(fields[6]);
234 time_t now = time(NULL);
235
236 if (expiry_timestamp > 0 && expiry_timestamp < now) {
237 *is_expired = true;
238 log_warn("GPG key %s has expired (expiry: %ld, now: %ld)", key_id, expiry_timestamp, (long)now);
239 } else if (expiry_timestamp > 0) {
240 log_debug("GPG key %s expires at timestamp %ld (valid)", key_id, expiry_timestamp);
241 } else {
242 log_debug("GPG key %s has no expiration date", key_id);
243 }
244 } else {
245 log_debug("GPG key %s has no expiration date (field empty)", key_id);
246 }
247
248 break; // Found the pub line, stop parsing
249 }
250 }
251
252 SAFE_PCLOSE(fp);
253
254 if (!found_pub) {
255 log_warn("Could not find GPG key %s in keyring", key_id);
256 *is_expired = false; // Assume not expired if key not found
257 }
258
259 return ASCIICHAT_OK;
260}
#define SAFE_PCLOSE
Definition gpg_keys.c:25
#define SAFE_POPEN
Definition gpg_keys.c:24
#define log_warn(...)
Log a WARN message.
#define log_debug(...)
Log a DEBUG message.

References ASCIICHAT_OK, ERROR_INVALID_PARAM, log_debug, log_error, log_warn, SAFE_PCLOSE, SAFE_POPEN, 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 110 of file keys_validation.c.

110 {
111 if (!key || !is_expired) {
112 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, is_expired=%p", key, is_expired);
113 return ERROR_INVALID_PARAM;
114 }
115
116 // For now, we don't implement key expiry checking
117 // This would require parsing key creation dates and expiration times
118 *is_expired = false;
119
120 return ASCIICHAT_OK;
121}

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 435 of file keys_validation.c.

436 {
437 if (!key || !fingerprint || !matches) {
438 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, fingerprint=%p, matches=%p", key, fingerprint, matches);
439 return ERROR_INVALID_PARAM;
440 }
441
442 *matches = false;
443
444 // Generate fingerprint for the key
445 uint8_t key_fingerprint[32];
446 asciichat_error_t fingerprint_result = generate_key_fingerprint(key, key_fingerprint, 32);
447 if (fingerprint_result != ASCIICHAT_OK) {
448 return fingerprint_result;
449 }
450
451 // Compare fingerprints
452 size_t compare_len = (fingerprint_len < 32) ? fingerprint_len : 32;
453 if (sodium_memcmp(key_fingerprint, fingerprint, compare_len) == 0) {
454 *matches = true;
455 }
456
457 return ASCIICHAT_OK;
458}
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 320 of file keys_validation.c.

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

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

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 414 of file keys_validation.c.

414 {
415 if (!key1 || !key2 || !are_equal) {
416 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key1=%p, key2=%p, are_equal=%p", key1, key2, are_equal);
417 return ERROR_INVALID_PARAM;
418 }
419
420 *are_equal = false;
421
422 // Compare key types
423 if (key1->type != key2->type) {
424 return ASCIICHAT_OK;
425 }
426
427 // Compare key data using constant-time comparison
428 if (sodium_memcmp(key1->key, key2->key, 32) == 0) {
429 *are_equal = true;
430 }
431
432 return ASCIICHAT_OK;
433}
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 1067 of file ssh_keys.c.

1068 {
1069 if (!key || !message || !signature) {
1070 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, message=%p, signature=%p", key, message,
1071 signature);
1072 }
1073
1074 if (key->type != KEY_TYPE_ED25519) {
1075 return SET_ERRNO(ERROR_CRYPTO_KEY, "Key is not an Ed25519 key");
1076 }
1077
1078 // If using GPG agent, delegate signing to GPG agent
1079 if (key->use_gpg_agent) {
1080 log_debug("Using GPG agent for Ed25519 signing (keygrip: %.40s)", key->gpg_keygrip);
1081
1082 // Connect to GPG agent
1083 int agent_sock = gpg_agent_connect();
1084 log_debug("GPG agent connection result: %d", agent_sock);
1085
1086 if (agent_sock < 0) {
1087 log_info("GPG agent not available, falling back to gpg --detach-sign for signing");
1088
1089 // Extract GPG key ID from key comment ("GPG key <key_id>")
1090 const char *key_id_prefix = "GPG key ";
1091 const char *key_id = NULL;
1092 if (strncmp(key->key_comment, key_id_prefix, strlen(key_id_prefix)) == 0) {
1093 key_id = key->key_comment + strlen(key_id_prefix);
1094 }
1095
1096 log_debug("Extracted GPG key ID: %s (len=%zu)", key_id ? key_id : "NULL", key_id ? strlen(key_id) : 0);
1097
1098 if (!key_id || strlen(key_id) != 16) {
1099 return SET_ERRNO(ERROR_CRYPTO, "Cannot extract GPG key ID from comment for fallback signing");
1100 }
1101
1102 // Use gpg --detach-sign fallback
1103 log_debug("Calling gpg_sign_detached_ed25519 with key %s", key_id);
1104 int result = gpg_sign_detached_ed25519(key_id, message, message_len, signature);
1105 log_debug("gpg_sign_detached_ed25519 result: %d", result);
1106
1107 if (result != 0) {
1108 return SET_ERRNO(ERROR_CRYPTO, "GPG fallback signing failed (both agent and gpg command failed)");
1109 }
1110
1111 log_info("Successfully signed message with gpg --detach-sign fallback (64 bytes)");
1112 return ASCIICHAT_OK;
1113 }
1114
1115 log_debug("Using GPG agent for signing");
1116
1117 // Sign the message using GPG agent
1118 size_t sig_len = 0;
1119 int result = gpg_agent_sign(agent_sock, key->gpg_keygrip, message, message_len, signature, &sig_len);
1120
1121 // Disconnect from agent
1122 gpg_agent_disconnect(agent_sock);
1123
1124 if (result != 0) {
1125 return SET_ERRNO(ERROR_CRYPTO, "GPG agent signing failed");
1126 }
1127
1128 if (sig_len != 64) {
1129 return SET_ERRNO(ERROR_CRYPTO, "GPG agent returned invalid signature length: %zu (expected 64)", sig_len);
1130 }
1131
1132 log_info("Successfully signed message with GPG agent (64 bytes)");
1133 return ASCIICHAT_OK;
1134 }
1135
1136 // If using SSH agent, delegate signing to SSH agent
1137 if (key->use_ssh_agent) {
1138 log_debug("Using SSH agent for Ed25519 signing");
1139
1140 // Create a public_key_t from the stored public key
1141 public_key_t pub_key = {0};
1142 pub_key.type = KEY_TYPE_ED25519;
1143 memcpy(pub_key.key, key->public_key, 32);
1144
1145 // Sign using SSH agent
1146 asciichat_error_t result = ssh_agent_sign(&pub_key, message, message_len, signature);
1147 if (result != ASCIICHAT_OK) {
1148 return result; // Error already set by ssh_agent_sign
1149 }
1150
1151 log_info("Successfully signed message with SSH agent (64 bytes)");
1152 return ASCIICHAT_OK;
1153 }
1154
1155 // Sign the message with Ed25519 (in-memory key)
1156 if (crypto_sign_detached(signature, NULL, message, message_len, key->key.ed25519) != 0) {
1157 return SET_ERRNO(ERROR_CRYPTO, "Failed to sign message with Ed25519");
1158 }
1159
1160 return ASCIICHAT_OK;
1161}
int gpg_agent_connect(void)
Connect to gpg-agent.
Definition agent.c:267
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:345
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:180
void gpg_agent_disconnect(int sock)
Disconnect from gpg-agent.
Definition agent.c:337
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:295
@ ERROR_CRYPTO_KEY
Definition error_codes.h:89
@ ERROR_CRYPTO
Definition error_codes.h:88
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::@1 key

References ASCIICHAT_OK, private_key_t::ed25519, ERROR_CRYPTO, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, 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, log_info, 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 1050 of file ssh_keys.c.

1050 {
1051 if (!ed25519_sk || !x25519_sk) {
1052 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ed25519_sk=%p, x25519_sk=%p", ed25519_sk, x25519_sk);
1053 }
1054
1055 // Convert Ed25519 private key to X25519 private key
1056 if (crypto_sign_ed25519_sk_to_curve25519(x25519_sk, ed25519_sk) != 0) {
1057 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 private key to X25519");
1058 }
1059
1060 return ASCIICHAT_OK;
1061}

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 1037 of file ssh_keys.c.

1037 {
1038 if (!ed25519_pk || !x25519_pk) {
1039 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: ed25519_pk=%p, x25519_pk=%p", ed25519_pk, x25519_pk);
1040 }
1041
1042 // Convert Ed25519 public key to X25519 public key
1043 if (crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk) != 0) {
1044 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 public key to X25519");
1045 }
1046
1047 return ASCIICHAT_OK;
1048}

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 1163 of file ssh_keys.c.

1164 {
1165 if (!public_key || !message || !signature) {
1166 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: public_key=%p, message=%p, signature=%p", public_key,
1167 message, signature);
1168 }
1169
1170 // Try standard Ed25519 verification first
1171 if (crypto_sign_verify_detached(signature, message, message_len, public_key) == 0) {
1172 return ASCIICHAT_OK;
1173 }
1174
1175 // If standard verification fails, try GPG fallback (for GPG-signed messages)
1176 // Use provided gpg_key_id if available, otherwise check environment variable (for tests)
1177 const char *key_id_to_use = gpg_key_id;
1178 size_t key_id_len = key_id_to_use ? strlen(key_id_to_use) : 0;
1179 // Accept 8, 16, or 40 character GPG key IDs (short, long, or full fingerprint)
1180 bool valid_key_id = (key_id_len == 8 || key_id_len == 16 || key_id_len == 40);
1181
1182 if (!valid_key_id) {
1183 key_id_to_use = platform_getenv("TEST_GPG_KEY_ID");
1184 key_id_len = key_id_to_use ? strlen(key_id_to_use) : 0;
1185 valid_key_id = (key_id_len == 8 || key_id_len == 16 || key_id_len == 40);
1186 }
1187
1188 if (valid_key_id) {
1189 log_debug("Standard Ed25519 verification failed, trying GPG fallback with key %s", key_id_to_use);
1190 int gpg_result = gpg_verify_detached_ed25519(key_id_to_use, message, message_len, signature);
1191 if (gpg_result == 0) {
1192 log_debug("GPG verification succeeded");
1193 return ASCIICHAT_OK;
1194 }
1195 log_debug("GPG verification also failed");
1196 }
1197
1198 return SET_ERRNO(ERROR_CRYPTO, "Ed25519 signature verification failed (tried both libsodium and GPG)");
1199}
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.

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 95 of file gpg_keys.c.

95 {
96 if (!gpg_key_id || !ed25519_pk) {
97 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_id=%p, ed25519_pk=%p", gpg_key_id, ed25519_pk);
99 }
100
101 // Use gpg_get_public_key() to extract Ed25519 public key from GPG keyring
102 // Note: gpg_key_id should be the key ID (e.g., "7FE90A79F2E80ED3")
103 char keygrip[64];
104 if (gpg_get_public_key(gpg_key_id, ed25519_pk, keygrip) != 0) {
105 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to extract Ed25519 public key from GPG for key ID: %s", gpg_key_id);
106 }
107
108 return ASCIICHAT_OK;
109}
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:251

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 303 of file gpg_keys.c.

303 {
304 if (!gpg_key_text || !comment_out) {
305 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, comment_out=%p", gpg_key_text, comment_out);
306 return ERROR_INVALID_PARAM;
307 }
308
309 if (comment_size == 0) {
310 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid comment size: %zu", comment_size);
311 return ERROR_INVALID_PARAM;
312 }
313
314 // TODO: Implement GPG key comment extraction
315 // This requires parsing the OpenPGP packet structure and extracting user ID packets
316
317 // For now, use a generic comment
318 SAFE_STRNCPY(comment_out, "GPG key", comment_size - 1);
319 return ASCIICHAT_OK;
320}

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 242 of file https_keys.c.

242 {
243 if (!username || !keys_out || !num_keys) {
244 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
245 num_keys);
246 return ERROR_INVALID_PARAM;
247 }
248
249 // Build the GitHub GPG keys URL
250 char url[256];
251 asciichat_error_t url_result = build_github_gpg_url(username, url, sizeof(url));
252 if (url_result != ASCIICHAT_OK) {
253 return url_result;
254 }
255
256 // Fetch the keys using HTTPS
257 char *response_text = NULL;
258 size_t response_len = 0;
259 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
260 if (fetch_result != ASCIICHAT_OK) {
261 return fetch_result;
262 }
263
264 // Parse GPG keys from the response
265 asciichat_error_t parse_result =
266 parse_gpg_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
267
268 // Clean up response text
269 SAFE_FREE(response_text);
270
271 return parse_result;
272}
#define SAFE_FREE(ptr)
Definition common.h:320
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:116
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:408

References ASCIICHAT_OK, 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 315 of file keys.c.

315 {
316 if (!username || !keys_out || !num_keys) {
317 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_github_keys");
318 }
319
320 if (use_gpg) {
321 return fetch_github_gpg_keys(username, keys_out, num_keys);
322 } else {
323 return fetch_github_ssh_keys(username, keys_out, num_keys);
324 }
325}

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 178 of file https_keys.c.

178 {
179 if (!username || !keys_out || !num_keys) {
180 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
181 num_keys);
182 return ERROR_INVALID_PARAM;
183 }
184
185 // Build the GitHub SSH keys URL
186 char url[256];
187 asciichat_error_t url_result = build_github_ssh_url(username, url, sizeof(url));
188 if (url_result != ASCIICHAT_OK) {
189 return url_result;
190 }
191
192 // Fetch the keys using HTTPS
193 char *response_text = NULL;
194 size_t response_len = 0;
195 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
196 if (fetch_result != ASCIICHAT_OK) {
197 return fetch_result;
198 }
199
200 // Parse SSH keys from the response
201 asciichat_error_t parse_result =
202 parse_ssh_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
203
204 // Clean up response text
205 SAFE_FREE(response_text);
206
207 return parse_result;
208}
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:74
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:310

References ASCIICHAT_OK, 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 274 of file https_keys.c.

274 {
275 if (!username || !keys_out || !num_keys) {
276 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
277 num_keys);
278 return ERROR_INVALID_PARAM;
279 }
280
281 // Build the GitLab GPG keys URL
282 char url[256];
283 asciichat_error_t url_result = build_gitlab_gpg_url(username, url, sizeof(url));
284 if (url_result != ASCIICHAT_OK) {
285 return url_result;
286 }
287
288 // Fetch the keys using HTTPS
289 char *response_text = NULL;
290 size_t response_len = 0;
291 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
292 if (fetch_result != ASCIICHAT_OK) {
293 return fetch_result;
294 }
295
296 // Parse GPG keys from the response
297 asciichat_error_t parse_result =
298 parse_gpg_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
299
300 // Clean up response text
301 SAFE_FREE(response_text);
302
303 return parse_result;
304}
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:145

References ASCIICHAT_OK, 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 327 of file keys.c.

327 {
328 if (!username || !keys_out || !num_keys) {
329 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for fetch_gitlab_keys");
330 }
331
332 if (use_gpg) {
333 return fetch_gitlab_gpg_keys(username, keys_out, num_keys);
334 } else {
335 return fetch_gitlab_ssh_keys(username, keys_out, num_keys);
336 }
337}

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 210 of file https_keys.c.

210 {
211 if (!username || !keys_out || !num_keys) {
212 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: username=%p, keys_out=%p, num_keys=%p", username, keys_out,
213 num_keys);
214 return ERROR_INVALID_PARAM;
215 }
216
217 // Build the GitLab SSH keys URL
218 char url[256];
219 asciichat_error_t url_result = build_gitlab_ssh_url(username, url, sizeof(url));
220 if (url_result != ASCIICHAT_OK) {
221 return url_result;
222 }
223
224 // Fetch the keys using HTTPS
225 char *response_text = NULL;
226 size_t response_len = 0;
227 asciichat_error_t fetch_result = https_fetch_keys(url, &response_text, &response_len);
228 if (fetch_result != ASCIICHAT_OK) {
229 return fetch_result;
230 }
231
232 // Parse SSH keys from the response
233 asciichat_error_t parse_result =
234 parse_ssh_keys_from_response(response_text, response_len, keys_out, num_keys, MAX_CLIENTS);
235
236 // Clean up response text
237 SAFE_FREE(response_text);
238
239 return parse_result;
240}
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:95

References ASCIICHAT_OK, 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 266 of file gpg_keys.c.

266 {
267 if (!gpg_key_text || !output) {
268 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, output=%p", gpg_key_text, output);
269 return ERROR_INVALID_PARAM;
270 }
271
272 if (output_size < 64) {
273 SET_ERRNO(ERROR_INVALID_PARAM, "Output buffer too small: %zu (minimum 64)", output_size);
274 return ERROR_INVALID_PARAM;
275 }
276
277 // Extract key ID for display
278 uint8_t key_id[8];
279 asciichat_error_t key_id_result = get_gpg_key_id(gpg_key_text, key_id);
280 if (key_id_result != ASCIICHAT_OK) {
281 // Fallback to generic GPG key display
282 SAFE_STRNCPY(output, "GPG key (key ID extraction failed)", output_size - 1);
283 return ASCIICHAT_OK;
284 }
285
286 // Format as hex key ID
287 char hex_key_id[17];
288 for (int i = 0; i < 8; i++) {
289 safe_snprintf(hex_key_id + i * 2, 3, "%02x", key_id[i]);
290 }
291 hex_key_id[16] = '\0';
292
293 // Create display string
294 int result = safe_snprintf(output, output_size, "GPG key ID: %s", hex_key_id);
295 if (result < 0 || result >= (int)output_size) {
296 SET_ERRNO(ERROR_STRING, "Failed to format GPG key display");
297 return ERROR_STRING;
298 }
299
300 return ASCIICHAT_OK;
301}
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:151

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 387 of file keys.c.

387 {
388 if (!key || !output || output_size == 0) {
389 return;
390 }
391
392 if (key->type == KEY_TYPE_ED25519) {
393 // Format as SSH Ed25519 public key
394 // Simple base64 encoding for 32 bytes = 43 chars + padding
395 // For now, just use hex encoding
396 char hex_key[65];
397 for (size_t i = 0; i < 32; i++) {
398 char hex_byte[3];
399 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
400 hex_key[i * 2] = hex_byte[0];
401 hex_key[i * 2 + 1] = hex_byte[1];
402 }
403 hex_key[64] = '\0';
404 safe_snprintf(output, output_size, "ssh-ed25519 %s %s", hex_key, key->comment);
405 } else if (key->type == KEY_TYPE_X25519) {
406 // Format as hex X25519 key
407 char hex_key[65];
408 for (size_t i = 0; i < 32; i++) {
409 char hex_byte[3];
410 safe_snprintf(hex_byte, sizeof(hex_byte), "%02x", key->key[i]);
411 hex_key[i * 2] = hex_byte[0];
412 hex_key[i * 2 + 1] = hex_byte[1];
413 }
414 hex_key[64] = '\0';
415 safe_snprintf(output, output_size, "x25519 %s", hex_key);
416 } else {
417 safe_snprintf(output, output_size, "unknown key type: %d", key->type);
418 }
419}
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 460 of file keys_validation.c.

460 {
461 if (!key || !fingerprint_out) {
462 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p, fingerprint_out=%p", key, fingerprint_out);
463 return ERROR_INVALID_PARAM;
464 }
465
466 if (fingerprint_size < 32) {
467 SET_ERRNO(ERROR_INVALID_PARAM, "Fingerprint buffer too small: %zu (minimum 32)", fingerprint_size);
468 return ERROR_INVALID_PARAM;
469 }
470
471 // Generate SHA-256 fingerprint of the key
472 if (crypto_hash_sha256(fingerprint_out, key->key, 32) != 0) {
473 SET_ERRNO(ERROR_CRYPTO, "Failed to generate key fingerprint");
474 return ERROR_CRYPTO;
475 }
476
477 return ASCIICHAT_OK;
478}

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 137 of file gpg_keys.c.

137 {
138 if (!gpg_key_text || !fingerprint_out) {
139 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, fingerprint_out=%p", gpg_key_text,
140 fingerprint_out);
141 return ERROR_INVALID_PARAM;
142 }
143
144 // TODO: Implement GPG fingerprint extraction
145 // This requires parsing the OpenPGP packet structure and computing SHA-1
146
147 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG fingerprint extraction not yet implemented");
148 return ERROR_CRYPTO_KEY;
149}

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 151 of file gpg_keys.c.

151 {
152 if (!gpg_key_text || !key_id_out) {
153 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, key_id_out=%p", gpg_key_text, key_id_out);
154 return ERROR_INVALID_PARAM;
155 }
156
157 // TODO: Implement GPG key ID extraction
158 // This is typically the last 8 bytes of the fingerprint
159
160 SET_ERRNO(ERROR_CRYPTO_KEY, "GPG key ID extraction not yet implemented");
161 return ERROR_CRYPTO_KEY;
162}

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 111 of file gpg_keys.c.

111 {
112 if (!gpg_key_text || !x25519_pk) {
113 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_text=%p, x25519_pk=%p", gpg_key_text, x25519_pk);
114 return ERROR_INVALID_PARAM;
115 }
116
117 // First extract the Ed25519 public key from the GPG key
118 uint8_t ed25519_pk[32];
119 asciichat_error_t extract_result = extract_ed25519_from_gpg(gpg_key_text, ed25519_pk);
120 if (extract_result != ASCIICHAT_OK) {
121 return extract_result;
122 }
123
124 // Convert Ed25519 to X25519
125 if (crypto_sign_ed25519_pk_to_curve25519(x25519_pk, ed25519_pk) != 0) {
126 SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to convert Ed25519 to X25519");
127 return ERROR_CRYPTO_KEY;
128 }
129
130 return ASCIICHAT_OK;
131}
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:95

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 425 of file keys.c.

425 {
426 if (!hex || !output) {
427 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for hex_decode");
428 }
429
430 size_t hex_len = strlen(hex);
431 size_t expected_hex_len = output_len * 2;
432
433 if (hex_len != expected_hex_len) {
435 "Hex string length (%zu) doesn't match expected output length (%zu * 2 = %zu)", hex_len,
436 output_len, expected_hex_len);
437 }
438
439 // Decode hex string to binary
440 for (size_t i = 0; i < output_len; i++) {
441 char hex_byte[3] = {hex[i * 2], hex[i * 2 + 1], '\0'};
442 char *endptr;
443 unsigned long byte = strtoul(hex_byte, &endptr, 16);
444
445 // Validate hex character
446 if (*endptr != '\0' || byte > 255) {
447 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid hex character at position %zu: '%c%c'", i * 2, hex[i * 2],
448 hex[i * 2 + 1]);
449 }
450
451 output[i] = (uint8_t)byte;
452 }
453
454 return ASCIICHAT_OK;
455}

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 32 of file gpg_keys.c.

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

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

78 {
79 if (!gpg_key_binary || !key_out) {
80 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: gpg_key_binary=%p, key_out=%p", gpg_key_binary, key_out);
82 }
83
84 if (key_size == 0) {
85 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid key size: %zu", key_size);
87 }
88
89 // TODO: Implement binary GPG key parsing
90 // This requires parsing the OpenPGP packet format
91 SET_ERRNO(ERROR_CRYPTO_KEY, "Binary GPG key parsing not yet implemented");
92 return ERROR_CRYPTO_KEY;
93}

References ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

◆ 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 408 of file https_keys.c.

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

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, ERROR_MEMORY, log_debug, log_warn, SAFE_FREE, SAFE_MALLOC, 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 343 of file keys.c.

343 {
344 if (!path || !keys || !num_keys) {
345 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for key file parsing");
346 }
347
348 *num_keys = 0;
349
350 if (!path_looks_like_path(path)) {
351 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid keys file path: %s", path);
352 }
353
354 char *normalized_path = NULL;
355 asciichat_error_t path_result = path_validate_user_path(path, PATH_ROLE_CLIENT_KEYS, &normalized_path);
356 if (path_result != ASCIICHAT_OK) {
357 SAFE_FREE(normalized_path);
358 return path_result;
359 }
360
361 FILE *f = platform_fopen(normalized_path, "r");
362 if (!f) {
363 SAFE_FREE(normalized_path);
364 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to open keys file: %s", path);
365 }
366
367 char line[BUFFER_SIZE_LARGE];
368 while (fgets(line, sizeof(line), f) && *num_keys < max_keys) {
369 // Remove newline
370 line[strcspn(line, "\r\n")] = 0;
371
372 // Skip empty lines and comments
373 if (strlen(line) == 0 || line[0] == '#') {
374 continue;
375 }
376
377 if (parse_public_key(line, &keys[*num_keys]) == ASCIICHAT_OK) {
378 (*num_keys)++;
379 }
380 }
381
382 (void)fclose(f);
383 SAFE_FREE(normalized_path);
384 return ASCIICHAT_OK;
385}
#define BUFFER_SIZE_LARGE
Large buffer size (1024 bytes)
FILE * platform_fopen(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:439
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:498
@ PATH_ROLE_CLIENT_KEYS
Definition path.h:258

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 108 of file keys.c.

108 {
109 if (!key_path || !key_out) {
110 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private key parsing");
111 }
112
113 // Clear output structure
114 memset(key_out, 0, sizeof(private_key_t));
115
116 // Check for GPG key format: "gpg:KEYID" where KEYID is 8, 16, or 40 hex chars
117 // - 8 chars: short key ID (last 8 chars of fingerprint)
118 // - 16 chars: long key ID (last 16 chars of fingerprint)
119 // - 40 chars: full fingerprint
120 if (strncmp(key_path, "gpg:", 4) == 0) {
121 const char *key_id = key_path + 4; // Skip "gpg:" prefix
122
123 // Validate key ID format (must be 8, 16, or 40 hex characters)
124 size_t key_id_len = strlen(key_id);
125 if (key_id_len != 8 && key_id_len != 16 && key_id_len != 40) {
126 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID length: %zu (expected 8, 16, or 40 hex chars)",
127 key_id_len);
128 }
129
130 // Validate hex characters
131 for (size_t i = 0; i < key_id_len; i++) {
132 char c = key_id[i];
133 if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
134 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid GPG key ID: contains non-hex character '%c'", c);
135 }
136 }
137
138 // Get public key and keygrip from GPG keyring
139 uint8_t public_key[32];
140 char keygrip[64];
141 if (gpg_get_public_key(key_id, public_key, keygrip) != 0) {
142 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to get public key from GPG for key ID: %s", key_id);
143 }
144
145 // Populate private_key_t structure for GPG agent use
146 key_out->type = KEY_TYPE_ED25519;
147 key_out->use_gpg_agent = true;
148 key_out->use_ssh_agent = false;
149
150 // Store public key (for handshake and verification)
151 memcpy(key_out->public_key, public_key, 32);
152
153 // Store keygrip (for GPG agent signing operations)
154 platform_strncpy(key_out->gpg_keygrip, sizeof(key_out->gpg_keygrip), keygrip, sizeof(key_out->gpg_keygrip) - 1);
155
156 // Store comment for display
157 safe_snprintf(key_out->key_comment, sizeof(key_out->key_comment), "GPG key %s", key_id);
158
159 // Clear the key.ed25519 field (we don't have the private key in memory)
160 // The private key stays protected in GPG agent
161 memset(key_out->key.ed25519, 0, 64);
162
163 // Set the public key in the second half of ed25519 key (standard Ed25519 format)
164 // This is for compatibility with code that expects the public key at offset 32
165 memcpy(key_out->key.ed25519 + 32, public_key, 32);
166
167 log_info("Loaded GPG key %s (keygrip: %.40s) for agent signing", key_id, keygrip);
168 return ASCIICHAT_OK;
169 }
170
171 // Try SSH private key parsing
172 char *normalized_path = NULL;
173 asciichat_error_t path_result = path_validate_user_path(key_path, PATH_ROLE_KEY_PRIVATE, &normalized_path);
174 if (path_result != ASCIICHAT_OK) {
175 SAFE_FREE(normalized_path);
176 return path_result;
177 }
178 asciichat_error_t result = parse_ssh_private_key(normalized_path, key_out);
179 SAFE_FREE(normalized_path);
180 return result;
181}
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:218
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:256

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_info, parse_ssh_private_key(), PATH_ROLE_KEY_PRIVATE, path_validate_user_path(), platform_strncpy(), private_key_t::public_key, SAFE_FREE, 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:

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.
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 24 of file keys.c.

24 {
25 if (!input || !key_out) {
26 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public key parsing");
27 }
28
29 // Clear output structure
30 memset(key_out, 0, sizeof(public_key_t));
31
32 // Try SSH key parsing first
33 if (strncmp(input, "ssh-ed25519", 11) == 0) {
34 key_out->type = KEY_TYPE_ED25519;
35 asciichat_error_t result = parse_ssh_ed25519_line(input, key_out->key);
36 if (result == ASCIICHAT_OK) {
37 platform_strncpy(key_out->comment, sizeof(key_out->comment), "ssh-ed25519", sizeof(key_out->comment) - 1);
38 }
39 return result;
40 }
41
42 // Try GPG key parsing
43 if (strncmp(input, "gpg:", 4) == 0) {
44 return parse_gpg_key(input + 4, key_out);
45 }
46
47 // Try HTTPS key fetching (GitHub/GitLab) - delegate to parse_public_keys and return first
48 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
49 public_key_t keys[1];
50 size_t num_keys = 0;
51 asciichat_error_t result = parse_public_keys(input, keys, &num_keys, 1);
52 if (result == ASCIICHAT_OK && num_keys > 0) {
53 *key_out = keys[0];
54 }
55 return result;
56 }
57
58 // Try raw hex key (64 hex chars = 32 bytes)
59 if (strlen(input) == 64) {
60 // Check if it's valid hex
61 bool is_valid_hex = true;
62 for (int i = 0; i < 64; i++) {
63 if (!((input[i] >= '0' && input[i] <= '9') || (input[i] >= 'a' && input[i] <= 'f') ||
64 (input[i] >= 'A' && input[i] <= 'F'))) {
65 is_valid_hex = false;
66 break;
67 }
68 }
69
70 if (is_valid_hex) {
71 // Assume it's a raw X25519 public key in hex (default for raw hex)
72 key_out->type = KEY_TYPE_X25519;
73 // Use hex_decode utility to safely decode hex string to binary
74 if (hex_decode(input, key_out->key, 32) != ASCIICHAT_OK) {
75 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode hex key");
76 }
77 platform_strncpy(key_out->comment, sizeof(key_out->comment), "raw-hex", sizeof(key_out->comment) - 1);
78 return ASCIICHAT_OK;
79 }
80 }
81
82 if (path_looks_like_path(input)) {
83 char *normalized_path = NULL;
84 asciichat_error_t path_result = path_validate_user_path(input, PATH_ROLE_KEY_PUBLIC, &normalized_path);
85 if (path_result != ASCIICHAT_OK) {
86 SAFE_FREE(normalized_path);
87 return path_result;
88 }
89
90 FILE *f = platform_fopen(normalized_path, "r");
91 if (f) {
92 char line[BUFFER_SIZE_LARGE];
93 if (fgets(line, sizeof(line), f)) {
94 (void)fclose(f);
95 SAFE_FREE(normalized_path);
96 // Remove newline
97 line[strcspn(line, "\r\n")] = 0;
98 return parse_public_key(line, key_out);
99 }
100 (void)fclose(f);
101 }
102 SAFE_FREE(normalized_path);
103 }
104
105 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key format: %s", input);
106}
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:167
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:187
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:32
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:425
@ PATH_ROLE_KEY_PUBLIC
Definition path.h:257

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, public_key_t::comment, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, hex_decode(), public_key_t::key, KEY_TYPE_ED25519, KEY_TYPE_X25519, parse_gpg_key(), parse_public_key(), parse_public_keys(), parse_ssh_ed25519_line(), path_looks_like_path(), PATH_ROLE_KEY_PUBLIC, path_validate_user_path(), platform_fopen(), platform_strncpy(), SAFE_FREE, SET_ERRNO, and public_key_t::type.

Referenced by check_known_host(), 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)

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

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.

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 187 of file keys.c.

187 {
188 if (!input || !keys_out || !num_keys || max_keys == 0) {
189 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for multi-key parsing");
190 }
191
192 *num_keys = 0;
193
194 // Check for direct SSH Ed25519 key format BEFORE checking for file paths
195 // (SSH keys can contain '/' in base64, which would match path_looks_like_path)
196 if (strncmp(input, "ssh-ed25519", 11) == 0) {
197 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
198 if (result == ASCIICHAT_OK) {
199 *num_keys = 1;
200 }
201 return result;
202 }
203
204 // Check if this is a GitHub/GitLab reference - these support multiple keys
205 if (strncmp(input, "github:", 7) == 0 || strncmp(input, "gitlab:", 7) == 0) {
206 const char *username = input + 7; // Skip "github:" or "gitlab:"
207 bool is_github = (strncmp(input, "github:", 7) == 0);
208 bool is_gpg = (strstr(username, ".gpg") != NULL);
209
210 char **keys = NULL;
211 size_t num_fetched_keys = 0;
212 asciichat_error_t result;
213
214 if (is_github) {
215 if (is_gpg) {
216 result = fetch_github_gpg_keys(username, &keys, &num_fetched_keys);
217 } else {
218 result = fetch_github_ssh_keys(username, &keys, &num_fetched_keys);
219 }
220 } else {
221 if (is_gpg) {
222 result = fetch_gitlab_gpg_keys(username, &keys, &num_fetched_keys);
223 } else {
224 result = fetch_gitlab_ssh_keys(username, &keys, &num_fetched_keys);
225 }
226 }
227
228 if (result != ASCIICHAT_OK || num_fetched_keys == 0) {
229 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to fetch keys from %s for user: %s", is_github ? "GitHub" : "GitLab",
230 username);
231 }
232
233 // Parse each fetched key (only Ed25519 keys will succeed)
234 for (size_t i = 0; i < num_fetched_keys && *num_keys < max_keys; i++) {
235 if (parse_public_key(keys[i], &keys_out[*num_keys]) == ASCIICHAT_OK) {
236 (*num_keys)++;
237 }
238 // Non-Ed25519 keys are silently skipped
239 }
240
241 // Free the keys array
242 for (size_t i = 0; i < num_fetched_keys; i++) {
243 SAFE_FREE(keys[i]);
244 }
245 SAFE_FREE(keys);
246
247 if (*num_keys == 0) {
248 return SET_ERRNO(ERROR_CRYPTO_KEY, "No valid Ed25519 keys found for %s user: %s", is_github ? "GitHub" : "GitLab",
249 username);
250 }
251
252 log_info("Parsed %zu Ed25519 key(s) from %s user: %s", *num_keys, is_github ? "GitHub" : "GitLab", username);
253 return ASCIICHAT_OK;
254 }
255
256 // For file paths, delegate to parse_keys_from_file
257 if (path_looks_like_path(input)) {
258 return parse_keys_from_file(input, keys_out, num_keys, max_keys);
259 }
260
261 // For all other formats, use single-key parsing
262 asciichat_error_t result = parse_public_key(input, &keys_out[0]);
263 if (result == ASCIICHAT_OK) {
264 *num_keys = 1;
265 }
266 return result;
267}

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, fetch_github_gpg_keys(), fetch_github_ssh_keys(), fetch_gitlab_gpg_keys(), fetch_gitlab_ssh_keys(), log_info, parse_keys_from_file(), parse_public_key(), path_looks_like_path(), SAFE_FREE, and SET_ERRNO.

Referenced by crypto_handshake_client_key_exchange(), and parse_public_key().

◆ 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 167 of file ssh_keys.c.

167 {
168 if (!line || !ed25519_pk) {
169 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: line=%p, ed25519_pk=%p", line, ed25519_pk);
170 }
171
172 // Find "ssh-ed25519 "
173 const char *type_start = strstr(line, "ssh-ed25519");
174 if (!type_start) {
175 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key line does not contain 'ssh-ed25519'");
176 }
177
178 // Skip to base64 part
179 const char *base64_start = type_start + 11; // strlen("ssh-ed25519")
180 while (*base64_start == ' ' || *base64_start == '\t') {
181 base64_start++;
182 }
183
184 // Find end of base64 (space, newline, or end of string)
185 const char *base64_end = base64_start;
186 while (*base64_end && *base64_end != ' ' && *base64_end != '\t' && *base64_end != '\n' && *base64_end != '\r') {
187 base64_end++;
188 }
189
190 size_t base64_len = base64_end - base64_start;
191
192 // Base64 decode
193 uint8_t *blob;
194 size_t blob_len;
195 if (base64_decode_ssh_key(base64_start, base64_len, &blob, &blob_len) != 0) {
196 return SET_ERRNO(ERROR_CRYPTO_KEY, "Failed to decode SSH key base64 data");
197 }
198
199 // Parse SSH key blob structure:
200 // [4 bytes: length of "ssh-ed25519"]
201 // [11 bytes: "ssh-ed25519"]
202 // [4 bytes: length of public key (32)]
203 // [32 bytes: Ed25519 public key]
204
205 if (blob_len < SSH_KEY_HEADER_SIZE) {
206 SAFE_FREE(blob);
207 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key blob too small: %zu bytes (expected at least %d)", blob_len,
209 }
210
211 // Extract Ed25519 public key (last 32 bytes)
212 memcpy(ed25519_pk, blob + blob_len - ED25519_PUBLIC_KEY_SIZE, ED25519_PUBLIC_KEY_SIZE);
213 SAFE_FREE(blob);
214
215 return ASCIICHAT_OK;
216}
#define ED25519_PUBLIC_KEY_SIZE
Ed25519 public key size in bytes.
#define SSH_KEY_HEADER_SIZE

References ASCIICHAT_OK, 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 310 of file https_keys.c.

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

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 218 of file ssh_keys.c.

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

References ASCIICHAT_OK, BUFFER_SIZE_LARGE, BUFFER_SIZE_SMALL, BUFFER_SIZE_XXLARGE, 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_info, 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 292 of file keys.c.

292 {
293 if (!key || !x25519_sk) {
294 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for private_key_to_x25519");
295 }
296
297 if (key->type == KEY_TYPE_X25519) {
298 // Passthrough for X25519 keys
299 memcpy(x25519_sk, key->key.x25519, 32);
300 return ASCIICHAT_OK;
301 }
302
303 if (key->type == KEY_TYPE_ED25519) {
304 // Convert Ed25519 to X25519 (Ed25519 private key is 64 bytes: seed + public)
305 return ed25519_to_x25519_private(key->key.ed25519, x25519_sk);
306 }
307
308 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
309}
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 273 of file keys.c.

273 {
274 if (!key || !x25519_pk) {
275 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for public_key_to_x25519");
276 }
277
278 if (key->type == KEY_TYPE_X25519) {
279 // Passthrough for X25519 keys
280 memcpy(x25519_pk, key->key, 32);
281 return ASCIICHAT_OK;
282 }
283
284 if (key->type == KEY_TYPE_ED25519) {
285 // Convert Ed25519 to X25519
286 return ed25519_to_x25519_public(key->key, x25519_pk);
287 }
288
289 return SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported key type for X25519 conversion: %d", key->type);
290}

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 176 of file keys_validation.c.

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

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

◆ validate_key_permissions()

asciichat_error_t validate_key_permissions ( const char *  key_path)

#include <keys_validation.h>

Validate key file permissions.

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

Validates key file permissions by checking Unix file permissions. Ensures file has restrictive permissions (0600).

Note
Permission checking: Checks file permissions on Unix systems. Validates that file has owner read/write only (0600).
Platform-specific: Permission checking is Unix-specific. Windows does not have Unix-style permissions.
Recommended permissions: Private key files should have 0600 permissions. Public key files can have more permissive permissions (0644).
Warning
Platform-specific: Permission checking is Unix-specific. Windows does not have Unix-style permissions.

Definition at line 297 of file keys_validation.c.

297 {
298 if (!key_path) {
299 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p", key_path);
300 return ERROR_INVALID_PARAM;
301 }
302
303#ifndef _WIN32
304 struct stat st;
305 if (stat(key_path, &st) != 0) {
306 SET_ERRNO(ERROR_CRYPTO_KEY, "Cannot stat key file: %s", key_path);
307 return ERROR_CRYPTO_KEY;
308 }
309
310 // Check for overly permissive permissions
311 if ((st.st_mode & SSH_KEY_PERMISSIONS_MASK) != 0) {
312 SET_ERRNO(ERROR_CRYPTO_KEY, "Key file has overly permissive permissions: %o (recommended: 600)", st.st_mode & 0777);
313 return ERROR_CRYPTO_KEY;
314 }
315#endif
316
317 return ASCIICHAT_OK;
318}
#define SSH_KEY_PERMISSIONS_MASK

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

Referenced by validate_key_security().

◆ 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 123 of file keys_validation.c.

123 {
124 if (!key_path) {
125 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p", key_path);
126 return ERROR_INVALID_PARAM;
127 }
128
129 // Check file permissions
130 asciichat_error_t perm_result = validate_key_permissions(key_path);
131 if (perm_result != ASCIICHAT_OK) {
132 return perm_result;
133 }
134
135 // Additional security checks have been implemented in check_key_strength()
136 // and check_key_patterns() functions. File permissions check is the critical one.
137 // Known weak keys database would require periodic updates and is beyond scope.
138
139 return ASCIICHAT_OK;
140}
asciichat_error_t validate_key_permissions(const char *key_path)
Validate key file permissions.

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

◆ 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 67 of file keys_validation.c.

67 {
68 if (!key) {
69 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key=%p", key);
71 }
72
73 // Check key type
74 if (key->type == KEY_TYPE_UNKNOWN) {
75 SET_ERRNO(ERROR_CRYPTO_KEY, "Private key type is unknown");
76 return ERROR_CRYPTO_KEY;
77 }
78
79 if (key->type != KEY_TYPE_ED25519 && key->type != KEY_TYPE_X25519) {
80 SET_ERRNO(ERROR_CRYPTO_KEY, "Unsupported private key type: %d", key->type);
81 return ERROR_CRYPTO_KEY;
82 }
83
84 // Check key data is not all zeros
85 bool is_zero = true;
86 size_t key_size = (key->type == KEY_TYPE_ED25519) ? 64 : 32;
87
88 for (size_t i = 0; i < key_size; i++) {
89 if (key->key.ed25519[i] != 0) {
90 is_zero = false;
91 break;
92 }
93 }
94
95 if (is_zero) {
96 SET_ERRNO(ERROR_CRYPTO_KEY, "Private key data is all zeros");
97 return ERROR_CRYPTO_KEY;
98 }
99
100 // Check comment length
101 if (strlen(key->key_comment) >= MAX_COMMENT_LEN) {
102 SET_ERRNO(ERROR_CRYPTO_KEY, "Private key comment too long: %zu (maximum %d)", strlen(key->key_comment),
103 MAX_COMMENT_LEN - 1);
104 return ERROR_CRYPTO_KEY;
105 }
106
107 return ASCIICHAT_OK;
108}
#define MAX_COMMENT_LEN

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, and private_key_t::type.

◆ 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 27 of file keys_validation.c.

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

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, and public_key_t::type.

◆ 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 977 of file ssh_keys.c.

977 {
978 if (!key_path) {
979 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters: key_path=%p", key_path);
980 }
981
982 if (!path_looks_like_path(key_path)) {
983 return SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid SSH key path: %s", key_path);
984 }
985
986 char *normalized_path = NULL;
987 asciichat_error_t path_result = path_validate_user_path(key_path, PATH_ROLE_KEY_PRIVATE, &normalized_path);
988 if (path_result != ASCIICHAT_OK) {
989 SAFE_FREE(normalized_path);
990 return path_result;
991 }
992
993 // Check if file exists and is readable
994 FILE *test_file = platform_fopen(normalized_path, "r");
995 if (test_file == NULL) {
996 SAFE_FREE(normalized_path);
997 return SET_ERRNO(ERROR_CRYPTO_KEY, "Cannot read key file: %s", key_path);
998 }
999
1000 // Check if this is an SSH key file by looking for the header
1001 char header[BUFFER_SIZE_SMALL];
1002 bool is_ssh_key_file = false;
1003 if (fgets(header, sizeof(header), test_file) != NULL) {
1004 if (strstr(header, "BEGIN OPENSSH PRIVATE KEY") != NULL || strstr(header, "BEGIN RSA PRIVATE KEY") != NULL ||
1005 strstr(header, "BEGIN EC PRIVATE KEY") != NULL) {
1006 is_ssh_key_file = true;
1007 }
1008 }
1009 (void)fclose(test_file);
1010
1011 if (!is_ssh_key_file) {
1012 SAFE_FREE(normalized_path);
1013 return SET_ERRNO(ERROR_CRYPTO_KEY, "File is not a valid SSH key: %s", key_path);
1014 }
1015
1016 // Check permissions for SSH key files (should be 600 or 400)
1017#ifndef _WIN32
1018 struct stat st;
1019 if (stat(normalized_path, &st) == 0) {
1020 if ((st.st_mode & SSH_KEY_PERMISSIONS_MASK) != 0) {
1021 log_error("SSH key file %s has overly permissive permissions: %o", key_path, st.st_mode & 0777);
1022 log_error("Run 'chmod 600 %s' to fix this", key_path);
1023 SAFE_FREE(normalized_path);
1024 return SET_ERRNO(ERROR_CRYPTO_KEY, "SSH key file has overly permissive permissions: %s", key_path);
1025 }
1026 }
1027#endif
1028
1029 SAFE_FREE(normalized_path);
1030 return ASCIICHAT_OK;
1031}

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

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

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

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

References ASCIICHAT_OK, ERROR_CRYPTO_KEY, ERROR_INVALID_PARAM, and SET_ERRNO.

Variable Documentation

◆ comment

char public_key_t::comment[256]

◆ ed25519 [1/2]

uint8_t private_key_t::ed25519[64]

◆ [] [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]

union { ... } private_key_t::key

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