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:
- Laravel service provider
- Publishable configuration
- API routes
- HTTP controller
- Facade
- Artisan commands
- Database migrations
- Storage backend integration
- Silent duress handling support
- Transaction signing support
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:
- offline or semi-offline authentication
- legacy OTP-style input flows
- QR-code and deep-link authentication
- constrained hardware terminals
- point-of-sale and IoT devices
- transaction signing with human-visible amount confirmation
- silent duress signaling
- offline multi-device threshold approval
| 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-ppsLaravel 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:installThis publishes:
config/pps.php
database/migrations/*_create_pps_tables.php
Then run migrations if you are using the PDO storage driver:
php artisan migrateThe 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=apiThe rp_id identifies your service.
Examples:
example.com
login.example.com
localhost
For local development:
PPS_RP_ID=localhostPPS needs storage for:
- device public keys
- key sequence counters
- threshold groups
- nonces
Supported drivers:
memory
file
pdo
redis
For testing only:
PPS_STORAGE_DRIVER=memoryGood for development or single-process apps:
PPS_STORAGE_DRIVER=fileDefault path:
storage/app/pps
Recommended for production:
PPS_STORAGE_DRIVER=pdo
PPS_DB_CONNECTION=mysqlThen run:
php artisan migrateFor high-performance distributed deployments:
PPS_STORAGE_DRIVER=redis
PPS_REDIS_CONNECTION=defaultBy 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=ppsOr disable default routes:
PPS_ROUTES_ENABLED=falsecurl http://localhost:8000/pps/healthExample 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/challengeExample 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:
- restrict account capabilities
- block high-value operations
- delay settlement
- trigger a silent alert
- preserve audit logs
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:installPublishes:
config/pps.php
database/migrations/*_create_pps_tables.php
php artisan pps:key-generateExample 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/phpunitpps-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:
- cryptographic review
- security audit
- interoperability testing
- operational risk assessment
See SECURITY.md.
See CHANGELOG.md.
Apache License 2.0
See LICENSE.