# Plan: Migrate FreeIPA DNSSEC and PKI HSM from SoftHSM to Kryoptic

## Context

FreeIPA uses SoftHSM2 as a software PKCS#11 token for two purposes:
1. **DNSSEC key management** — storing DNSSEC signing keys for ipa-dnskeysyncd, ipa-ods-exporter, and BIND/named
2. **PKI HSM testing** — allowing Dogtag CA to be deployed with a software HSM for testing

Kryoptic is a modern Rust-based PKCS#11 implementation that uses SQLite storage, supports PKCS#11 v3.2, and ships a built-in `softhsm_migrate` tool for migrating existing SoftHSM tokens. This plan replaces all SoftHSM usage with Kryoptic across new installations, upgrades (with token migration), and tests.

**Decisions made:**
- PIN files renamed from `softhsm_pin` → `hsm_pin` (and `softhsm_pin_so` → `hsm_pin_so`)
- Kryoptic is a hard requirement on all platforms (no SoftHSM fallback)
- PKI/Dogtag HSM testing also migrates to Kryoptic

---

## Part 1: New Installations

### Patch 1 — Add Kryoptic path constants and rename SoftHSM-specific symbols

**Files:**

- `ipaplatform/base/paths.py` — Add new constants, keep old ones as deprecated aliases:
  ```python
  # Kryoptic PKCS#11 token
  DNSSEC_KRYOPTIC_CONF = "/etc/ipa/dnssec/kryoptic.conf"
  LIBKRYOPTIC_SO = "/usr/lib/pkcs11/libkryoptic_pkcs11.so"
  LIBKRYOPTIC_SO_64 = "/usr/lib64/pkcs11/libkryoptic_pkcs11.so"
  SOFTHSM_MIGRATE = "/usr/bin/softhsm_migrate"

  # Backend-neutral PIN paths (new file locations)
  DNSSEC_HSM_PIN = "/var/lib/ipa/dnssec/hsm_pin"
  DNSSEC_HSM_PIN_SO = "/etc/ipa/dnssec/hsm_pin_so"

  # Deprecated aliases — kept for backward compatibility
  DNSSEC_SOFTHSM_PIN = DNSSEC_HSM_PIN
  DNSSEC_SOFTHSM_PIN_SO = DNSSEC_HSM_PIN_SO
  DNSSEC_SOFTHSM2_CONF = "/etc/ipa/dnssec/softhsm2.conf"  # needed for upgrade detection
  SOFTHSM2_UTIL = "/usr/bin/softhsm2-util"  # needed for upgrade detection
  ```

- `ipaplatform/redhat/paths.py` — Add 64-bit Kryoptic path override (within `sys.maxsize > 2**32` block):
  ```python
  LIBKRYOPTIC_SO = BasePathNamespace.LIBKRYOPTIC_SO_64
  ```

- `ipaplatform/debian/paths.py` — Add Debian multiarch Kryoptic path:
  ```python
  LIBKRYOPTIC_SO = "/usr/lib/{0}/kryoptic/libkryoptic_pkcs11.so".format(MULTIARCH)
  ```

- `ipalib/constants.py` — Rename token label constant:
  ```python
  DNSSEC_TOKEN_LABEL = u'ipaDNSSEC'
  SOFTHSM_DNSSEC_TOKEN_LABEL = DNSSEC_TOKEN_LABEL  # backward compat alias
  ```

### Patch 2 — Enhance p11helper with C_InitToken / C_InitPIN

**File:** `ipaserver/p11helper.py`

- Expose full function signatures for `C_InitToken` and `C_InitPIN` in the CFFI `cdef` (currently typedef'd to opaque pointers at ~lines 116-117)
- Add a new function `init_token(library_path, label, so_pin, user_pin)` that:
  1. Loads the PKCS#11 library
  2. Calls `C_Initialize`
  3. Gets slot list via `C_GetSlotList`
  4. Calls `C_InitToken(slot, so_pin, len, label_padded_32bytes)`
  5. Opens SO session, logs in as CKU_SO
  6. Calls `C_InitPIN(session, user_pin, len)`
  7. Logs out, closes session, finalizes
- Update SoftHSM-specific comments (lines 915-917: label padding comment → make generic about PKCS#11 spec)

### Patch 3 — Update OpenSSL and OpenDNSSEC templates

**Files:**

- `install/share/bind.openssl.cnf.template` — `$SOFTHSM_MODULE` → `$PKCS11_MODULE`
- `install/share/bind.openssl.provider.cnf.template` — `$SOFTHSM_MODULE` → `$PKCS11_MODULE`, `$SOFTHSM_PIN` → `$PKCS11_PIN`
- `install/share/bind.openssl.cryptopolicy.cnf.template` — `$SOFTHSM_MODULE` → `$PKCS11_MODULE`
- `install/share/bind.openssl.provider.crp.cnf.template` — `$SOFTHSM_MODULE` → `$PKCS11_MODULE`, `$SOFTHSM_PIN` → `$PKCS11_PIN`
- `install/share/opendnssec_conf.template` — `$SOFTHSM_LIB` → `$PKCS11_LIB`, Repository name `"SoftHSM"` → `"Kryoptic"`

### Patch 4 — Rewrite DNSSEC installation to use Kryoptic

**File:** `ipaserver/install/dnskeysyncinstance.py`

- Rename `__setup_softhsm()` → `__setup_hsm()`. Step description: `"setting up PKCS#11 token"`
- New `__setup_hsm()` logic:
  - Create DNSSEC dir (unchanged)
  - Write Kryoptic TOML config to `paths.DNSSEC_KRYOPTIC_CONF`:
    ```toml
    [[slots]]
    slot = 1
    dbtype = "sqlite"
    dbargs = "/var/lib/ipa/dnssec/tokens/kryoptic.sql"
    ```
  - Call `setup_named_openssl_conf()`, `setup_named_sysconfig()`, `setup_ipa_dnskeysyncd_sysconfig()`
  - Check for existing token (PIN files + kryoptic.sql existence)
  - Generate PINs, write to `paths.DNSSEC_HSM_PIN` (0o660, ods:named) and `paths.DNSSEC_HSM_PIN_SO` (0o400, root)
  - Initialize token via `p11helper.init_token()` with `KRYOPTIC_CONF` env var set

- Update `setup_named_openssl_conf()` (lines 169-207):
  - `conf_file_dict` keys: `'PKCS11_MODULE': paths.LIBKRYOPTIC_SO`, `'PKCS11_PIN': paths.DNSSEC_HSM_PIN`
  - Comments: remove SoftHSM references

- Update `setup_named_sysconfig()` (lines 209-251):
  - Set `KRYOPTIC_CONF` instead of `SOFTHSM2_CONF`

- Update `setup_ipa_dnskeysyncd_sysconfig()` (lines 253-276):
  - Set `KRYOPTIC_CONF` instead of `SOFTHSM2_CONF`
  - Set `DNSSEC_HSM_PIN` instead of `DNSSEC_SOFTHSM_PIN`

- Update `__setup_replica_keys()` (line 369+):
  - Read `paths.DNSSEC_HSM_PIN`, set `KRYOPTIC_CONF` env, use `paths.LIBKRYOPTIC_SO`

- Update `uninstall()` (lines 543-579):
  - Remove `paths.DNSSEC_HSM_PIN`, `paths.DNSSEC_KRYOPTIC_CONF`, `paths.DNSSEC_OPENSSL_CONF`
  - Also remove legacy `paths.DNSSEC_SOFTHSM2_CONF`, legacy PIN paths if they exist (for upgrades-in-progress)

**File:** `ipaserver/install/opendnssecinstance.py`

- `__init__` conf_file_dict: `'PKCS11_LIB': paths.LIBKRYOPTIC_SO` (was `SOFTHSM_LIB`)
- `__setup_conf_files()` (line 148): `paths.DNSSEC_HSM_PIN` (was `DNSSEC_SOFTHSM_PIN`)
- `__setup_ownership_file_modes()` (~line 189): Set `KRYOPTIC_CONF` (was `SOFTHSM2_CONF`)
- `__generate_master_key()` (~line 226): Use `paths.DNSSEC_HSM_PIN`, `KRYOPTIC_CONF`, `paths.LIBKRYOPTIC_SO`

**File:** `ipaserver/install/odsexporterinstance.py` (~line 67-68)

- Set `KRYOPTIC_CONF` instead of `SOFTHSM2_CONF`

**File:** `ipaserver/install/dns.py`

- Line 249: Step description → `"Configure PKCS#11 token (required by DNSSEC)"`
- Line 327: env var → `"KRYOPTIC_CONF": paths.DNSSEC_KRYOPTIC_CONF`

### Patch 5 — Update DNSSEC daemon scripts

**File:** `daemons/dnssec/ipa-dnskeysync-replica.in` (lines 178-183)

- Import `DNSSEC_TOKEN_LABEL` (alias works)
- Use `paths.DNSSEC_HSM_PIN`, `paths.LIBKRYOPTIC_SO`, `DNSSEC_TOKEN_LABEL`

**File:** `daemons/dnssec/ipa-ods-exporter.in` (lines 711-712)

- Use `paths.LIBKRYOPTIC_SO`, `DNSSEC_TOKEN_LABEL`, `paths.DNSSEC_HSM_PIN`

**File:** `ipaserver/dnssec/localhsm.py` (`__main__` block, lines 190-194)

- Set `KRYOPTIC_CONF` env var, use `paths.LIBKRYOPTIC_SO`, `DNSSEC_TOKEN_LABEL`, `paths.DNSSEC_HSM_PIN`

**File:** `ipaserver/dnssec/bindmgr.py` (lines 129, 134)

- `paths.DNSSEC_HSM_PIN` instead of `DNSSEC_SOFTHSM_PIN`
- `DNSSEC_TOKEN_LABEL` instead of `SOFTHSM_DNSSEC_TOKEN_LABEL`

### Patch 6 — Update systemd service and tmpfiles.d

**File:** `daemons/dnssec/ipa-dnskeysyncd.service.in` (line 7)

The `ExecStartPre` sed command substitutes `@DNSSEC_SOFTHSM_PIN@` from the sysconfig env var `${DNSSEC_SOFTHSM_PIN}` into the tmpfiles template. Update both:
- sysconfig env var name: `DNSSEC_HSM_PIN`
- sed pattern: `s,@DNSSEC_HSM_PIN@,${DNSSEC_HSM_PIN},g`

**File:** `init/tmpfilesd/ipa-dnssec.conf.in` (line 4)

- `@DNSSEC_SOFTHSM_PIN@` → `@DNSSEC_HSM_PIN@`

### Patch 7 — Update p11-kit module configuration

**File:** `ipaplatform/redhat/tasks.py`

- `PKCS11_MODULES`: change from `('softhsm2', paths.LIBSOFTHSM2_SO, ['p11-kit-proxy'])` to `('kryoptic', paths.LIBKRYOPTIC_SO, ['p11-kit-proxy'])`
- Update docstrings in `configure_pkcs11_modules()` / `restore_pkcs11_modules()` to say "Kryoptic" instead of "SoftHSM2"

**File:** `ipaplatform/base/tasks.py`

- Update docstrings/comments referencing SoftHSM2

### Patch 8 — Update PKI HSM testing config

**File:** `install/share/ipaca_softhsm2.ini` → rename to `install/share/ipaca_kryoptic.ini`

```ini
[DEFAULT]
pki_hsm_enable=True
pki_hsm_libfile=%(kryoptic_so)s
pki_hsm_modulename=kryoptic
pki_token_name=kryoptic_token
```

**File:** `install/share/Makefile.am` (line 103)

- `ipaca_softhsm2.ini` → `ipaca_kryoptic.ini`

**File:** `ipaserver/install/dogtaginstance.py`

- Line 1094: `softhsm2_so=paths.LIBSOFTHSM2_SO` → `kryoptic_so=paths.LIBKRYOPTIC_SO`
- Line 1300: `ipaca_softhsm2.ini` → `ipaca_kryoptic.ini`

### Patch 9 — SELinux policy and file contexts

**File:** `selinux/ipa.fc`

- Line 29: `/var/lib/ipa/dnssec/softhsm_pin` → `/var/lib/ipa/dnssec/hsm_pin`
- Add: `/etc/ipa/dnssec/kryoptic\.conf -- gen_context(system_u:object_r:ipa_dnskey_t,s0)`
- Add: `/etc/ipa/dnssec/hsm_pin_so -- gen_context(system_u:object_r:ipa_dnskey_t,s0)` (if not already covered by a directory wildcard)

**File:** `selinux/ipa.te`

- Lines 397-403: Update comment from "SoftHSM" to "PKCS#11 token". The actual `allow` rules don't change — they apply to `ipa_dnskey_t` labeled files regardless of backend.
- Verify that SQLite file operations (lock, mmap) are permitted. If not, add:
  ```
  allow ipa_dnskey_t ipa_dnskey_t:file { lock };
  ```

### Patch 10 — Backup and restore

**File:** `ipaserver/install/ipa_backup.py` (lines 191-192)

Replace:
```python
paths.DNSSEC_SOFTHSM2_CONF,
paths.DNSSEC_SOFTHSM_PIN_SO,
```
With:
```python
paths.DNSSEC_KRYOPTIC_CONF,
paths.DNSSEC_HSM_PIN_SO,
# Legacy files for backward compatibility with old backups
paths.DNSSEC_SOFTHSM2_CONF,
```

(Backup code should already handle missing files gracefully.)

**File:** `ipaserver/install/ipa_restore.py`

- No path changes needed — `DNSSEC_TOKENS_DIR` is unchanged and works for both backends
- Add post-restore logic: if restoring a SoftHSM backup on a Kryoptic-only system, clear the `kryoptic_migration` upgrade state so the next `ipa-server-upgrade` triggers migration

### Patch 11 — RPM spec

**File:** `freeipa.spec.in`

- Remove `softhsm_version` macros (lines 178-183)
- Add: `%global kryoptic_version 1.5.0`
- Line 410: `BuildRequires: softhsm` → `BuildRequires: kryoptic`
- Line 528: `Requires: softhsm >= 2.0.0rc1-1` → `Requires: kryoptic >= %{kryoptic_version}`
- Line 692: `Requires: softhsm >= %{softhsm_version}` → `Requires: kryoptic >= %{kryoptic_version}` and add `Requires: kryoptic-tools` (for `softhsm_migrate`)
- Add `Requires: opensc` (provides `pkcs11-tool`, unless we rely purely on p11helper for token init)

---

## Part 2: Upgrades (SoftHSM → Kryoptic migration)

### Patch 12 — Add migration function in upgrade.py

**File:** `ipaserver/install/server/upgrade.py`

Add function `dnssec_migrate_softhsm_to_kryoptic(dnskeysyncd)`:

1. **Guard:** Check `sysupgrade.get_upgrade_state('dns', 'kryoptic_migration')` — skip if already done
2. **Detect SoftHSM:** Check `os.path.exists(paths.DNSSEC_SOFTHSM2_CONF)` — if absent, mark done and return
3. **Check for initialized token:** Verify old PIN file exists at `/var/lib/ipa/dnssec/softhsm_pin` (the legacy path, before rename)
4. **Generate Kryoptic config:** Write TOML to `paths.DNSSEC_KRYOPTIC_CONF`
5. **Read existing PINs** from old paths (`/var/lib/ipa/dnssec/softhsm_pin`, `/etc/ipa/dnssec/softhsm_pin_so`)
6. **Initialize Kryoptic token** via `p11helper.init_token()` using same label (`ipaDNSSEC`) and same PINs
7. **Find SoftHSM token directory:** Scan `paths.DNSSEC_TOKENS_DIR` for subdirectories containing `.object` files (SoftHSM v2 file store)
8. **Run `softhsm_migrate`:**
   ```bash
   softhsm_migrate \
       -m /usr/lib64/pkcs11/libkryoptic_pkcs11.so \
       -i kryoptic_conf=/etc/ipa/dnssec/kryoptic.conf \
       -p <user_pin> \
       -q <user_pin> \
       /var/lib/ipa/dnssec/tokens/<uuid_dir>
   ```
9. **Rename PIN files:** `softhsm_pin` → `hsm_pin`, `softhsm_pin_so` → `hsm_pin_so` (preserve permissions and ownership)
10. **Update sysconfig files:** Call updated `setup_named_sysconfig()`, `setup_ipa_dnskeysyncd_sysconfig()`, etc.
11. **Remove stale `SOFTHSM2_CONF`** directives from all sysconfig files
12. **Remove stale p11-kit module file:** `/etc/pkcs11/modules/softhsm2.module`
13. **Verify migration:** Open Kryoptic token via p11helper, verify token label and at least one key is accessible
14. **Cleanup:** Remove old SoftHSM config (`softhsm2.conf`). Leave old token directory (UUID subdirs) as backup for one release cycle.
15. **Mark done:** `sysupgrade.set_upgrade_state('dns', 'kryoptic_migration', True)`

**Error handling:** If `softhsm_migrate` fails (exit code > 0):
- Log the error with details
- Remove partially-created Kryoptic SQLite file
- Do NOT rename PIN files
- Do NOT update sysconfig files
- Leave upgrade state unset so it retries on next `ipa-server-upgrade`
- The system continues running on SoftHSM until migration succeeds

**Integration point** (around line 1872-1880):
```python
if bindinstance.named_conf_exists():
    dnskeysyncd = dnskeysyncinstance.DNSKeySyncInstance(fstore)
    if not dnskeysyncd.is_configured():
        dnskeysyncd.create_instance(fqdn, api.env.realm)
        dnskeysyncd.start_dnskeysyncd()
    else:
        changed = False
        if dnssec_migrate_softhsm_to_kryoptic(dnskeysyncd):
            changed = True
        if dnssec_set_openssl_provider(dnskeysyncd):
            changed = True
        if changed:
            dnskeysyncd.start_dnskeysyncd()
```

### Multi-replica considerations

- Each replica has its own local PKCS#11 token store. Migration happens independently per server during `ipa-server-upgrade`.
- LDAP-stored keys (wrapped key blobs, public keys) are backend-agnostic. No LDAP schema or data changes needed.
- During the transition window (some replicas on SoftHSM, some on Kryoptic), the system functions normally — the PKCS#11 API abstraction hides the backend. Key replication between replicas happens via LDAP (wrapped keys), not via PKCS#11.

---

## Part 3: Tests

### Patch 13 — Update unit test for p11helper

**File:** `ipatests/test_ipaserver/test_ipap11helper.py`

- Write Kryoptic TOML config instead of SoftHSM INI in the fixture
- Set `KRYOPTIC_CONF` env var instead of `SOFTHSM2_CONF`
- Initialize token via `p11helper.init_token()` instead of `softhsm2-util`
- Use `paths.LIBKRYOPTIC_SO` instead of `libsofthsm2.so`

### Patch 14 — Update integration tests

**File:** `ipatests/test_integration/test_installation.py`

- `test_p11_kit_softhsm2` (line 975) → rename to `test_p11_kit_kryoptic`, check `"kryoptic" not in result.stdout_text.lower()`
- Lines 346-356 (PKI HSM test): Update `softhsm` references to `kryoptic` (library path, token init commands)

**File:** `ipatests/test_integration/test_hsm.py`

- Update `get_hsm_token()`: use `pkcs11-tool` or p11helper-based init instead of `softhsm2-util`
- Update `delete_hsm_token()`: remove Kryoptic SQLite file instead of `softhsm2-util --delete-token`
- Update `find_softhsm_token_files()` → `find_kryoptic_token_files()`: adapt for SQLite storage
- Update default `hsm_lib_path` from `libsofthsm2.so` to `libkryoptic_pkcs11.so`

**File:** `ipatests/test_integration/test_uninstallation.py`

- Line 182: `paths.DNSSEC_SOFTHSM_PIN_SO` → `paths.DNSSEC_HSM_PIN_SO`

**File:** `ipatests/test_integration/test_pki_config_override.py`

- Lines 26-27: Update `libsofthsm2.so` → `libkryoptic_pkcs11.so`, `libsofthsm2` → `kryoptic`

### Patch 15 — Add migration integration test (new)

Add a test class that validates the SoftHSM → Kryoptic upgrade path:
1. Verify that after `ipa-server-upgrade`, Kryoptic config exists
2. Verify Kryoptic token is accessible and keys are present
3. Verify DNSSEC signing still works
4. Verify sysconfig files reference `KRYOPTIC_CONF` (not `SOFTHSM2_CONF`)
5. Verify PIN files are renamed to `hsm_pin` / `hsm_pin_so`
6. Verify old SoftHSM config is removed

---

## Critical Files Summary

| File | Change Type |
|------|-------------|
| `ipaplatform/base/paths.py` | Add Kryoptic constants, rename PIN paths |
| `ipaplatform/redhat/paths.py` | Add 64-bit Kryoptic path |
| `ipaplatform/debian/paths.py` | Add Debian Kryoptic path |
| `ipalib/constants.py` | Rename token label constant |
| `ipaserver/p11helper.py` | Expose C_InitToken/C_InitPIN, add init_token() |
| `ipaserver/install/dnskeysyncinstance.py` | Core rewrite: SoftHSM → Kryoptic |
| `ipaserver/install/opendnssecinstance.py` | Library and config path updates |
| `ipaserver/install/odsexporterinstance.py` | Sysconfig env var update |
| `ipaserver/install/dns.py` | Env var and step description |
| `ipaserver/install/dogtaginstance.py` | PKI HSM config → Kryoptic |
| `ipaserver/install/server/upgrade.py` | Migration function |
| `ipaserver/dnssec/localhsm.py` | Library path and env var |
| `ipaserver/dnssec/bindmgr.py` | PIN path and token label |
| `daemons/dnssec/ipa-dnskeysyncd.service.in` | Env var in sed command |
| `daemons/dnssec/ipa-dnskeysync-replica.in` | Library and PIN paths |
| `daemons/dnssec/ipa-ods-exporter.in` | Library and PIN paths |
| `init/tmpfilesd/ipa-dnssec.conf.in` | PIN path substitution variable |
| `install/share/bind.openssl.*.template` (4 files) | Template variable rename |
| `install/share/opendnssec_conf.template` | Template variable + repo name |
| `install/share/ipaca_softhsm2.ini` → `ipaca_kryoptic.ini` | Rename + content update |
| `install/share/Makefile.am` | File listing update |
| `ipaplatform/redhat/tasks.py` | p11-kit module config |
| `ipaplatform/base/tasks.py` | Docstring updates |
| `selinux/ipa.fc` | File context for renamed PIN + kryoptic.conf |
| `selinux/ipa.te` | Comment updates, verify SQLite lock perms |
| `ipaserver/install/ipa_backup.py` | Backup file list |
| `ipaserver/install/ipa_restore.py` | Post-restore migration trigger |
| `freeipa.spec.in` | Package dependencies |
| `ipatests/test_ipaserver/test_ipap11helper.py` | Unit test rewrite |
| `ipatests/test_integration/test_installation.py` | Integration test updates |
| `ipatests/test_integration/test_hsm.py` | HSM test updates |
| `ipatests/test_integration/test_uninstallation.py` | Path constant update |
| `ipatests/test_integration/test_pki_config_override.py` | PKI config test update |

---

## Verification

1. **New install test:** Run `ipa-server-install` with DNS. Verify:
   - `/etc/ipa/dnssec/kryoptic.conf` exists with correct TOML content
   - `/var/lib/ipa/dnssec/tokens/kryoptic.sql` exists
   - `/var/lib/ipa/dnssec/hsm_pin` exists with correct permissions (0o660, ods:named)
   - `/etc/ipa/dnssec/hsm_pin_so` exists with correct permissions (0o400, root)
   - `KRYOPTIC_CONF` set in `/etc/sysconfig/named`, `/etc/sysconfig/ipa-dnskeysyncd`
   - No `SOFTHSM2_CONF` in any sysconfig
   - DNSSEC zone signing works end-to-end
   - `p11-kit list-modules` does NOT show kryoptic (p11-kit-proxy disabled)

2. **Upgrade test:** Start with existing SoftHSM installation, run `ipa-server-upgrade`. Verify:
   - `softhsm_migrate` ran successfully
   - Kryoptic token accessible with same keys
   - PIN files renamed from `softhsm_pin` → `hsm_pin`
   - Old `softhsm2.conf` removed
   - Sysconfig files updated
   - DNSSEC signing still works
   - `sysupgrade` state `dns/kryoptic_migration` is True

3. **Multi-replica test:** Upgrade one replica at a time. Verify DNSSEC key replication works across mixed SoftHSM/Kryoptic replicas.

4. **Backup/restore test:** Back up a Kryoptic system, restore it. Verify token integrity.

5. **SELinux test:** Run all above under SELinux enforcing mode. Check for AVCs related to Kryoptic SQLite operations.

6. **Unit tests:** `pytest ipatests/test_ipaserver/test_ipap11helper.py`

7. **PKI HSM test:** Install with `--pki-config-override=ipaca_kryoptic.ini` and verify Dogtag starts correctly.
