Skip to content

wasat.certs

Client certificate generation and storage management for Gemini connections.

ClientCertCallback

ClientCertCallback = Callable[
    [GeminiURI, ClientCertificateStore],
    Coroutine[
        None,
        None,
        Literal["transient", "persistent", "ignore"],
    ],
]

Async callback function signature for resolving a client certificate requirement.

This callback is invoked when a client certificate is required. It receives the requested GeminiURI and the ClientCertificateStore instance to query or update.

ClientCertificateStore

Bases: Protocol

Protocol defining the interface for client certificate storage and retrieval.

close async

close() -> None

Close the store, cleaning up transient resources if necessary.

create_credentials async

create_credentials(
    uri: GeminiURI,
    *,
    transient: bool = False,
    common_name: str | None = None,
    valid_days: int | None = 365,
    key_type: Literal["ecdsa", "rsa"] = "ecdsa",
    rsa_key_size: int = 2048,
    ecdsa_curve: str = "secp256r1",
    email: str | None = None,
    user_id: str | None = None,
    domain: str | None = None,
    organisation: str | None = None,
    country: str | None = None,
) -> tuple[Path, Path]

Generate and save a new self-signed client certificate and private key.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

transient

bool

If True, the certificate is generated in a temporary directory and not registered in the persistent store.

False

common_name

str | None

The Common Name (CN) for the certificate. Defaults to the host.

None

valid_days

int | None

Number of days the certificate should be valid. If None, the certificate will expire on 9999-12-31.

365

key_type

Literal['ecdsa', 'rsa']

The key type to generate ('ecdsa' or 'rsa').

'ecdsa'

rsa_key_size

int

RSA key size in bits.

2048

ecdsa_curve

str

ECDSA curve name.

'secp256r1'

email

str | None

Optional email address.

None

user_id

str | None

Optional user identifier.

None

domain

str | None

Optional domain name for Subject Alternative Name.

None

organisation

str | None

Optional organisation name.

None

country

str | None

Optional two-letter country code.

None

Returns:

Type Description
tuple[Path, Path]

A tuple of (cert_path, key_path) representing the generated certificate and key.

Raises:

Type Description
ValueError

If the key type is unsupported, the RSA key size is not one of 2048, 3072, or 4096, the ECDSA curve is not 'secp256r1' or 'secp384r1', or the country code is not exactly two characters.

OSError

If creating directories or writing the certificate or key file to disk fails.

RuntimeError

If saving the credentials or updating the store index fails.

delete_credentials async

delete_credentials(uri: GeminiURI) -> bool

Delete the certificate and key associated with the matching scope.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

Returns:

Type Description
bool

True if deleted, False if no matching scope was found.

Raises:

Type Description
RuntimeError

If updating the store index fails.

get_credentials async

get_credentials(uri: GeminiURI) -> tuple[Path, Path] | None

Retrieve the certificate and private key paths matching the given URI.

This should perform path prefix matching to find the most specific matching certificate for the requested host, port, and path.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

Returns:

Type Description
tuple[Path, Path] | None

A tuple of (cert_path, key_path) or None if no certificate is stored

tuple[Path, Path] | None

for this URI's scope.

has_exact_credentials async

has_exact_credentials(uri: GeminiURI) -> bool

Check if a certificate exists for the exact scope of the URI.

This checks if a certificate has been registered specifically for the exact host, port, and path of the given URI (without traversing parent scopes).

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

Returns:

Type Description
bool

True if a certificate is registered for this exact scope, False otherwise.

register_credentials async

register_credentials(
    uri: GeminiURI,
    cert_path: str | Path,
    key_path: str | Path,
    *,
    transient: bool = False,
) -> None

Register existing certificate and private key paths for the URI's scope.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

cert_path

str | Path

Path to the existing client certificate.

required

key_path

str | Path

Path to the existing private key.

required

transient

bool

If True, registers as transient.

False

Raises:

Type Description
FileNotFoundError

If the registry is persistent, and the source files do not exist at the specified paths.

OSError

If copying the certificate or private key files fails, or if creating the persistent store directory fails.

RuntimeError

If registering or persisting the credentials in the store index fails.

FileClientCertificateStore

FileClientCertificateStore(store_dir: str | Path)

Bases: ClientCertificateStore

File-based client certificate and key store.

Saves certificate files as PEM pairs and maintains a certs.json registry file mapping Gemini scopes (host[:port]/path) to certificate filenames.

Parameters:

Name Type Description Default

store_dir

str | Path

The directory where certificates, keys, and the index are stored.

required

store_dir instance-attribute

store_dir = Path(store_dir)

The directory path for storing certificates and index.

close async

close() -> None

Close the store, cleaning up transient directories if any were created.

create_credentials async

create_credentials(
    uri: GeminiURI,
    *,
    transient: bool = False,
    common_name: str | None = None,
    valid_days: int | None = 365,
    key_type: Literal["ecdsa", "rsa"] = "ecdsa",
    rsa_key_size: int = 2048,
    ecdsa_curve: str = "secp256r1",
    email: str | None = None,
    user_id: str | None = None,
    domain: str | None = None,
    organisation: str | None = None,
    country: str | None = None,
) -> tuple[Path, Path]

Generate and save a new self-signed client certificate and private key.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

transient

bool

If True, the certificate is generated in a temporary directory and not registered in the persistent store.

False

common_name

str | None

The Common Name (CN) for the certificate. Defaults to the host.

None

valid_days

int | None

Number of days the certificate should be valid. If None, the certificate will expire on 9999-12-31.

365

key_type

Literal['ecdsa', 'rsa']

The key type to generate ('ecdsa' or 'rsa').

'ecdsa'

rsa_key_size

int

RSA key size in bits.

2048

ecdsa_curve

str

ECDSA curve name.

'secp256r1'

email

str | None

Optional email address.

None

user_id

str | None

Optional user identifier.

None

domain

str | None

Optional domain name for Subject Alternative Name.

None

organisation

str | None

Optional organisation name.

None

country

str | None

Optional two-letter country code.

None

Returns:

Type Description
tuple[Path, Path]

A tuple of (cert_path, key_path) representing the generated certificate and key.

Raises:

Type Description
ValueError

If the key type is unsupported, the RSA key size is not one of 2048, 3072, or 4096, the ECDSA curve is not 'secp256r1' or 'secp384r1', or the country code is not exactly two characters.

OSError

If creating the persistent store directory or the temporary transient directory fails, or if writing the certificate or key file to disk fails.

RuntimeError

If saving the updated index (certs.json) file to disk fails.

delete_credentials async

delete_credentials(uri: GeminiURI) -> bool

Delete the certificate and key associated with the matching scope.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

Returns:

Type Description
bool

True if deleted, False if no matching scope was found.

Raises:

Type Description
RuntimeError

If saving the updated index (certs.json) file to disk fails.

get_credentials async

get_credentials(uri: GeminiURI) -> tuple[Path, Path] | None

Retrieve the certificate and private key paths matching the given URI.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

Returns:

Type Description
tuple[Path, Path] | None

A tuple of (cert_path, key_path) or None if no certificate is stored

tuple[Path, Path] | None

for this URI's scope.

has_exact_credentials async

has_exact_credentials(uri: GeminiURI) -> bool

Check if a certificate exists for the exact scope of the URI.

This checks if a certificate has been registered specifically for the exact host, port, and path of the given URI (without traversing parent scopes).

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

Returns:

Type Description
bool

True if a certificate is registered for this exact scope, False otherwise.

register_credentials async

register_credentials(
    uri: GeminiURI,
    cert_path: str | Path,
    key_path: str | Path,
    *,
    transient: bool = False,
) -> None

Register existing certificate and private key paths for the URI's scope.

Parameters:

Name Type Description Default

uri

GeminiURI

The target GeminiURI.

required

cert_path

str | Path

Path to the existing client certificate.

required

key_path

str | Path

Path to the existing private key.

required

transient

bool

If True, registers as transient.

False

Raises:

Type Description
FileNotFoundError

If transient is False, and the source certificate file or private key file does not exist at the specified paths.

OSError

If transient is False, and copying the certificate or private key files fails, or if creating the persistent store directory fails.

RuntimeError

If transient is False, and saving the updated index (certs.json) file to disk fails.

generate_self_signed_cert

generate_self_signed_cert(
    common_name: str,
    *,
    key_type: Literal["ecdsa", "rsa"] = "ecdsa",
    rsa_key_size: int = 2048,
    ecdsa_curve: str = "secp256r1",
    valid_days: int | None = 365,
    email: str | None = None,
    user_id: str | None = None,
    domain: str | None = None,
    organisation: str | None = None,
    country: str | None = None,
) -> tuple[bytes, bytes]

Generate a self-signed client certificate and private key.

Parameters:

Name Type Description Default

common_name

str

The Common Name (CN) for the certificate.

required

key_type

Literal['ecdsa', 'rsa']

The key type to generate ('ecdsa' or 'rsa').

'ecdsa'

rsa_key_size

int

RSA key size in bits.

2048

ecdsa_curve

str

ECDSA curve name.

'secp256r1'

valid_days

int | None

Certificate validity in days. If None, the certificate will expire on 9999-12-31.

365

email

str | None

Optional email address.

None

user_id

str | None

Optional user identifier.

None

domain

str | None

Optional domain name for Subject Alternative Name.

None

organisation

str | None

Optional organisation name.

None

country

str | None

Optional two-letter country code.

None

Returns:

Type Description
tuple[bytes, bytes]

A tuple containing (cert_pem, key_pem) as bytes.

Raises:

Type Description
ValueError

If the key type is unsupported, the RSA key size is not one of 2048, 3072, or 4096, the ECDSA curve is not 'secp256r1' or 'secp384r1', or the country code is not exactly two characters.

get_candidate_scopes

get_candidate_scopes(uri: GeminiURI) -> list[str]

Get candidate certificate scopes for a URI, sorted by specificity.

Parameters:

Name Type Description Default

uri

GeminiURI

The GeminiURI to generate scopes for.

required

Returns:

Type Description
list[str]

A list of scope strings in descending order of specificity.