Laravel integration for the PulseProof Sentinel Protocol (PPS)
laravel-pps is the official Laravel integration package for the PulseProof Sentinel Protocol, abbreviated PPS.
This package provides:
The core protocol implementation lives in the framework-agnostic package:
pps-protocol/pps-php
This package, pps-protocol/laravel-pps, depends on pps-protocol/pps-php and only adds Laravel-specific integration.
PulseProof Sentinel Protocol, or PPS, is an experimental authentication protocol based on time-bound asymmetric proofs.
Instead of shared-secret TOTP codes:
TOTP = HMAC(shared_secret, time)
PPS uses signed asymmetric proofs:
Pulse = Sign(private_key, time + rp + nonce + counter + context + policy)
The server stores only public keys.
PPS is designed to complement, not replace, WebAuthn/FIDO2.
WebAuthn remains preferred for browser-based passkey authentication.
PPS targets operational gaps such as:
| Resource | Link |
|---|---|
| PPS Specification | pps-protocol/pulseproof-sentinel |
| IETF Internet-Draft | draft-hezami-pulseproof-sentinel |
| Core PHP Package | pps-protocol/pps-php |
| Laravel Package | pps-protocol/laravel-pps |
| Documentation Site | pps-protocol.github.io |
| Requirement | Version |
|---|---|
| PHP | 8.1+ |
| Laravel | 10.x, 11.x, 12.x or 13.x |
| ext-sodium | required |
| ext-gmp | required |
| ext-hash | required |
| ext-json | required |
The core package pps-protocol/pps-php will be installed automatically.
Install via Composer:
composer require pps-protocol/laravel-pps
Laravel package discovery should automatically register:
Pps\Laravel\PulseProofServiceProvider
and the facade:
Pps\Laravel\Facades\PulseProof
If package discovery is disabled, register manually in config/app.php:
'providers' => [
Pps\Laravel\PulseProofServiceProvider::class,
],
'aliases' => [
'PulseProof' => Pps\Laravel\Facades\PulseProof::class,
],
Run the install command:
php artisan pps:install
This publishes:
config/pps.php
database/migrations/*_create_pps_tables.php
Then run migrations if you are using the PDO storage driver:
php artisan migrate
The main configuration file is:
config/pps.php
You can also configure PPS using environment variables.
PPS_RP_ID=example.com
PPS_ALGORITHM=PPS-ED25519-CBOR30
PPS_EPOCH_SECONDS=30
PPS_MAX_CLOCK_SKEW=1
PPS_TRUST_DIGITS=8
PPS_NONCE_TTL=60
PPS_ALLOW_OFFLINE=false
PPS_STORAGE_DRIVER=file
# PPS_STORAGE_DRIVER=pdo
# PPS_DB_CONNECTION=mysql
# PPS_STORAGE_DRIVER=redis
# PPS_REDIS_CONNECTION=default
PPS_ROUTES_ENABLED=true
PPS_ROUTE_PREFIX=pps
PPS_ROUTE_MIDDLEWARE=api
The rp_id identifies your service.
Examples:
example.com
login.example.com
localhost
For local development:
PPS_RP_ID=localhost
PPS needs storage for:
Supported drivers:
memory
file
pdo
redis
For testing only:
PPS_STORAGE_DRIVER=memory
Good for development or single-process apps:
PPS_STORAGE_DRIVER=file
Default path:
storage/app/pps
Recommended for production:
PPS_STORAGE_DRIVER=pdo
PPS_DB_CONNECTION=mysql
Then run:
php artisan migrate
For high-performance distributed deployments:
PPS_STORAGE_DRIVER=redis
PPS_REDIS_CONNECTION=default
By default, this package registers the following routes:
| Method | URI | Description |
|---|---|---|
| GET | /pps/health |
Health check |
| GET | /pps/challenge |
Create authentication/registration challenge |
| POST | /pps/register |
Register a device |
| POST | /pps/verify |
Verify a Pulse Token |
| POST | /pps/transaction/challenge |
Create transaction challenge |
| POST | /pps/transaction/verify |
Verify transaction-bound Pulse |
You can customize the prefix:
PPS_ROUTE_PREFIX=pps
Or disable default routes:
PPS_ROUTES_ENABLED=false
curl http://localhost:8000/pps/health
Example response:
{
"status": "ok",
"protocol": "PulseProof Sentinel Protocol",
"version": 1,
"algorithm": "PPS-ED25519-CBOR30",
"epoch_seconds": 30,
"rp_id": "localhost",
"time": 1769174400,
"epoch": 58972480
}
curl http://localhost:8000/pps/challenge
Example response:
{
"nonce": "base64url-nonce",
"epoch": 58972480,
"session_id": "base64url-session",
"qr_payload": "pulseproof://challenge?..."
}
Device registration requires a registration_token.
The registration_token is generated by a PPS client using the core package:
use Pps\Client\RegistrationBuilder;
$builder = new RegistrationBuilder();
$registration = $builder
->rpId('localhost')
->nonce($nonceFromChallenge)
->userHandle('user-123')
->deviceName('My Phone')
->withDuress(true)
->build();
$registrationToken = $registration['token'];
$clientState = $registration['client_state'];
Then send it to Laravel:
curl -X POST http://localhost:8000/pps/register \
-H "Content-Type: application/json" \
-d '{
"registration_token": "base64url-registration-token",
"nonce": "base64url-nonce"
}'
Example response:
{
"status": "ok",
"kid": "base64url-kid"
}
The client creates a Pulse Token:
use Pps\Client\AuthenticatorClient;
$client = new AuthenticatorClient($clientState);
$pulse = $client
->rpId('localhost')
->nonce($nonceFromChallenge)
->createPulse();
$pulseToken = $pulse['token'];
$trustCode = $pulse['trust_code'];
Then send it to Laravel:
curl -X POST http://localhost:8000/pps/verify \
-H "Content-Type: application/json" \
-d '{
"pulse_token": "base64url-pulse-token",
"nonce": "base64url-nonce",
"trust_code": "49371826"
}'
Example response:
{
"status": "ok",
"mode": "normal"
}
use Pps\Laravel\Facades\PulseProof;
$challenge = PulseProof::challenge();
$nonce = $challenge['nonce'];
Verify a Pulse:
$result = PulseProof::verify(
pulseToken: $request->input('pulse_token'),
nonceB64u: $request->input('nonce'),
trustCode: $request->input('trust_code')
);
if ($result->honey) {
// Silent duress detected.
// Return a normal-looking response.
}
if ($result->ok) {
// Authentication succeeded.
}
Create a transaction challenge:
curl -X POST http://localhost:8000/pps/transaction/challenge \
-H "Content-Type: application/json" \
-d '{
"action": "withdraw",
"amount_minor": 2500067,
"currency": "IRR",
"recipient": "IR820540102680020817909002"
}'
Example response:
{
"nonce": "base64url-nonce",
"epoch": 58972480,
"session_id": "base64url-session",
"tx_hash": "base64url-tx-hash",
"amount_mark": "67"
}
The client should bind the transaction hash and amount to the Pulse:
use Pps\Crypto\Base64Url;
use Pps\Payload\ContextObject;
$context = new ContextObject();
$context->sessionId = Base64Url::decode($sessionId);
$context->txHash = Base64Url::decode($txHash);
$pulse = $client
->rpId('localhost')
->nonce($nonce)
->context($context)
->amountMinor(2500067)
->createPulse();
Then verify:
curl -X POST http://localhost:8000/pps/transaction/verify \
-H "Content-Type: application/json" \
-d '{
"pulse_token": "base64url-pulse-token",
"nonce": "base64url-nonce",
"trust_code": "49371867",
"action": "withdraw",
"amount_minor": 2500067,
"currency": "IRR",
"recipient": "IR820540102680020817909002",
"session_id": "base64url-session",
"tx_hash": "base64url-tx-hash"
}'
The Trust Code includes the AmountMark:
Trust Code: 49371867
AmountMark: 67
The last two digits match the transaction amount modulo 100.
PPS supports silent duress signaling.
If a user authenticates under coercion, the client can sign with a hidden duress key.
The server detects this internally:
if ($result->honey) {
// Outward response must remain normal.
}
Important:
The HTTP response for a duress authentication must be indistinguishable from a normal successful authentication.
Do not return:
{
"status": "duress"
}
Instead, return:
{
"status": "ok"
}
Then internally:
PPS supports offline multi-device threshold approval.
The core package implements n-of-m Ed25519 multisignature.
This Laravel package can verify threshold Pulses through the same verification engine provided by pps-protocol/pps-php.
Threshold group management may be implemented in your application or added in future versions of this package.
php artisan pps:install
Publishes:
config/pps.php
database/migrations/*_create_pps_tables.php
php artisan pps:key-generate
Example output:
{
"public_key": "base64url-public-key",
"secret_key": "base64url-secret-key",
"seed": "base64url-seed"
}
Warning:
Never commit secret keys to source control.
Run package tests:
composer install
vendor/bin/phpunit
pps-protocol/pps-php
Core protocol implementation
No framework dependency
pps-protocol/laravel-pps
Laravel bridge
Depends on pps-protocol/pps-php
This package does not reimplement the protocol.
It only provides Laravel integration around the core package.
This package is experimental.
It has not been independently audited.
Do not use it in production without:
See SECURITY.md.
See CHANGELOG.md.
Apache License 2.0
See LICENSE.