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.

ServerCertificate

ServerCertificate(raw_der: bytes)

Representation of a server TLS X.509 certificate.

Parameters:

Name Type Description Default

raw_der

bytes

The raw DER-encoded certificate bytes.

required

fingerprint property

fingerprint: str

The hex-encoded SHA-256 fingerprint of the certificate.

is_expired property

is_expired: bool

Whether the certificate is currently expired.

is_self_signed property

is_self_signed: bool

Whether the certificate is self-signed (subject equals issuer).

issuer property

issuer: str

The certificate issuer formatted as an RFC 4514 string.

issuer_common_name property

issuer_common_name: str | None

The Common Name (CN) from the certificate issuer, or None if not present.

not_after property

not_after: datetime

The UTC timestamp at which the certificate expires.

not_before property

not_before: datetime

The UTC timestamp from which the certificate is valid.

raw_der property

raw_der: bytes

The raw DER-encoded certificate bytes.

raw_x509 property

raw_x509: Certificate

The underlying cryptography X.509 Certificate instance.

serial_number property

serial_number: int

The certificate serial number.

subject property

subject: str

The certificate subject formatted as an RFC 4514 string.

subject_alternative_names property

subject_alternative_names: tuple[str, ...]

Tuple of Subject Alternative Names (DNS names) in the certificate.

subject_common_name property

subject_common_name: str | None

The Common Name (CN) from the certificate subject, or None if not present.

from_der classmethod

Construct a ServerCertificate instance from raw DER bytes.

Parameters:

Name Type Description Default

cert_der

bytes

The raw DER-encoded certificate bytes.

required

Returns:

Type Description
ServerCertificate

A new ServerCertificate instance.

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.