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:
public_key_t pubkey;
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@example.com",
&pubkey
);
private_key_t privkey;
asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out)
asciichat_error_t parse_public_key(const char *input, public_key_t *key_out)
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:
private_key_t privkey;
"gpg:897607FA43DC66F612710AF97FE90A79F2E80ED3",
&privkey
);
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:
public_key_t pubkey;
"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
&pubkey
);
Key Sources
Local Files
Keys can be loaded from local files:
Public Keys:
public_key_t pubkey;
FILE *f = fopen("~/.ssh/id_ed25519.pub", "r");
char line[1024];
fgets(line, sizeof(line), f);
fclose(f);
public_key_t keys[10];
size_t num_keys;
asciichat_error_t parse_keys_from_file(const char *path, public_key_t *keys, size_t *num_keys, size_t max_keys)
Private Keys:
GitHub and GitLab Integration
Keys can be fetched directly from GitHub or GitLab using HTTPS:
SSH Keys:
char **keys;
size_t num_keys;
if (err == ASCIICHAT_OK && num_keys > 0) {
public_key_t pubkey;
for (size_t i = 0; i < num_keys; i++) {
free(keys[i]);
}
free(keys);
}
asciichat_error_t fetch_github_ssh_keys(const char *username, char ***keys_out, size_t *num_keys)
GPG Keys:
char **keys;
size_t num_keys;
asciichat_error_t fetch_github_gpg_keys(const char *username, char ***keys_out, size_t *num_keys)
GitLab Support:
asciichat_error_t fetch_gitlab_gpg_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)
Unified Parsing:
Unified Key Parsing
The parse_public_key() function supports multiple input formats automatically:
public_key_t pubkey;
asciichat_error_t err;
Key Validation
Public Key Validation
All public keys are validated before use:
public_key_t pubkey;
if (err != ASCIICHAT_OK) {
log_error("Invalid public key");
return err;
}
asciichat_error_t validate_public_key(const public_key_t *key)
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;
if (err != ASCIICHAT_OK) {
log_error("Invalid private key");
return err;
}
asciichat_error_t validate_private_key(const private_key_t *key)
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:
asciichat_error_t validate_ssh_key_file(const char *key_path)
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:
public_key_t ed25519_pubkey;
uint8_t x25519_pk[32];
asciichat_error_t public_key_to_x25519(const public_key_t *key, uint8_t x25519_pk[32])
Private Key Conversion:
private_key_t ed25519_privkey;
uint8_t x25519_sk[32];
asciichat_error_t private_key_to_x25519(const private_key_t *key, uint8_t x25519_sk[32])
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:
uint8_t ed25519_pk[32] = {...};
uint8_t x25519_pk[32];
uint8_t ed25519_sk[64] = {...};
uint8_t x25519_sk[32];
asciichat_error_t ed25519_to_x25519_private(const uint8_t ed25519_sk[64], uint8_t x25519_sk[32])
asciichat_error_t ed25519_to_x25519_public(const uint8_t ed25519_pk[32], uint8_t x25519_pk[32])
Signing and Verification
Signing Messages
Messages can be signed with Ed25519 private keys:
private_key_t privkey;
const uint8_t message[] = "Hello, secure world!";
uint8_t signature[64];
&privkey,
message,
sizeof(message) - 1,
signature
);
asciichat_error_t ed25519_sign_message(const private_key_t *key, const uint8_t *message, size_t message_len, uint8_t signature[64])
SSH Agent Support:
privkey.use_ssh_agent = true;
Verifying Signatures
Signatures can be verified with Ed25519 public keys:
public_key_t pubkey;
uint8_t ed25519_pk[32];
const uint8_t message[] = "Hello, secure world!";
uint8_t signature[64] = {...};
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)
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:
ssh_keys.h: SSH Ed25519 key parsing
gpg_keys.h: GPG key parsing (may be disabled)
https_keys.h: HTTPS key fetching from GitHub/GitLab
Support Modules:
pem_utils.h: PEM encoding/decoding utilities
http_client.h: BearSSL HTTPS client for key fetching
Data Structures
Public Key Structure:
typedef struct {
key_type_t type;
uint8_t data[32];
char comment[256];
} public_key_t;
Private Key Structure:
typedef struct {
key_type_t type;
union {
uint8_t x25519[32];
uint8_t ed25519[64];
} key;
char comment[256];
bool use_ssh_agent;
bool use_gpg_agent;
} private_key_t;
Usage Examples
Basic Key Loading
#include "crypto/keys/keys.h"
public_key_t server_pubkey;
"github:server-username",
&server_pubkey
);
if (err != ASCIICHAT_OK) {
log_error("Failed to load server public key");
return err;
}
if (err != ASCIICHAT_OK) {
log_error("Invalid server public key");
return err;
}
uint8_t x25519_pk[32];
Client Authentication
private_key_t client_privkey;
client_privkey.use_ssh_agent = true;
uint8_t challenge[] = {...};
uint8_t signature[64];
&client_privkey,
challenge,
sizeof(challenge),
signature
);
send_signature_to_server(signature);
Server Key Whitelist
Loading from file (recommended):
public_key_t client_keys[MAX_CLIENTS];
size_t num_authorized = 0;
asciichat_error_t err = parse_client_keys(
"~/.ssh/authorized_keys",
client_keys,
&num_authorized,
MAX_CLIENTS
);
Loading from array:
const char *authorized_clients[] = {
"github:alice",
"github:bob",
"ssh-ed25519 AAAAC3... charlie@example.com"
};
public_key_t client_keys[MAX_CLIENTS];
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) {
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:
Key Validation:
Key Conversion:
Signing and Verification:
size_t message_len, uint8_t signature[64]);
size_t message_len, const uint8_t signature[64]);
HTTPS Key Fetching:
- 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