pps-protocol / laravel-pps
Laravel integration for PulseProof Sentinel Protocol (PPS).
Requires
- php: ^8.1
- illuminate/console: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/database: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/routing: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0 || ^13.0
- pps-protocol/pps-php: ^0.1
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpunit/phpunit: ^10.0 || ^11.0
This package is auto-updated.
Last update: 2026-07-23 19:50:12 UTC
README
Laravel integration for the PulseProof Sentinel Protocol (PPS)
Introduction
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.
What is PPS?
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
Important Links
| 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 |
Requirements
| 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.
Installation
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, ],
Publish Configuration and Migrations
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
Configuration
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
Relying Party Identifier
The rp_id identifies your service.
Examples:
example.com
login.example.com
localhost
For local development:
PPS_RP_ID=localhost
Storage Drivers
PPS needs storage for:
- device public keys
- key sequence counters
- threshold groups
- nonces
Supported drivers:
memory
file
pdo
redis
Memory
For testing only:
PPS_STORAGE_DRIVER=memory
File
Good for development or single-process apps:
PPS_STORAGE_DRIVER=file
Default path:
storage/app/pps
PDO
Recommended for production:
PPS_STORAGE_DRIVER=pdo PPS_DB_CONNECTION=mysql
Then run:
php artisan migrate
Redis
For high-performance distributed deployments:
PPS_STORAGE_DRIVER=redis PPS_REDIS_CONNECTION=default
Routes
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
Health Check
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
}
Basic Authentication Flow
1. Get Challenge
curl http://localhost:8000/pps/challenge
Example response:
{
"nonce": "base64url-nonce",
"epoch": 58972480,
"session_id": "base64url-session",
"qr_payload": "pulseproof://challenge?..."
}
2. Register Device
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"
}
3. Authenticate
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"
}
Facade Usage
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. }
Transaction Signing
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.
Silent Duress
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
Threshold Mode
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.
Artisan Commands
Install PPS Resources
php artisan pps:install
Publishes:
config/pps.php
database/migrations/*_create_pps_tables.php
Generate Key Pair
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.
Testing
Run package tests:
composer install vendor/bin/phpunit
Package Architecture
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.
Security
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.
Changelog
See CHANGELOG.md.
License
Apache License 2.0
See LICENSE.