# PHP cryptography integration

Use the native `M7\Crypto\Key` class for local key, signature and RSA-OAEP
operations. This guide describes the 0.3.0 source candidate. Complete
[installation](https://m7.org/docs/sdk/m7-php-crypto/integration/installation.md) first and check [release status](https://m7.org/docs/sdk/m7-php-crypto/releases.md).
Local cryptography requires no network account or provider registration.

For a complete server setup, follow the [AlmaLinux walkthrough](https://m7.org/docs/sdk/m7-php-crypto/integration/almalinux.md):
development packages, C/PHP builds, correctness and memory tests, installation,
service activation and rollback. It also explains the
[optional SDK path and possible PHP 8.5 port](https://m7.org/docs/sdk/m7-php-crypto/integration/almalinux.md#when-you-need-this-optional-path).

## Public API

`Key` is final and has a private constructor. Its supported creation paths are:

| Factory | Input and result |
| --- | --- |
| `Key::fromPrivatePem($pem)` | Import unencrypted PKCS#8, RSA PKCS#1 or EC SEC1 PEM |
| `Key::fromPublicPem($pem)` | Import SPKI `BEGIN PUBLIC KEY` PEM |
| `Key::fromSecret($bytes)` | Copy a raw HMAC secret into a native handle |
| `Key::generate($algorithm, $rsaBits = 0)` | Generate an asymmetric key for an exact profile |

Each returns a `Key`. Public PEM import does not parse a certificate or JWK.
The instance operations are:

| Method | Return value |
| --- | --- |
| `sign($data, $algorithm)` | Binary signature or MAC string |
| `verify($data, $signature, $algorithm)` | `true` on valid signature; `false` for native invalid-signature status |
| `encrypt($plaintext, $algorithm)` | Binary RSA-OAEP ciphertext |
| `decrypt($ciphertext, $algorithm)` | Binary RSA-OAEP plaintext |
| `toPrivatePem()` | Unencrypted PKCS#8 PEM string |
| `toPublicPem()` | SPKI PEM string |
| `close()` | Release the native handle early; safe to repeat |

## Algorithms and key policy

| Family | Exact signing/MAC identifiers |
| --- | --- |
| RSA PKCS#1 v1.5 | `RS256`, `RS384`, `RS512` |
| RSA-PSS | `PS256`, `PS384`, `PS512` |
| ECDSA | `ES256`, `ES384`, `ES512`, `ES256K` |
| Edwards | `Ed25519`, `Ed448` |
| HMAC | `HS256`, `HS384`, `HS512` |
| ML-DSA | `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87` |

These 18 names are recognized profiles, not a guarantee that the active
OpenSSL provider supports every operation. Names are case-sensitive and have no
aliases, embedded-NUL truncation or fallback. Select an algorithm from trusted
application policy, not an untrusted token header alone. The static library
list is not identity-provider discovery or a client's permitted algorithm list.

Pass raw messages, not precomputed digests. Messages, signatures and secrets
are binary-safe PHP strings. ECDSA signatures use fixed-width `R || S`, not
DER. Edwards and ML-DSA operations use pure mode with an empty context.

Generation supports the 15 asymmetric signing profiles and `RSA-OAEP-256`.
RSA accepts 2048, 3072, 4096 or 8192 bits; zero selects 4096. Other profiles
require zero and choose their fixed curve/key type. PS profiles generate
ordinary RSA keys. HMAC generation and the ambiguous `EdDSA` alias are rejected.
For HMAC, import securely generated random bytes: at least 32 for HS256, 48 for
HS384, or 64 for HS512. Secret and asymmetric handles are not interchangeable.

Public export accepts a private or public-only asymmetric handle. Public-only
keys cannot export private PEM, and secret handles cannot export PEM. Export
is capped at 65536 bytes. Private PEM is unencrypted; protect it before storage.
Returned strings own their bytes and survive closing the key.

## Minimal sign and verify example

Save as `example.php` and run using the isolated module from installation.
The example generates a disposable key and prints no key or message material.

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

use M7\Crypto\Key;

$key = Key::generate('Ed25519');
try {
    $message = 'example payload';
    $signature = $key->sign($message, 'Ed25519');
    if ($key->verify($message, $signature, 'Ed25519') !== true) {
        throw new RuntimeException('Signature rejected');
    }
} finally {
    $key->close();
}
```

For incoming messages, import the expected public key separately. A verifier
must reject on either `false` **or an exception**, including provider errors.
A valid signature alone does not establish issuer, audience, expiration,
sender binding or permission to act. JWT assembly, base64url encoding, JWK
conversion and token policy belong to the application or its Identity SDK.

## Encrypt a small content key

`RSA-OAEP-256` fixes OAEP SHA-256, MGF1 SHA-256 and an empty label. Ordinary
RSA keys must be 2048–8192 bits. Encryption uses the public part of a private
or public key; decryption needs the private key. RSA-PSS-restricted, other
asymmetric and secret keys are rejected.

The plaintext limit is `ceil(key_bits / 8) - 66` bytes (190 at 2048 bits), and
ciphertext is `ceil(key_bits / 8)` bytes. Use it for a small secret, such as a
32-byte AES content-encryption key:

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

use M7\Crypto\Key;

$recipient = Key::generate('RSA-OAEP-256', 2048);
try {
    $contentKey = random_bytes(32);
    $encryptedKey = $recipient->encrypt($contentKey, 'RSA-OAEP-256');
    $recoveredKey = $recipient->decrypt($encryptedKey, 'RSA-OAEP-256');
    if (!hash_equals($contentKey, $recoveredKey)) {
        throw new RuntimeException('Content key mismatch');
    }
} finally {
    $recipient->close();
}
```

This example tests only key encryption. AES-GCM, JWE serialization and
application integration are separate. A failed decryption throws with a fixed
failure message and never returns partial plaintext. Do not convert this into
acceptance of the message or try another encryption algorithm automatically.

## Lifetime, errors and security

Each object owns one native key. Destruction and request shutdown release it;
`close()` is an optional earlier release. Later operations on a closed key raise
`ValueError`. Assigning the object to another variable shares the same object,
so closing either reference closes it. Cloning, serialization and dynamic
properties are disabled. Keep objects within their request/worker; cross-thread
key sharing is outside the supported contract.

The wrapper copies native output into a PHP string, then cleanses and frees the
native buffer. Closing the key does not erase caller variables or copies of
PEM, secrets, messages or returned plaintext. Sensitive parameters redact some
exception traces; they do not replace secure storage or safe logging.

| Failure | PHP behavior |
| --- | --- |
| Native invalid-signature status | `verify()` returns `false` |
| Unknown algorithm, invalid arguments or closed key | `ValueError` |
| Wrong PHP types | Normal PHP argument parsing, including `TypeError` and scalar coercion rules |
| Invalid PEM, wrong key type, bad secret length or disallowed export | `M7\Crypto\CryptoException` |
| Provider or native allocation failure | `M7\Crypto\CryptoException` |
| Wrong decryption key or invalid ciphertext | `M7\Crypto\CryptoException`, code 7, fixed failure message |

`CryptoException` extends `RuntimeException` and carries the native status
code. Missing provider support during generation uses code 3. Some malformed
signatures produce an OpenSSL error rather than `false`; all exceptions mean
the operation did not succeed. PHP engine allocation failures retain their
fatal-error behavior. Keep diagnostics free of keys, tokens and message bytes.

Return to [M7 PHP Crypto](https://m7.org/docs/sdk/m7-php-crypto/README.md).
