# PKCS#11 Token Management
The `synta.pkcs11` submodule provides management operations for PKCS#11 tokens:
slot enumeration, key discovery, key deletion, and on-token key generation. It
is backed by the `cryptoki` crate, which wraps the standard PKCS#11 C API.
Management operations open and close their own sessions; they are independent of
signing or verification, which remain the responsibility of the active crypto
backend (`openssl` or `nss` Cargo feature).
## Module resolution
All entry points accept an optional `module` argument. When `module` is omitted:
1. The `module-path=` attribute of the PKCS#11 URI is consulted (where applicable).
2. The `PKCS11_MODULE_PATH` environment variable is checked.
3. Well-known p11-kit-proxy paths are probed in order: `/usr/lib64/pkcs11/p11-kit-proxy.so`
(Fedora/RHEL), `/usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-proxy.so` (Debian/Ubuntu),
`/usr/lib/pkcs11/p11-kit-proxy.so` (generic Linux).
## `list_slots(module=None)`
```text
synta.pkcs11.list_slots(module: str | None = None) -> list[SlotInfo]
```
Return all slots that currently have an initialised token present.
```python
import synta.pkcs11
slots = synta.pkcs11.list_slots()
for s in slots:
print(s.slot_id, s.token_label, s.flags)
```
## `SlotInfo`
Returned by `list_slots()` and `Pkcs11Token.slot`.
| `slot_id` | `int` | PKCS#11 `CK_SLOT_ID` value |
| `token_label` | `str` | Token label from `CK_TOKEN_INFO`, trailing spaces stripped |
| `manufacturer_id` | `str` | Manufacturer ID from `CK_TOKEN_INFO`, trailing spaces stripped |
| `model` | `str` | Model string from `CK_TOKEN_INFO`, trailing spaces stripped |
| `serial_number` | `str` | Serial number from `CK_TOKEN_INFO`, trailing spaces stripped |
| `flags` | `int` | Bitmask of four `CKF_*` bits (see below) |
### `flags` bitmask
Only four bits are populated (PKCS#11 v3.0 §4.9):
| `0x0002` | `CKF_WRITE_PROTECTED` | Token is read-only |
| `0x0004` | `CKF_LOGIN_REQUIRED` | PIN must be presented before accessing private objects |
| `0x0100` | `CKF_PROTECTED_AUTHENTICATION_PATH` | PIN entry is via an on-device keypad |
| `0x0400` | `CKF_TOKEN_INITIALIZED` | Token has been initialised |
All other bits are zero.
## `Pkcs11Token`
A session handle for management operations on a single named token.
```python
token = synta.pkcs11.Pkcs11Token(uri, module=None)
```
`uri` is a `pkcs11:` URI (RFC 7512) that must include `token=<label>`. Include
`?pin-value=<PIN>` in the URI to authenticate automatically on each call.
```python
token = synta.pkcs11.Pkcs11Token(
"pkcs11:token=MySoftHSM2Token0?pin-value=1234"
)
```
### `find_key(object_uri)`
```text
token.find_key(object_uri: str) -> bool
```
Return `True` if a `CKO_PRIVATE_KEY` object whose `CKA_LABEL` matches the
`object=` attribute of `object_uri` exists on this token.
```python
exists = token.find_key("pkcs11:token=MySoftHSM2Token0;object=caKey")
```
### `list_keys()`
```text
token.list_keys() -> list[KeyInfo]
```
Return all `CKO_PRIVATE_KEY` objects on this token.
```python
for k in token.list_keys():
print(k.label, k.key_type, k.key_bits)
```
### `delete_key(object_uri)`
```text
token.delete_key(object_uri: str) -> None
```
Destroy both the `CKO_PRIVATE_KEY` and the `CKO_PUBLIC_KEY` objects whose
`CKA_LABEL` matches `object=` in `object_uri`.
```python
token.delete_key("pkcs11:token=MySoftHSM2Token0;object=caKey")
```
### `generate_key_pair(key_type, param, label, extractable=False)`
```text
token.generate_key_pair(
key_type: str,
param: int | str,
label: str,
extractable: bool = False,
) -> PrivateKey
```
Generate a key pair on the token and return the private key. Both objects are
stored with `CKA_TOKEN = true` so they survive session close.
| `"rsa"` | `int` | modulus bit-length, e.g. `2048`, `3072`, `4096` |
| `"ec"` | `str` | NIST curve: `"P-256"`, `"P-384"`, `"P-521"` |
| `"ed25519"` | ignored | — |
| `"ed448"` | ignored | — |
| `"ml-dsa"` | `str` | `"ML-DSA-44"`, `"ML-DSA-65"`, or `"ML-DSA-87"` |
| `"ml-kem"` | `str` | `"ML-KEM-512"`, `"ML-KEM-768"`, or `"ML-KEM-1024"` |
Set `extractable=True` only when key export is explicitly required (e.g. software
tokens used in backup scenarios). The default `False` generates a
hardware-resident key that cannot leave the token.
The PKCS#11 session used for generation is closed before the key is loaded
back through the active crypto backend. This ensures that the returned
`PrivateKey` carries a fully populated SPKI and is ready for immediate use.
```python
rsa_key = token.generate_key_pair("rsa", 4096, "caKey")
ec_key = token.generate_key_pair("ec", "P-256", "ecKey")
dsa_key = token.generate_key_pair("ml-dsa", "ML-DSA-65", "pqKey")
```
## `KeyInfo`
Returned by `Pkcs11Token.list_keys()`.
| `label` | `str` | `CKA_LABEL` of the private key object |
| `id` | `bytes` | `CKA_ID` bytes; may be empty if the token does not set this attribute |
| `key_type` | `str` | `"RSA"`, `"EC"`, `"Ed"`, `"ML-DSA"`, `"ML-KEM"`, or `"Unknown"` |
| `key_bits` | `int` | RSA modulus length in bits (from `CKA_MODULUS`); `0` for all non-RSA types |
## Full class stubs
```python
class SlotInfo:
slot_id: int
token_label: str
manufacturer_id: str
model: str
serial_number: str
flags: int # CKF_* bitmask; see table above
class KeyInfo:
label: str
id: bytes
key_type: str
key_bits: int
class Pkcs11Token:
def __init__(self, uri: str, module: str | None = None) -> None: ...
def find_key(self, object_uri: str) -> bool: ...
def list_keys(self) -> list[KeyInfo]: ...
def delete_key(self, object_uri: str) -> None: ...
def generate_key_pair(
self,
key_type: str,
param: int | str,
label: str,
extractable: bool = False,
) -> PrivateKey: ...
def list_slots(module: str | None = None) -> list[SlotInfo]: ...
```
## PKCS#11 URI format (RFC 7512)
```
pkcs11:<path-attrs>(?<query-attrs>)
```
- Path attributes are separated by `;`: `token=`, `object=`, `id=`, `module-path=`
- Query attributes follow `?` and are separated by `&`: `pin-value=`
```
pkcs11:token=MySoftHSM2Token0;object=caKey?pin-value=1234
pkcs11:token=MyHSM;object=signingKey;module-path=/opt/vendor/pkcs11.so?pin-value=secret
```
See also [PublicKey and PrivateKey](keys.md) for loading a token-resident key
via `PrivateKey.from_pkcs11_uri` and using it directly for signing.