# Build and install M7 Crypto on AlmaLinux

This walkthrough installs the optional M7 C Crypto library and M7 PHP Crypto
extension for **PHP 8.4 NTS on AlmaLinux 10**. It takes you from development
packages through compilation, correctness and memory tests, installation and
PHP-FPM activation. The example build account is `m7`, with home `/home/m7`.
These are intentional public example paths, not requirements to reproduce an
M7 server layout.

For another distribution, your local LLM or administrator can adapt the package
names, tool paths, service names and security labels. Preserve the dependency
order, matching PHP build tools and test gates. An adapted command sequence is
not evidence of platform support until it passes on that platform.

**Version status:** these examples use the reviewed **0.3.0 development
candidates**. Final releases and public download verification are pending.
The procedure follows earlier AlmaLinux installation experience and current
0.3.0 build/test interfaces; this complete 0.3.0 Linux walkthrough has not yet
been executed and accepted. Do not treat older Linux results or the successful
macOS candidate checks as its acceptance result.

## When you need this optional path

Ordinary Identity SDK use keeps its PHP 8.1+ baseline and supported PHP OpenSSL
paths. Install M7 Crypto when selecting a feature that needs it, including the
SDK's additional native signature profiles, local HMAC verification or encrypted
responses. The current extension initially targets PHP **8.4 NTS, non-debug**;
this is not an installation path for PHP 8.1–8.3. Broader PHP support is later work.

Once the extension is installed **and loaded in the application's PHP process**,
the SDK detects it when a configured operation needs the native backend. There
is no separate backend-selection switch. You still select trusted
`allowed_algs`, supply the appropriate keys/secrets, and explicitly configure
encryption recipients and policy. Installing a module does not enable algorithms,
change client registration or turn encryption on. Required native operations
fail if the module or provider is unavailable; validation is never bypassed.

### A possible custom PHP 8.5 port

PHP 8.5 adds a padding parameter to
[`openssl_sign`](https://www.php.net/manual/en/function.openssl-sign.php) and an
OAEP digest parameter to
[`openssl_public_encrypt`](https://www.php.net/manual/en/function.openssl-public-encrypt.php).
These additions may let a custom SDK adaptation replace some M7 Crypto calls
with PHP OpenSSL calls. **The current SDK does not implement that alternative**,
and PHP 8.5 alone does not remove its native dependency for these features.

Full parity with M7 Crypto has not been established. A port must verify the
exact algorithms and provider support, RSA-PSS/OAEP parameters, ECDSA signature
encoding, Edwards/ML-DSA behavior, key formats, errors and rejection tests it
needs. Re-run SDK conformance and application interoperability tests. Do not
assume that a new PHP version or similarly named operation is interchangeable.

## 1. Check OpenSSL and install development tools

First check the OS and OpenSSL installation:

```bash
cat /etc/os-release
uname -m
openssl version -a
rpm -q openssl openssl-libs openssl-devel
```

The C library needs **OpenSSL 3.0+**, including development headers and
libraries. This walkthrough runs the complete algorithm suite, including
ML-DSA, so use **OpenSSL 3.5+ with an available ML-DSA provider**. Having an
OpenSSL executable alone does not supply its development headers.
See the [OpenSSL ML-DSA contract](https://docs.openssl.org/3.5/man7/EVP_SIGNATURE-ML-DSA/).

As an administrator, install the compiler, build tools and test tools:

```bash
sudo dnf install gcc make pkgconf-pkg-config openssl-devel autoconf \
  libasan libubsan valgrind nodejs tar gzip policycoreutils-python-utils
```

Review the transaction. Keep `openssl-devel` and the installed OpenSSL runtime
from compatible packages; do not replace the system crypto stack with an
unrelated build to satisfy a test. Sanitizer runtimes and Valgrind are memory
test tools. Node.js is used for an independent encryption interoperability test;
they are not extension runtime dependencies.

This example uses AlmaLinux's versioned PHP 8.4 package family. Check what the
enabled repositories supply before installing it:

```bash
dnf info php8.4-cli php8.4-devel php8.4-fpm
```

For a new PHP 8.4 installation from that package family:

```bash
sudo dnf install php8.4-cli php8.4-devel php8.4-fpm
```

If PHP is already installed, identify its owner first:

```bash
command -v php phpize php-config
rpm -qf "$(readlink -f "$(command -v php)")"
```

Install the matching development package from that same PHP provider and
version/release. A `phpize` executable can exist while its headers or
`php-config` are missing. Generic `php-devel`, versioned `php8.4-devel` and
third-party parallel PHP packages are not interchangeable. If the packages
above are unavailable, stop and resolve the PHP 8.4 package/toolchain selection;
do not silently build against another PHP version or switch an existing site's
PHP runtime. PHP's [phpize guide](https://www.php.net/manual/en/install.pecl.phpize.php)
explains selecting a matching `php-config`.

## 2. Use a build account and establish paths

Compile and run tests as the ordinary `m7` account. Create it only if it does
not already exist, using an administrator account:

```bash
if ! id m7 >/dev/null 2>&1; then
  sudo useradd --create-home --home-dir /home/m7 --shell /bin/bash m7
fi
getent passwd m7
```

Open a Bash login shell as `m7` (`sudo -iu m7` from an administrator shell).
Use this same shell for the following steps. Lines using `sudo` are privileged
installation actions; an administrator can run those lines if the build account
does not have sudo permission.

```bash
export m7_crypto_version=0.3.0
export m7_crypto_downloads=/home/m7/downloads
export m7_crypto_src=/home/m7/src
export m7_crypto_c="$m7_crypto_src/m7-c-crypto-$m7_crypto_version"
export m7_crypto_php="$m7_crypto_src/m7-php-crypto-$m7_crypto_version"
export m7_crypto_prefix="/opt/m7/crypto/$m7_crypto_version"
export m7_crypto_run=$(date -u +%Y%m%dT%H%M%SZ)
export m7_crypto_logs="$m7_crypto_src/validation/m7-crypto-$m7_crypto_version-$m7_crypto_run"
export m7_php=/usr/bin/php
export m7_phpize=/usr/bin/phpize
export m7_php_config=/usr/bin/php-config
export m7_openssl=/usr/bin/openssl
mkdir -p "$m7_crypto_downloads" "$m7_crypto_src" "$m7_crypto_logs"
```

Change the four executable paths if your selected PHP/OpenSSL packages install
elsewhere. Each execution block below uses a Bash subshell with
`set -euo pipefail`: a failure stops that block, including failures piped to
`tee`. Read its log and fix the cause before proceeding to the next block.
After reconnecting, restore these variables; retain the original log-directory
value when continuing the same run.

| Location | Purpose |
| --- | --- |
| `/home/m7/downloads/` | Reviewed archives and integrity sidecars |
| `/home/m7/src/` | Unprivileged source builds and validation logs |
| `/opt/m7/crypto/0.3.0/include/` and `lib/` | Root-owned C header and normal shared library |
| `/opt/m7/crypto/0.3.0/php/` | Root-owned PHP module linked to that same C prefix |
| `/etc/php.d/20-m7crypto.ini` | Extension entry, after confirming the target runtime scans this directory |
| `/var/backups/m7crypto/` | Previous extension configuration for rollback |

Confirm the toolchain before building:

```bash
(
set -euo pipefail
cc --version
pkg-config --modversion libcrypto
pkg-config --cflags --libs libcrypto
pkg-config --atleast-version=3.0 libcrypto
pkg-config --atleast-version=3.5 libcrypto
"$m7_openssl" list -signature-algorithms | tee "$m7_crypto_logs/openssl-algorithms.log"
"$m7_php" -n -r '
if (PHP_MAJOR_VERSION !== 8 || PHP_MINOR_VERSION !== 4 || PHP_ZTS || PHP_DEBUG) {
    fwrite(STDERR, "This walkthrough requires PHP 8.4 NTS, non-debug.\n"); exit(1);
}
echo PHP_VERSION, "\n", OPENSSL_VERSION_TEXT, "\n";'
"$m7_phpize" --version
test "$("$m7_php_config" --phpapi)" = 20240924
test "$("$m7_php_config" --version)" = "$("$m7_php" -n -r 'echo PHP_VERSION;')"
"$m7_php_config" --extension-dir
)
```

The second `pkg-config` version check enforces this walkthrough's full-profile
test target, not the library's minimum 3.0 build requirement. Confirm ML-DSA
appears in the provider output and that `phpize` reports module API 20240924.
If `pkg-config` selects a custom OpenSSL, set its `PKG_CONFIG_PATH` deliberately
and verify compatibility with PHP's OpenSSL; a version string alone is not proof
that the dynamic loader will select the intended library.

## 3. Verify and extract both source bundles

Place each reviewed `.tar.gz`, `.tar.gz.sha256` and `.tar.gz.manifest.json`
in `/home/m7/downloads/`. See the
[C release status](https://m7.org/docs/sdk/m7-c-crypto/releases.md) and
[PHP release status](https://m7.org/docs/sdk/m7-php-crypto/releases.md) for availability. Until final downloads
are verified, use only the reviewed candidate files supplied through a trusted
channel; the version in these examples is not a promise that a public URL exists.

```bash
(
set -euo pipefail
cd "$m7_crypto_downloads"
sha256sum -c "m7-c-crypto-$m7_crypto_version.tar.gz.sha256"
sha256sum -c "m7-php-crypto-$m7_crypto_version.tar.gz.sha256"
tar -tvzf "m7-c-crypto-$m7_crypto_version.tar.gz"
tar -tvzf "m7-php-crypto-$m7_crypto_version.tar.gz"
)
```

Stop on any mismatch. Review the listings before extraction: each archive must
have its single matching versioned root, with no parent traversal, absolute
paths, symlinks or unexpected payload. A checksum verifies agreement with the
sidecar; publisher trust comes from the channel supplying both.

```bash
(
set -euo pipefail
test ! -e "$m7_crypto_c"
test ! -e "$m7_crypto_php"
tar --no-same-owner --no-same-permissions -xzf \
  "$m7_crypto_downloads/m7-c-crypto-$m7_crypto_version.tar.gz" -C "$m7_crypto_src"
tar --no-same-owner --no-same-permissions -xzf \
  "$m7_crypto_downloads/m7-php-crypto-$m7_crypto_version.tar.gz" -C "$m7_crypto_src"
cmp "$m7_crypto_downloads/m7-c-crypto-$m7_crypto_version.tar.gz.manifest.json" "$m7_crypto_c/MANIFEST.json"
cmp "$m7_crypto_downloads/m7-php-crypto-$m7_crypto_version.tar.gz.manifest.json" "$m7_crypto_php/MANIFEST.json"
(cd "$m7_crypto_c" && sha256sum -c CHECKSUMS.sha256)
(cd "$m7_crypto_php" && sha256sum -c CHECKSUMS.sha256)
test "$(cat "$m7_crypto_c/VERSION")" = "$m7_crypto_version"
test "$(cat "$m7_crypto_php/VERSION")" = "$m7_crypto_version"
)
```

Use fresh directories for a new attempt rather than extracting over an old
build. The source bundle also includes `scripts/release.py verify --integrity-only`
for complete inventory/reproducibility checks; it requires Python 3.10+.
Neither checksums nor successful extraction establish runtime correctness.

## 4. Build and test the C library

```bash
(
set -euo pipefail
cd "$m7_crypto_c"
make -j2 2>&1 | tee "$m7_crypto_logs/c-build.log"
M7CRYPTO_REQUIRE_ML_DSA=1 make test 2>&1 | tee "$m7_crypto_logs/c-tests.log"
ldd build/libm7crypto.so | tee "$m7_crypto_logs/c-linkage.log"
! grep -q 'not found' "$m7_crypto_logs/c-linkage.log"
)
```

The normal library is `build/libm7crypto.so`. The four executables test PSS,
signing families, generation/export and encryption; `make test` also runs the
key-generation and encryption provider-failure cases. Require all PASS results
and a successful exit. `M7CRYPTO_REQUIRE_ML_DSA=1` makes missing ML-DSA support
a failure instead of an acceptable skip. Fixtures use disposable test keys.

### Check C memory handling

```bash
(
set -euo pipefail
cd "$m7_crypto_c"
M7CRYPTO_REQUIRE_ML_DSA=1 \
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1 \
  make sanitize 2>&1 | tee "$m7_crypto_logs/c-sanitizers.log"
)
```

This builds a separate instrumented library and tests under `build/sanitize`.
AddressSanitizer checks invalid memory access and leaks; UBSan checks undefined
behavior. Require successful tests with no sanitizer diagnostics. A missing
sanitizer runtime, unsupported instrumentation or skipped run is **not a pass**.
Use a fresh build when changing compilers or flags. Install the normal library,
not the instrumented one. Passing these tests checks exercised paths; it does
not prove that no defect or leak can occur under every workload.

## 5. Install the tested C library at its final path

The PHP extension will link against this versioned prefix. Keep it at this
location after linking; do not build against a temporary prefix and then move
the library. These privileged commands only copy the tested normal output:

```bash
(
set -euo pipefail
test ! -e "$m7_crypto_prefix"
sudo install -d -o root -g root -m 0755 \
  "$m7_crypto_prefix/include" "$m7_crypto_prefix/lib" "$m7_crypto_prefix/php"
sudo install -o root -g root -m 0644 "$m7_crypto_c/include/m7crypto.h" "$m7_crypto_prefix/include/m7crypto.h"
sudo install -o root -g root -m 0755 "$m7_crypto_c/build/libm7crypto.so" "$m7_crypto_prefix/lib/libm7crypto.so"
cmp "$m7_crypto_c/build/libm7crypto.so" "$m7_crypto_prefix/lib/libm7crypto.so"
ldd "$m7_crypto_prefix/lib/libm7crypto.so"
)
```

The fresh-prefix check prevents accidental replacement of an existing version.
If it already exists, inspect that installation and its provenance before
continuing; do not delete it merely to make the command pass. Keep prior versions
for rollback. Applications must not be able to modify installed libraries or
their parent directories. C-only consumers can stop after validating their own
program against this header/library; the remaining steps install PHP support.

## 6. Build the matching PHP extension

```bash
(
set -euo pipefail
cd "$m7_crypto_php"
"$m7_phpize" 2>&1 | tee "$m7_crypto_logs/phpize.log"
mkdir build-alma
cd build-alma
../configure --with-php-config="$m7_php_config" \
  --with-m7crypto="$m7_crypto_prefix" 2>&1 | tee "$m7_crypto_logs/php-configure.log"
make -j2 2>&1 | tee "$m7_crypto_logs/php-build.log"
)
export m7_crypto_module="$m7_crypto_php/build-alma/modules/m7crypto.so"
```

`phpize` prepares the extension build; `configure` selects the PHP ABI and the
installed C header/library; `make` produces the module. No PHP configuration has
been changed yet. The declaration stub is not a runtime PHP implementation.

```bash
(
set -euo pipefail
ldd "$m7_crypto_module" | tee "$m7_crypto_logs/php-linkage.log"
! grep -q 'not found' "$m7_crypto_logs/php-linkage.log"
grep -F "$m7_crypto_prefix/lib/libm7crypto.so" "$m7_crypto_logs/php-linkage.log"
"$m7_php" -n -d "extension=$m7_crypto_module" --ri m7crypto
"$m7_php" -n -d "extension=$m7_crypto_module" -r \
  'exit(phpversion("m7crypto") === "0.3.0" ? 0 : 1);'
)
```

Require extension version 0.3.0, backend `external libm7crypto`, and the exact
versioned C library path. Also inspect libcrypto resolution for compatibility
with PHP. A stale C library, missing getter or wrong PHP ABI must be fixed before
continuing. `-n` ignores normal INI files so this check loads only the selected
module, even if an older installation is enabled for ordinary PHP commands.

## 7. Run PHP correctness and interoperability tests

```bash
(
set -euo pipefail
cd "$m7_crypto_php/build-alma"
TEST_PHP_EXECUTABLE="$m7_php" M7CRYPTO_TEST_OPENSSL="$m7_openssl" \
M7CRYPTO_TEST_PREFIX="$m7_crypto_prefix" NO_INTERACTION=1 REPORT_EXIT_STATUS=1 \
  make test TESTS="$m7_crypto_php/tests" 2>&1 | tee "$m7_crypto_logs/php-tests.log"
grep -Eq 'Tests passed[[:space:]]*:[[:space:]]*6([[:space:]]|$)' "$m7_crypto_logs/php-tests.log"
! grep -Eq 'Tests (failed|warned|skipped)[[:space:]]*:[[:space:]]*[1-9]' "$m7_crypto_logs/php-tests.log"
PHP_BINARY="$m7_php" M7CRYPTO_TEST_MODULE="$m7_crypto_module" \
  node "$m7_crypto_php/tests/encryption_interop.mjs" 2>&1 | tee "$m7_crypto_logs/php-node-interop.log"
)
```

Require **six PHPT passes, zero failures/warnings/skips**, and successful Node
interoperability. The PHPT cases cover the API, signing, cleanup, PHP allocation
bailout, generation/export and encryption. The bailout case deliberately induces
a child-process failure and verifies cleanup; the parent test must still PASS.
The Node check independently exercises OAEP and test-only nested-JWT composition.
It does not establish that a production Identity SDK integration is configured.

## 8. Check PHP/native memory cleanup with Valgrind

Use the normal, unsanitized module and C library. Do not combine this run with
the C ASan build. `USE_ZEND_ALLOC=0` exposes PHP process allocations to Memcheck.
The drivers test both explicit close/destruction and request-shutdown cleanup.

```bash
(
set -euo pipefail
cd "$m7_crypto_php/build-alma"
run_memory_check() {
  local driver=$1 variable=$2 cycles=$3 result
  if env USE_ZEND_ALLOC=0 M7CRYPTO_TEST_OPENSSL="$m7_openssl" \
    M7CRYPTO_TEST_PREFIX="$m7_crypto_prefix" "$variable=$cycles" \
    valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all \
      --errors-for-leak-kinds=definite,indirect,possible --error-exitcode=1 \
      --child-silent-after-fork=yes --log-file="$m7_crypto_logs/$driver.memcheck.log" \
      "$m7_php" -n -d "extension=$m7_crypto_module" \
      "$m7_crypto_php/tests/$driver.inc.php" \
      > "$m7_crypto_logs/$driver.output.log" 2>&1; then
    result=0
  else
    result=$?
  fi
  printf 'Exit status: %s\n' "$result" | tee "$m7_crypto_logs/$driver.status"
  cat "$m7_crypto_logs/$driver.output.log"
  tail -n 25 "$m7_crypto_logs/$driver.memcheck.log"
  test "$result" -eq 0
  grep -q '^PASS:' "$m7_crypto_logs/$driver.output.log"
}
run_memory_check stress M7CRYPTO_STRESS_ITERATIONS 1000
run_memory_check keygen_stress M7CRYPTO_KEYGEN_ITERATIONS 3
run_memory_check encryption_stress M7CRYPTO_ENCRYPTION_ITERATIONS 1000
)
```

Signing repeatedly imports, signs, verifies, rejects tampering and frees keys.
Generation repeatedly creates and exports keys for all asymmetric profiles;
three cycles per profile keeps this expensive check manageable. Encryption runs
1000 OAEP round trips, rejection and cleanup cycles. Increase counts for longer
stress testing after the baseline passes.

For **each** driver, retain its PASS output, exit status **0**, and Memcheck
report. Require zero memory errors and zero definitely, indirectly or possibly
lost bytes. `still reachable` allocations are a separate category, often from
process-lifetime PHP/OpenSSL state; review them rather than reporting every
reachable allocation as a leak or hiding findings with blanket suppressions.
See [Memcheck's leak categories](https://valgrind.org/docs/manual/mc-manual.html).

A PASS line alone is insufficient: a failure can occur during shutdown after
that line. These commands preserve the final process status. A clean run means
no counted errors were found in these tested paths, not a universal guarantee
of memory safety. Missing tools, a timeout or an incomplete run remain untested.

## 9. Install the PHP module and apply SELinux labels

After all checks pass, copy the tested module into the same protected version
prefix. This keeps both native components together and makes rollback an INI
selection rather than overwriting a previous version's binaries.

```bash
(
set -euo pipefail
sudo install -o root -g root -m 0755 "$m7_crypto_module" "$m7_crypto_prefix/php/m7crypto.so"
cmp "$m7_crypto_module" "$m7_crypto_prefix/php/m7crypto.so"
"$m7_php" -n -d "extension=$m7_crypto_prefix/php/m7crypto.so" --ri m7crypto
)
```

Check `getenforce` and existing local file-context rules. On an enforcing
AlmaLinux installation, the versioned `lib` and `php` directories hold shared
libraries. If no equivalent rule exists, record a persistent label rule and
apply it:

```bash
getenforce
sudo semanage fcontext -l | grep '/opt/m7/crypto'
# Add once, only when this rule is absent:
sudo semanage fcontext -a -t lib_t '/opt/m7/crypto/[^/]+/(lib|php)(/.*)?'
sudo restorecon -Rv "$m7_crypto_prefix"
ls -lZ "$m7_crypto_prefix/lib/libm7crypto.so" "$m7_crypto_prefix/php/m7crypto.so"
matchpathcon -V "$m7_crypto_prefix/lib/libm7crypto.so" "$m7_crypto_prefix/php/m7crypto.so"
```

An empty `grep` result means no matching rule was listed. If the exact rule
already exists, inspect/reuse it; use `semanage fcontext -m` only when intentionally
correcting that rule. Do not disable SELinux. For a real service denial, inspect
the audit record with `sudo ausearch -m AVC,USER_AVC -ts recent` and correct the
specific label or policy issue. CLI loading alone does not prove service access.

## 10. Enable the extension and verify the real PHP service

Run `"$m7_php" --ini` and inspect the target FPM configuration before choosing
an INI path. This example uses `/etc/php.ini` plus `/etc/php.d/`, as in the
standard AlmaLinux layout. Adapt the path if your PHP package uses another scan
directory. There must be exactly one effective `m7crypto` extension entry.

```bash
"$m7_php" --ini
grep -nE '^[[:space:]]*extension[[:space:]]*=.*m7crypto' /etc/php.ini /etc/php.d/*.ini
```

No match is normal for a first installation. If an older entry exists elsewhere,
back it up and consolidate the effective configuration before adding another.
Use these variables only after confirming the target directory:

```bash
export m7_crypto_ini=/etc/php.d/20-m7crypto.ini
export m7_crypto_backup="/var/backups/m7crypto/$m7_crypto_run"
(
set -euo pipefail
sudo install -d -o root -g root -m 0700 "$m7_crypto_backup"
if sudo test -f "$m7_crypto_ini"; then
  sudo cp -a "$m7_crypto_ini" "$m7_crypto_backup/20-m7crypto.ini"
else
  sudo touch "$m7_crypto_backup/no-previous-ini"
fi
printf 'extension=%s/php/m7crypto.so\n' "$m7_crypto_prefix" > "$m7_crypto_logs/20-m7crypto.ini"
sudo install -o root -g root -m 0644 "$m7_crypto_logs/20-m7crypto.ini" "$m7_crypto_ini"
sudo restorecon -v "$m7_crypto_ini"
"$m7_php" --ri m7crypto
)
```

Verify the existing PHP-FPM service's executable and configuration, then reload
it. These commands target the conventional `php-fpm` service; substitute the
actual service and executable for a parallel PHP installation.

```bash
(
set -euo pipefail
systemctl show -p ExecStart php-fpm
sudo /usr/sbin/php-fpm -t
sudo /usr/sbin/php-fpm -i | grep -E 'PHP Version|Loaded Configuration|Scan this dir|m7crypto|Extension version|Crypto backend'
sudo systemctl reload php-fpm
systemctl is-active --quiet php-fpm
sudo journalctl -u php-fpm --since '5 minutes ago' --no-pager
)
```

This assumes the site's FPM service already exists and is running. For a new
site, finish its FPM pool/web-server setup before starting the service. Apache
usually proxies PHP to FPM on this platform, so it is the FPM workers that need
the reload. If your site instead uses `mod_php`, check the matching Apache/PHP
build, run `sudo httpd -t`, then `sudo systemctl reload httpd` and verify a real
request in that runtime. A shell PHP success is not a web-runtime test.

Save this generated-key smoke check as `/home/m7/src/m7crypto-smoke.php`:

```php
<?php
declare(strict_types=1);

use M7\Crypto\Key;

if (!extension_loaded('m7crypto') || !class_exists(Key::class, false)
    || phpversion('m7crypto') !== '0.3.0') {
    throw new RuntimeException('Expected m7crypto 0.3.0');
}
$signing = null;
$recipient = null;
try {
    $signing = Key::generate('Ed25519');
    $message = 'installation smoke check';
    $signature = $signing->sign($message, 'Ed25519');
    if ($signing->verify($message, $signature, 'Ed25519') !== true) {
        throw new RuntimeException('Signature check failed');
    }
    $recipient = Key::generate('RSA-OAEP-256', 2048);
    $secret = random_bytes(32);
    $ciphertext = $recipient->encrypt($secret, 'RSA-OAEP-256');
    if (!hash_equals($secret, $recipient->decrypt($ciphertext, 'RSA-OAEP-256'))) {
        throw new RuntimeException('Encryption check failed');
    }
    echo 'PASS: m7crypto signing and encryption; SAPI=', PHP_SAPI, "\n";
} finally {
    $signing?->close();
    $recipient?->close();
}
```

Run `"$m7_php" /home/m7/src/m7crypto-smoke.php` without `-n` or a module override.
Then run the same check through a temporary, access-restricted diagnostic route
in the actual application worker. The result should report the expected SAPI
and PASS; remove that route afterward. Do not expose `phpinfo()`, generated keys
or application credentials. Finally test the application's selected SDK feature
with its explicit policy and keys. Primitive success is not token-validation or
end-to-end login acceptance.

## 11. Roll back if activation fails

Keep the previous versioned C/PHP pair until the application passes. Restore
the saved INI, or remove the new dedicated entry only for a first installation:

```bash
(
set -euo pipefail
if sudo test -f "$m7_crypto_backup/20-m7crypto.ini"; then
  sudo cp -a "$m7_crypto_backup/20-m7crypto.ini" "$m7_crypto_ini"
elif sudo test -f "$m7_crypto_backup/no-previous-ini"; then
  sudo rm -- "$m7_crypto_ini"
else
  printf 'No recorded previous configuration; inspect before changing it.\n' >&2
  exit 1
fi
if sudo test -f "$m7_crypto_ini"; then sudo restorecon -v "$m7_crypto_ini"; fi
sudo /usr/sbin/php-fpm -t
sudo systemctl reload php-fpm
systemctl is-active --quiet php-fpm
)
```

Use the same actual service choice as activation, and recheck the application.
On a first-install rollback, also revert any application change that newly
requires native crypto; never turn a failed verification or required encrypted
response into success. Do not delete libraries still used by running processes.

## What to retain from a successful run

Keep the archive digests, OS/CPU, compiler, PHP/API/NTS/debug and OpenSSL/provider
versions, exact installed prefix, build/test logs, sanitizer report, all three
Valgrind outputs/statuses and the application-worker result. The final state is
a tested installed pair; public artifact release and deployment of SDK features
remain separate decisions.

Return to [M7 PHP Crypto](https://m7.org/docs/sdk/m7-php-crypto/README.md), [general installation](https://m7.org/docs/sdk/m7-php-crypto/integration/installation.md),
or [Identity SDK native-runtime guidance](https://m7.org/docs/sdk/m7-identity/README.md#optional-native-runtime).
