Security API Reference¶
This page documents the security-related modules in Nauyaca, including certificate management, TOFU (Trust On First Use) validation, and TLS context creation.
Security Model
Nauyaca uses Trust On First Use (TOFU) for certificate validation instead of traditional Certificate Authority (CA) validation. This is the recommended approach for the Gemini protocol. See the Security Explanation for more details.
Overview¶
The security modules provide:
- Certificate Management (
nauyaca.security.certificates) - Generate, load, validate, and fingerprint TLS certificates - TOFU Database (
nauyaca.security.tofu) - Store and verify certificate fingerprints for known hosts - TLS Context Creation (
nauyaca.security.tls) - Create SSL contexts for client and server connections
Certificate Management¶
certificates
¶
Certificate generation and management utilities.
This module provides utilities for generating, loading, and validating TLS certificates for use with Gemini protocol servers and clients.
generate_self_signed_cert
¶
generate_self_signed_cert(
hostname: str,
key_size: int = 2048,
valid_days: int = 365,
) -> tuple[bytes, bytes]
Generate a self-signed TLS certificate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
Hostname for the certificate (CN and SAN). |
required |
key_size
|
int
|
RSA key size in bits (default: 2048). |
2048
|
valid_days
|
int
|
Certificate validity period in days (default: 365). |
365
|
Returns:
| Type | Description |
|---|---|
tuple[bytes, bytes]
|
Tuple of (certificate_pem, private_key_pem) as bytes. |
Example
cert_pem, key_pem = generate_self_signed_cert("localhost") Path("cert.pem").write_bytes(cert_pem) Path("key.pem").write_bytes(key_pem)
Source code in src/nauyaca/security/certificates.py
get_certificate_fingerprint
¶
Calculate the fingerprint of a certificate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert
|
Certificate
|
The certificate to fingerprint. |
required |
algorithm
|
str
|
Hash algorithm to use (default: sha256). |
'sha256'
|
Returns:
| Type | Description |
|---|---|
str
|
Fingerprint string in format "algorithm:hexdigest". |
Example
cert = load_certificate(Path("cert.pem")) fingerprint = get_certificate_fingerprint(cert) print(fingerprint) 'sha256:a1b2c3d4e5f6...'
Source code in src/nauyaca/security/certificates.py
get_certificate_fingerprint_from_path
¶
Calculate the fingerprint of a certificate file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert_path
|
Path
|
Path to the certificate file. |
required |
algorithm
|
str
|
Hash algorithm to use (default: sha256). |
'sha256'
|
Returns:
| Type | Description |
|---|---|
str
|
Hex-encoded fingerprint string. |
Source code in src/nauyaca/security/certificates.py
get_certificate_info
¶
Extract human-readable information from a certificate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert
|
Certificate
|
The certificate to inspect. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Dictionary containing certificate information. |
Source code in src/nauyaca/security/certificates.py
is_certificate_expired
¶
Check if a certificate has expired.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert
|
Certificate
|
The certificate to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the certificate has expired, False otherwise. |
Source code in src/nauyaca/security/certificates.py
is_certificate_valid_for_hostname
¶
Check if a certificate is valid for a given hostname.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert
|
Certificate
|
The certificate to check. |
required |
hostname
|
str
|
The hostname to validate against. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the certificate is valid for the hostname, False otherwise. |
Source code in src/nauyaca/security/certificates.py
load_certificate
¶
Load a certificate from a PEM file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert_path
|
Path
|
Path to the certificate file. |
required |
Returns:
| Type | Description |
|---|---|
Certificate
|
The loaded certificate object. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If certificate file doesn't exist. |
ValueError
|
If certificate file is invalid. |
Source code in src/nauyaca/security/certificates.py
validate_certificate_file
¶
Validate a certificate file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cert_path
|
Path
|
Path to the certificate file. |
required |
Returns:
| Type | Description |
|---|---|
tuple[bool, str]
|
Tuple of (is_valid, error_message). error_message is empty if valid. |
Source code in src/nauyaca/security/certificates.py
Common Certificate Operations¶
Generating a Self-Signed Certificate¶
from pathlib import Path
from nauyaca.security.certificates import generate_self_signed_cert
# Generate certificate for localhost
cert_pem, key_pem = generate_self_signed_cert("localhost")
# Save to files
Path("cert.pem").write_bytes(cert_pem)
Path("key.pem").write_bytes(key_pem)
Loading and Fingerprinting Certificates¶
from pathlib import Path
from nauyaca.security.certificates import (
load_certificate,
get_certificate_fingerprint,
get_certificate_info
)
# Load certificate
cert = load_certificate(Path("cert.pem"))
# Get fingerprint
fingerprint = get_certificate_fingerprint(cert)
print(f"Certificate fingerprint: {fingerprint}")
# Get detailed information
info = get_certificate_info(cert)
for key, value in info.items():
print(f"{key}: {value}")
Validating Certificates¶
from pathlib import Path
from nauyaca.security.certificates import (
load_certificate,
is_certificate_expired,
is_certificate_valid_for_hostname,
validate_certificate_file
)
# Check if certificate is valid
is_valid, error_msg = validate_certificate_file(Path("cert.pem"))
if not is_valid:
print(f"Certificate invalid: {error_msg}")
# Load and perform detailed checks
cert = load_certificate(Path("cert.pem"))
# Check expiration
if is_certificate_expired(cert):
print("Certificate has expired!")
# Check hostname validity
if is_certificate_valid_for_hostname(cert, "example.com"):
print("Certificate is valid for example.com")
TOFU Database¶
TOFUDatabase
¶
SQLite-backed TOFU certificate database.
This class manages a database of known host certificates and provides methods for trusting, verifying, and revoking certificates.
Initialize the TOFU database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
db_path
|
Path | None
|
Path to the SQLite database file. If None, uses ~/.nauyaca/tofu.db (creates directory if needed). |
None
|
Source code in src/nauyaca/security/tofu.py
clear
¶
Clear all entries from the TOFU database.
Returns:
| Type | Description |
|---|---|
int
|
Number of entries removed. |
Source code in src/nauyaca/security/tofu.py
count_by_hostname
¶
Count all entries for a hostname across all ports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
The hostname to count entries for. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of entries for this hostname. |
Source code in src/nauyaca/security/tofu.py
export_toml
¶
Export the TOFU database to a TOML file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path
|
Path
|
Path to the output TOML file. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of hosts exported. |
Raises:
| Type | Description |
|---|---|
IOError
|
If the file cannot be written. |
Source code in src/nauyaca/security/tofu.py
get_host_info
¶
Get information about a specific host.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
The hostname to look up. |
required |
port
|
int
|
The port number. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str] | None
|
Dictionary containing host information, or None if not found. |
Source code in src/nauyaca/security/tofu.py
import_toml
¶
import_toml(
file_path: Path,
merge: bool = True,
on_conflict: Callable[[str, int, str, str], bool]
| None = None,
) -> tuple[int, int, int]
Import hosts from a TOML file into the TOFU database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path
|
Path
|
Path to the input TOML file. |
required |
merge
|
bool
|
If True, merge with existing entries. If False, replace all. |
True
|
on_conflict
|
Callable[[str, int, str, str], bool] | None
|
Callback for resolving fingerprint conflicts. Called with (hostname, port, old_fingerprint, new_fingerprint). Should return True to update, False to skip. If None, conflicts are skipped. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[int, int, int]
|
Tuple of (added_count, updated_count, skipped_count). |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the TOML file doesn't exist. |
ValueError
|
If the TOML structure is invalid. |
Source code in src/nauyaca/security/tofu.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 | |
list_hosts
¶
List all known hosts in the database.
Returns:
| Type | Description |
|---|---|
list[dict[str, str]]
|
List of dictionaries containing host information. |
Source code in src/nauyaca/security/tofu.py
revoke
¶
Remove a host from the TOFU database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
The hostname to revoke. |
required |
port
|
int
|
The port number. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the host was removed, False if it wasn't in the database. |
Source code in src/nauyaca/security/tofu.py
revoke_by_hostname
¶
Remove all entries for a hostname from the TOFU database.
This removes all port entries for the given hostname.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
The hostname to revoke all entries for. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of entries removed. |
Source code in src/nauyaca/security/tofu.py
trust
¶
Trust a certificate for a host.
This stores the certificate fingerprint in the database. If a certificate already exists for this host, it will be replaced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
The hostname (e.g., "example.com"). |
required |
port
|
int
|
The port number. |
required |
cert
|
Certificate
|
The certificate to trust. |
required |
Source code in src/nauyaca/security/tofu.py
verify
¶
Verify a certificate against the TOFU database.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
The hostname to verify. |
required |
port
|
int
|
The port number. |
required |
cert
|
Certificate
|
The certificate to verify. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Tuple of (is_valid, message): |
str
|
|
tuple[bool, str]
|
|
tuple[bool, str]
|
|
Source code in src/nauyaca/security/tofu.py
CertificateChangedError
¶
Bases: Exception
Exception raised when a certificate has changed unexpectedly.
This indicates a potential MITM attack or legitimate certificate renewal.
Initialize the exception.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
hostname
|
str
|
The hostname where certificate changed. |
required |
port
|
int
|
The port number. |
required |
old_fingerprint
|
str
|
The previously stored fingerprint. |
required |
new_fingerprint
|
str
|
The new certificate fingerprint. |
required |
Source code in src/nauyaca/security/tofu.py
Common TOFU Operations¶
Basic TOFU Validation¶
from pathlib import Path
from nauyaca.security.tofu import TOFUDatabase
from nauyaca.security.certificates import load_certificate
# Initialize TOFU database (uses ~/.nauyaca/tofu.db by default)
tofu = TOFUDatabase()
# Load certificate to verify
cert = load_certificate(Path("server.pem"))
# Verify certificate
is_valid, reason = tofu.verify("example.com", 1965, cert)
if reason == "first_use":
print("First connection to this host - trusting certificate")
tofu.trust("example.com", 1965, cert)
elif is_valid:
print("Certificate verified successfully")
else:
print(f"Certificate verification failed: {reason}")
Managing Known Hosts¶
from nauyaca.security.tofu import TOFUDatabase
tofu = TOFUDatabase()
# List all known hosts
hosts = tofu.list_hosts()
for host in hosts:
print(f"{host['hostname']}:{host['port']} - {host['fingerprint']}")
# Get info about specific host
info = tofu.get_host_info("example.com", 1965)
if info:
print(f"First seen: {info['first_seen']}")
print(f"Last seen: {info['last_seen']}")
# Revoke trust for a host
if tofu.revoke("example.com", 1965):
print("Host removed from database")
Exporting and Importing TOFU Data¶
from pathlib import Path
from nauyaca.security.tofu import TOFUDatabase
tofu = TOFUDatabase()
# Export to TOML file
count = tofu.export_toml(Path("tofu-backup.toml"))
print(f"Exported {count} hosts")
# Import from TOML file (merge with existing)
added, updated, skipped = tofu.import_toml(
Path("tofu-backup.toml"),
merge=True
)
print(f"Import: {added} added, {updated} updated, {skipped} skipped")
# Import with conflict resolution
def resolve_conflict(hostname, port, old_fp, new_fp):
"""Ask user whether to update the fingerprint."""
print(f"\nConflict for {hostname}:{port}")
print(f"Old: {old_fp}")
print(f"New: {new_fp}")
response = input("Update? [y/N]: ")
return response.lower() == 'y'
added, updated, skipped = tofu.import_toml(
Path("tofu-backup.toml"),
merge=True,
on_conflict=resolve_conflict
)
Custom TOFU Database Location¶
from pathlib import Path
from nauyaca.security.tofu import TOFUDatabase
# Use custom database path
custom_db = Path("/var/lib/myapp/tofu.db")
tofu = TOFUDatabase(db_path=custom_db)
TLS Context Creation¶
tls
¶
TLS context creation for Gemini protocol.
This module provides functions for creating SSL/TLS contexts for both client and server connections, following Gemini protocol requirements.
create_client_context
¶
create_client_context(
verify_mode: VerifyMode = ssl.CERT_NONE,
check_hostname: bool = False,
certfile: str | None = None,
keyfile: str | None = None,
) -> ssl.SSLContext
Create an SSL context for Gemini client connections.
The Gemini protocol requires TLS 1.2 or higher. This function creates an SSL context configured for client connections.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
verify_mode
|
VerifyMode
|
SSL certificate verification mode. Default is CERT_NONE for testing/development. Use CERT_REQUIRED with proper TOFU validation for production. |
CERT_NONE
|
check_hostname
|
bool
|
Whether to check that the certificate hostname matches the server hostname. Default is False (for testing/development). |
False
|
certfile
|
str | None
|
Optional path to client certificate file (for client cert auth). |
None
|
keyfile
|
str | None
|
Optional path to client private key file (for client cert auth). |
None
|
Returns:
| Type | Description |
|---|---|
SSLContext
|
An SSL context configured for Gemini client connections. |
Examples:
>>> # Production mode with TOFU (implement custom verification)
>>> context = create_client_context(
... verify_mode=ssl.CERT_REQUIRED,
... check_hostname=True
... )
>>> # With client certificate authentication
>>> context = create_client_context(
... certfile='client.pem',
... keyfile='client-key.pem'
... )
Source code in src/nauyaca/security/tls.py
create_server_context
¶
create_server_context(
certfile: str,
keyfile: str,
request_client_cert: bool = False,
client_ca_certs: list[str] | None = None,
) -> ssl.SSLContext
Create an SSL context for Gemini server connections.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
certfile
|
str
|
Path to server certificate file. |
required |
keyfile
|
str
|
Path to server private key file. |
required |
request_client_cert
|
bool
|
Whether to request client certificates. When True, the server will ask clients to send a certificate. With OpenSSL 3.x, client certificates must be signed by a CA in client_ca_certs or the TLS handshake will fail silently. Enforcement should be done via CertificateAuth middleware. Default is False. |
False
|
client_ca_certs
|
list[str] | None
|
List of paths to CA certificates for verifying client certificates. For self-signed client certs, include each client's cert file here. Required when request_client_cert=True with OpenSSL 3.x. Default is None. |
None
|
Returns:
| Type | Description |
|---|---|
SSLContext
|
An SSL context configured for Gemini server connections. |
Examples:
>>> # Server requesting client certificates (for middleware auth)
>>> context = create_server_context(
... 'cert.pem',
... 'key.pem',
... request_client_cert=True,
... client_ca_certs=['trusted_client1.pem', 'trusted_client2.pem']
... )
Source code in src/nauyaca/security/tls.py
Common TLS Operations¶
Creating Client Contexts¶
import ssl
from nauyaca.security.tls import create_client_context
# Testing/development - accept all certificates
context = create_client_context()
# Production - with certificate verification
# (Combine with TOFU for full validation)
context = create_client_context(
verify_mode=ssl.CERT_REQUIRED,
check_hostname=True
)
# With client certificate authentication
context = create_client_context(
verify_mode=ssl.CERT_REQUIRED,
check_hostname=True,
certfile="client.pem",
keyfile="client-key.pem"
)
Creating Server Contexts¶
from nauyaca.security.tls import create_server_context
# Basic server
context = create_server_context(
certfile="server.pem",
keyfile="server-key.pem"
)
# Server requesting client certificates
# (Used with CertificateAuth middleware)
context = create_server_context(
certfile="server.pem",
keyfile="server-key.pem",
request_client_cert=True,
client_ca_certs=["trusted_client1.pem", "trusted_client2.pem"]
)
Client Certificate Authentication
When using request_client_cert=True with OpenSSL 3.x, you must provide client_ca_certs or the TLS handshake will fail silently for self-signed client certificates. For self-signed client certs, include each client's certificate file in the client_ca_certs list.
Integration Examples¶
Custom Client with TOFU Validation¶
import asyncio
import ssl
from pathlib import Path
from nauyaca.security.tls import create_client_context
from nauyaca.security.tofu import TOFUDatabase, CertificateChangedError
async def fetch_with_tofu(url: str):
"""Fetch a Gemini URL with TOFU validation."""
# Parse URL
from urllib.parse import urlparse
parsed = urlparse(url)
hostname = parsed.hostname or "localhost"
port = parsed.port or 1965
# Initialize TOFU database
tofu = TOFUDatabase()
# Create SSL context
ssl_context = create_client_context(
verify_mode=ssl.CERT_NONE, # We do our own TOFU validation
check_hostname=False
)
# Connect and get certificate
reader, writer = await asyncio.open_connection(
hostname, port, ssl=ssl_context
)
# Get peer certificate
ssl_object = writer.get_extra_info('ssl_object')
cert_der = ssl_object.getpeercert(binary_form=True)
# Parse certificate
from cryptography import x509
cert = x509.load_der_x509_certificate(cert_der)
# Verify with TOFU
is_valid, reason = tofu.verify(hostname, port, cert)
if reason == "first_use":
# Prompt user to trust
from nauyaca.security.certificates import get_certificate_info
info = get_certificate_info(cert)
print("First connection to this host!")
print(f"Fingerprint: {info['fingerprint_sha256']}")
response = input("Trust this certificate? [y/N]: ")
if response.lower() == 'y':
tofu.trust(hostname, port, cert)
else:
writer.close()
await writer.wait_closed()
raise ValueError("Certificate not trusted")
elif not is_valid:
# Certificate changed - potential security issue
writer.close()
await writer.wait_closed()
raise CertificateChangedError(
hostname, port,
tofu.get_host_info(hostname, port)['fingerprint'],
cert.fingerprint(ssl.create_default_context().check_hostname)
)
# Send request
writer.write(f"{url}\r\n".encode())
await writer.drain()
# Read response
response = await reader.read()
# Close connection
writer.close()
await writer.wait_closed()
return response.decode('utf-8')
# Use the function
asyncio.run(fetch_with_tofu("gemini://example.com/"))
Programmatic Certificate Generation¶
from pathlib import Path
from nauyaca.security.certificates import (
generate_self_signed_cert,
get_certificate_fingerprint,
load_certificate
)
def setup_server_certificate(hostname: str, cert_dir: Path):
"""Generate and save server certificate."""
cert_dir.mkdir(parents=True, exist_ok=True)
cert_path = cert_dir / "cert.pem"
key_path = cert_dir / "key.pem"
# Generate certificate
print(f"Generating certificate for {hostname}...")
cert_pem, key_pem = generate_self_signed_cert(
hostname=hostname,
key_size=2048,
valid_days=365
)
# Save to files
cert_path.write_bytes(cert_pem)
key_path.write_bytes(key_pem)
# Display fingerprint
cert = load_certificate(cert_path)
fingerprint = get_certificate_fingerprint(cert)
print(f"Certificate saved to {cert_path}")
print(f"Private key saved to {key_path}")
print(f"Fingerprint: {fingerprint}")
return cert_path, key_path
# Example usage
cert_path, key_path = setup_server_certificate(
"localhost",
Path("/etc/nauyaca/certs")
)
See Also¶
- Security Model Explanation - Understand TOFU and why it's used
- Server Configuration Reference - Configure TLS for servers
- CLI Reference - Use the
tofucommand for certificate management - How to Set Up TOFU - Step-by-step TOFU setup guide