Loading & generating keys
Build every key the signers take through Crypto::keys() — rsa(), ec(), ed25519() and hmac() — loaded, generated or bootstrapped:
use RoundlyConsulting\Crypto\Facades\Crypto;
$rsa = Crypto::keys()->rsa()->privateFromStorage('local', 'keys/rsa.pem');
$rsaPub = Crypto::keys()->rsa()->publicFromConfig('jwt.public_key');
$ec = Crypto::keys()->ec()->generate('P-384');
$ed = Crypto::keys()->ed25519()->public($raw32Bytes); // raw 32-byte public key
$secret = Crypto::keys()->hmac()->fromConfig('services.webhook.secret');
// Load, or generate-and-persist on first boot:
$hmac = Crypto::keys()->hmac()->fromStorageOrGenerate('local', 'keys/hmac.key');
// A fresh secret, returned and never cached (shortcut for keys()->hmac()->generate(48)):
$fresh = Crypto::generateHmacSecret(48);For Ed25519 the facade uses the same public/private words as RSA and EC: public is the raw 32-byte key, private the 64-byte libsodium secret key.
The key classes
Every key class has a core zero-config factory that needs no container, config or disk. Each one validates on the way in, so a key is either strong and usable or rejected with a typed reason:
use RoundlyConsulting\Crypto\Signature\Key\EcKey;
use RoundlyConsulting\Crypto\Signature\Key\HmacSecret;
use RoundlyConsulting\Crypto\Signature\Key\OkpKey;
use RoundlyConsulting\Crypto\Signature\Key\RsaKey;
$secret = HmacSecret::fromString($raw); // ≥ 32 bytes, never a PEM
$rsa = RsaKey::private($privatePem); // RSA 2048–8192 bits
$rsaPub = RsaKey::public($publicPem);
$ec = EcKey::private($pem); // curve auto-detected: P-256 / P-384 / P-521
$ecPub = EcKey::fromCoordinates($x, $y, 'P-256'); // raw COSE/JWK point
$rsaJwk = RsaKey::fromModulusExponent($n, $e); // raw COSE/JWK modulus + exponent
$okp = OkpKey::ed25519($rawPublic); // 32-byte Ed25519 public keyValidation guards
- HmacSecret — rejects empty values, anything carrying public-key material (a PEM, even behind whitespace or a UTF-8 BOM, and raw DER key bytes — blocking RS256→HS256 confusion), secrets under 32 bytes and single-repeated-byte secrets. Throws WeakKeyException. HS384 and HS512 need 48 and 64 bytes — the Hs signer checks that. A value starting with file:// is never handed to OpenSSL, so its path is never opened — it is judged as the string it is.
- RsaKey — must be RSA, 2048–8192 bits, with an odd public exponent of at least 3. An oversized raw modulus is rejected before it is parsed.
- EcKey — P-256, P-384 or P-521 only; the curve is detected from the key itself.
- OkpKey — a raw 32-byte Ed25519 public key or a 64-byte libsodium secret key; wrong-length material throws KeyLoadException. fromSecretKey() rebuilds the pair from the seed, so a secret key whose public half belongs to another seed throws KeyLoadException::mismatchedKeyPair().
From a disk or your config
Opt-in Laravel-native loaders read material from any filesystem disk or from a config key you own, and run exactly the same guards as the core factory. A missing file (whether the disk returns null or is configured with 'throw' => true), an unknown disk name, a failed write in fromStorageOrGenerate() or a missing, blank or non-string config value throws Signature\KeyLoadException — never a PHP warning and never a filesystem exception; the original stays on getPrevious():
use RoundlyConsulting\Crypto\Signature\Key\HmacSecret;
use RoundlyConsulting\Crypto\Signature\Key\RsaKey;
use RoundlyConsulting\Crypto\Signature\Key\EcKey;
use RoundlyConsulting\Crypto\Signature\Key\OkpKey;
// From a filesystem disk (any configured disk name):
$secret = HmacSecret::fromStorage('local', 'keys/hmac.key');
$rsa = RsaKey::privateFromStorage('local', 'keys/rsa.pem');
$rsaPub = RsaKey::publicFromStorage('local', 'keys/rsa.pub');
$ec = EcKey::privateFromStorage('local', 'keys/ec.pem');
$okp = OkpKey::ed25519FromStorage('local', 'keys/ed25519.pub'); // 32 raw bytes
// From YOUR config key (explicit — the package reads no config on its own):
$secret = HmacSecret::fromConfig('services.webhook.secret');
$rsa = RsaKey::privateFromConfig('jwt.private_key');
$ecPub = EcKey::publicFromConfig('tokens.public_key');The PEM factories — RsaKey and EcKey public() / private(), Certificate::fromPem() and Chain::fromPems() — take PEM text, never a path: a file://… string, which PHP’s OpenSSL functions would read from disk, is refused as unreadable. Load files through fromStorage().
The public loaders — RsaKey::public(), EcKey::public(), OkpKey::ed25519() and their Crypto::keys() forms — mark their input #[\SensitiveParameter] just like the private ones: private keys come in the same formats, so one handed over by mistake stays out of the trace of the exception that refuses it.
Generate fresh material
$secret = HmacSecret::generate(); // 32 random bytes (≥256 bits); pass a larger byte count if you like
$rsa = RsaKey::generate(2048); // or 3072 / 4096
$ec = EcKey::generate('P-256'); // or P-384 / P-521
$okp = OkpKey::generate(); // Ed25519, needs ext-sodiumHmacSecret::generate() accepts 32 to 1024 bytes — pass 48 or 64 for an HS384 or HS512 signer. RsaKey::generate() and EcKey::generate() need a usable OpenSSL configuration (openssl.cnf); loading PEMs, signing and verifying do not. On a host with a missing or broken config, generate() throws KeyLoadException rather than leaking a warning.
Load, or generate-and-persist on first boot
fromStorageOrGenerate() loads the key from a disk path or — only when the file is missing — generates a fresh one, writes it with private visibility and returns it. An existing-but-invalid file is never overwritten; it still throws. A write that fails throws KeyLoadException — a generated key that was not persisted would be replaced by a different one on the next boot. For asymmetric keys the persisted artifact is the private PEM (Ed25519 persists the 64-byte secret), so persist the public half yourself:
use Illuminate\Support\Facades\Storage;
// Bootstraps a secret on first run, reuses it forever after:
$secret = HmacSecret::fromStorageOrGenerate('local', 'keys/hmac.key');
// Asymmetric: private PEM is written; persist the public half alongside it:
$key = RsaKey::fromStorageOrGenerate('local', 'keys/rsa.pem', bits: 3072);
Storage::disk('local')->put('keys/rsa.pub', $key->publicPem(), 'private');
$ec = EcKey::fromStorageOrGenerate('local', 'keys/ec.pem', curve: 'P-384');
$okp = OkpKey::fromStorageOrGenerate('local', 'keys/ed25519.key'); // 64-byte secret, ext-sodiumConcurrent first boots get one key. Generation runs under an exclusive flock(), the existence check is repeated inside it, and the key returned is the one read back from the disk, so a process that lost the race adopts the winner’s key instead of overwriting it. On a local disk the lock is an empty <key>.lock file next to the key, shared by every process and container that mounts the directory — leave it in place. On any other disk it is a lock file in the system temp directory, which serialises one host only, so on a shared object store provision the key once, as a deploy step. A directory the process cannot lock in throws KeyLoadException::unlockable(). When the key already exists, no lock is taken and nothing is written, so a read-only key directory works.
On a local disk the new key is written to an owner-only (0600) temporary file beside it, given private visibility and renamed into place in one step, so it is never visible half-written or world-readable; if any step fails, nothing is left at the key path and the next boot generates afresh.
$bytes, $bits and $curve only shape a new key. When the file exists, the stored key wins as it is — asking for P-384 does not change a stored P-256 key. Changing them in config therefore means a deliberate key rotation: keep the old public key available to verifiers until every token it signed has expired.
Private visibility is driver-dependent: owner-only file permissions on the local driver, but a coarser ACL or a no-op on some object stores. Persist secret key material only to a private disk you control — never a publicly served one.
Exporting PEMs
Storing keys somewhere other than a Laravel disk? RsaKey and EcKey export both halves — privatePem() (PKCS#8) and publicPem() (SPKI). privatePem() throws KeyLoadException::notPrivate() on a public key, so a verify-only key can never be mistaken for signing material:
$key = RsaKey::generate(2048);
file_put_contents('/etc/app/private.pem', $key->privatePem()); // secret — chmod 0600
file_put_contents('/etc/app/public.pem', $key->publicPem());OkpKey exports its public half with publicPem() — an RFC 8410 SubjectPublicKeyInfo PEM (-----BEGIN PUBLIC KEY-----, OID 1.3.101.112), the same shape RsaKey::publicPem() and EcKey::publicPem() return. It works on a public-only key and needs neither ext-sodium nor ext-openssl. It is meant for verifiers that load public keys from PEM — OpenSSL, a JOSE library, another service; the package itself reads an Ed25519 public key back as the raw 32 bytes:
use RoundlyConsulting\Crypto\Signature\Key\OkpKey;
$key = OkpKey::fromStorageOrGenerate('local', 'keys/ed25519.key'); // needs ext-sodium
$key->publicPem(); // -----BEGIN PUBLIC KEY----- (RFC 8410, OID 1.3.101.112) for OpenSSL, a JOSE library, another service
$key->publicKey; // the raw 32 bytes OkpKey::ed25519() reads backThe bytes match openssl pkey -pubout and the RFC 8410 example key, both pinned by tests. Only Ed25519 lives in OkpKey, so the OID is always id-Ed25519.
Factory reference
| Facade accessor | Methods |
|---|---|
Crypto::keys()->rsa() | public · private · fromModulusExponent · generate · publicFromStorage · privateFromStorage · publicFromConfig · privateFromConfig · fromStorageOrGenerate |
Crypto::keys()->ec() | public · private · fromCoordinates · generate · publicFromStorage · privateFromStorage · publicFromConfig · privateFromConfig · fromStorageOrGenerate |
Crypto::keys()->ed25519() | public · private · generate · publicFromStorage · privateFromStorage · publicFromConfig · privateFromConfig · fromStorageOrGenerate |
Crypto::keys()->hmac() | fromString · generate · fromStorage · fromConfig · fromStorageOrGenerate |
| Key | Static factories |
|---|---|
HmacSecret | fromString · generate · fromStorage · fromConfig · fromStorageOrGenerate |
RsaKey | public · private · fromModulusExponent · generate · publicFromStorage · privateFromStorage · publicFromConfig · privateFromConfig · fromStorageOrGenerate |
EcKey | public · private · fromCoordinates · generate · publicFromStorage · privateFromStorage · publicFromConfig · privateFromConfig · fromStorageOrGenerate |
OkpKey | ed25519 · fromSecretKey · generate · ed25519FromStorage · ed25519FromConfig · secretKeyFromStorage · secretKeyFromConfig · fromStorageOrGenerate |
Where ext-sodium is present, HmacSecret and OkpKey secrets are best-effort zeroed from memory when the object is destroyed — defence in depth, not a guarantee. HmacSecret::$value and OkpKey::$secretKey are readable but private(set), so a validated key can never be swapped for an unchecked one.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.