pps-protocol / pps-php
PHP implementation of the PulseProof Sentinel Protocol (PPS): time-bound asymmetric authentication proofs for offline, OTP-style, transaction-signing, duress-aware, and multi-device flows.
Requires
- php: ^8.1
- ext-gmp: *
- ext-hash: *
- ext-json: *
- ext-sodium: *
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0 || ^11.0
- squizlabs/php_codesniffer: ^3.7
This package is auto-updated.
Last update: 2026-07-23 19:50:09 UTC
README
PHP implementation of the PulseProof Sentinel Protocol (PPS)
What is PPS-PHP?
PPS-PHP is a complete PHP implementation of the PulseProof Sentinel Protocol (PPS), an experimental authentication protocol that replaces shared-secret TOTP-style codes with signed, time-bound, asymmetric proofs called Pulses.
PPS is designed to complement, not replace, WebAuthn/FIDO2. It targets operational gaps where WebAuthn is unavailable or insufficient:
- offline or semi-offline authentication
- legacy OTP-style input flows
- constrained hardware terminals
- QR-code and deep-link authentication
- transaction signing with human-visible amount confirmation
- silent duress signaling
- offline multi-device threshold approval
Features
Core Protocol
- Ed25519 digital signatures
- Deterministic CBOR encoding
- Time-bound authentication proofs
- Server nonce binding
- Monotonic counter replay protection
- Expiration enforcement
- RP (Relying Party) binding
Advanced Features
- Verifiable Forward-Secure Ratchet — automatic key rotation with signed next public key
- Rotation Certificates — offline key rotation catch-up
- Silent Duress / Honey-Pulse — hidden duress key with indistinguishable response
- Threshold Mode — n-of-m Ed25519 multisignature for offline multi-device approval
- Transaction Signing — AmountMark embedding in Trust Code
- Policy Binding — dynamic server-issued security policy enforcement
- Context Binding — session, BLE, acoustic, and device integrity binding
- Trust Code — human-verifiable numeric code derived from signature
- Trust Phrase — optional three-word human-friendly phrase
- Offline Mode — authentication without live network round-trip
Implementation Quality
- PHP 8.1+ with strict types
ext-sodiumfor Ed25519ext-gmpfor unbiased decimal derivation- Constant-time comparisons
- Pluggable storage backends
- Comprehensive test suite
- PSR-12 code style
Requirements
| Requirement | Version |
|---|---|
| PHP | 8.1 or higher |
| ext-sodium | required |
| ext-gmp | required |
| ext-hash | required |
| ext-json | required |
| ext-mbstring | recommended |
Optional Extensions
| Extension | Purpose |
|---|---|
| ext-pdo | Database storage backend |
| ext-redis | Redis storage backend |
Installation
Composer
composer require pps-protocol/pps-php
From Source
git clone https://github.com/pps-protocol/pps-php.git
cd pps-php
composer install
Verify Installation
<?php require_once 'vendor/autoload.php'; use Pps\Crypto\Ed25519; $keyPair = Ed25519::generateKeyPair(); echo "PPS-PHP installed successfully.\n"; echo "Public key: " . \Pps\Crypto\Base64Url::encode($keyPair->publicKey) . "\n";
Quick Start
1. Registration
<?php require_once 'vendor/autoload.php'; use Pps\Client\RegistrationBuilder; use Pps\Server\RegistrationHandler; use Pps\Storage\InMemoryStorage; // Server side: create registration challenge $storage = new InMemoryStorage(); $handler = new RegistrationHandler($storage); $challenge = $handler->createChallenge('example.com'); // Client side: generate keys and sign registration $builder = new RegistrationBuilder(); $registration = $builder ->rpId('example.com') ->nonce($challenge->nonce) ->userHandle('user-123') ->deviceName('My Phone') ->build(); // Server side: verify and store $result = $handler->verify($registration, 'example.com', $challenge->nonce); if ($result->ok) { echo "Registration successful.\n"; echo "Device KID: " . $result->kid . "\n"; }
2. Single-Device Authentication
<?php use Pps\Client\AuthenticatorClient; use Pps\Server\AuthenticationHandler; use Pps\Server\ChallengeGenerator; // Server side: create authentication challenge $challengeGen = new ChallengeGenerator(); $challenge = $challengeGen->create('example.com'); // Client side: create Pulse $client = new AuthenticatorClient($clientState); $pulse = $client ->rpId('example.com') ->nonce($challenge->nonce) ->createPulse(); echo "Trust Code: " . $pulse->trustCode . "\n"; echo "Pulse Token: " . $pulse->token . "\n"; // Server side: verify Pulse $authHandler = new AuthenticationHandler($storage); $result = $authHandler->verify($pulse->token, 'example.com', $challenge->nonce); if ($result->ok) { echo "Authentication successful.\n"; }
3. Transaction Signing with AmountMark
<?php use Pps\Client\AuthenticatorClient; use Pps\Payload\TransactionObject; use Pps\Payload\ContextObject; // Create transaction $transaction = new TransactionObject( action: 'withdraw', amountMinor: 2500067, currency: 'IRR', recipient: 'IR820540102680020817909002' ); // Create context with transaction hash $context = new ContextObject(); $context->txHash = $transaction->hash(); $context->sessionId = $sessionId; // Client signs transaction $client = new AuthenticatorClient($clientState); $pulse = $client ->rpId('example.com') ->nonce($nonce) ->context($context) ->amountMinor(2500067) ->createPulse(); // Trust Code includes AmountMark echo "Trust Code: " . $pulse->trustCode . "\n"; // Output: 49371867 // Last two digits (67) match amount modulo 100
4. Silent Duress Authentication
<?php use Pps\Client\AuthenticatorClient; use Pps\Server\AuthenticationHandler; use Pps\Server\DuressHandler; // Client authenticates under duress $client = new AuthenticatorClient($clientState); $pulse = $client ->rpId('example.com') ->nonce($nonce) ->duress(true) // Use hidden duress key ->createPulse(); // Server verifies — response looks normal $authHandler = new AuthenticationHandler($storage); $result = $authHandler->verify($pulse->token, 'example.com', $nonce); // Outward response is identical to normal success echo json_encode(['status' => 'ok']); // Internal handling if ($result->honey) { $duressHandler = new DuressHandler($storage); $duressHandler->handleSilentAlert($result); // - Restrict account // - Block high-value operations // - Trigger silent alert // - Delay settlement }
5. Threshold Multi-Device Approval
<?php use Pps\Client\AuthenticatorClient; use Pps\Mode\ThresholdMode; use Pps\Server\AuthenticationHandler; // Device 1 (phone) creates payload $phone = new AuthenticatorClient($phoneState); $partial = $phone ->rpId('example.com') ->nonce($nonce) ->amountMinor(5000000) ->thresholdGroup($groupId, 2) ->createPartialPulse(); // Device 2 (watch) co-signs $watch = new AuthenticatorClient($watchState); $coSigned = $watch ->coSign($partial) ->createPulse(); // Server verifies threshold $authHandler = new AuthenticationHandler($storage); $result = $authHandler->verifyThreshold($coSigned->token, 'example.com', $nonce); if ($result->ok && $result->thresholdMet) { echo "Threshold approval successful.\n"; }
6. Policy Binding
<?php use Pps\Payload\PolicyObject; use Pps\Client\AuthenticatorClient; // Server defines policy $policy = new PolicyObject(); $policy->minTrustDigits = 8; $policy->requireBiometric = true; $policy->requireSecureEnclave = true; $policy->threshold = 2; $policy->maxAmountMinor = 10000000; $policy->requireProximity = true; $policy->requireAmountMark = true; // Client binds policy to Pulse $client = new AuthenticatorClient($clientState); $pulse = $client ->rpId('example.com') ->nonce($nonce) ->policy($policy) ->amountMinor(2500067) ->createPulse(); // Server verifies policy hash matches expected $result = $authHandler->verify( $pulse->token, 'example.com', $nonce, expectedPolicy: $policy );
7. Context Binding
<?php use Pps\Payload\ContextObject; use Pps\Client\AuthenticatorClient; // Create context with environmental data $context = new ContextObject(); $context->sessionId = $sessionId; $context->txHash = $txHash; $context->bleHash = hash('sha256', $bleBeaconPayload, true); $context->acousticHash = hash('sha256', $acousticNonce, true); $context->deviceIntegrityHash = hash('sha256', $integrityData, true); // Client binds context to Pulse $client = new AuthenticatorClient($clientState); $pulse = $client ->rpId('example.com') ->nonce($nonce) ->context($context) ->createPulse(); // Server verifies context hash $result = $authHandler->verify( $pulse->token, 'example.com', $nonce, expectedContext: $context );
8. Offline Mode
<?php use Pps\Client\AuthenticatorClient; use Pps\Mode\OfflineMode; // Client creates offline Pulse (no nonce) $client = new AuthenticatorClient($clientState); $pulse = $client ->rpId('example.com') ->offline(true) // No nonce required ->createPulse(); // Server verifies with stricter rate limiting $authHandler = new AuthenticationHandler($storage); $result = $authHandler->verifyOffline($pulse->token, 'example.com'); if ($result->ok) { echo "Offline authentication successful.\n"; }
Usage Modes
Mode Comparison
| Mode | Nonce | Network | Use Case |
|---|---|---|---|
| Single-Device Online | Required | Required | Standard login |
| Single-Device Offline | Optional | Not required | Terminals, IoT |
| Threshold | Required | Optional | High-value transactions |
| Duress | Required | Required | Coercion scenarios |
| Transaction Signing | Required | Required | Payments, withdrawals |
Mode Selection Guide
// Standard login $pulse = $client->rpId('example.com')->nonce($nonce)->createPulse(); // High-value transaction with threshold $pulse = $client ->rpId('example.com') ->nonce($nonce) ->amountMinor($amount) ->thresholdGroup($gid, 2) ->createPulse(); // Offline terminal $pulse = $client->rpId('example.com')->offline(true)->createPulse(); // Duress $pulse = $client->rpId('example.com')->nonce($nonce)->duress(true)->createPulse();
Storage Backends
PPS-PHP supports multiple storage backends:
In-Memory (Testing)
use Pps\Storage\InMemoryStorage; $storage = new InMemoryStorage();
File Storage (Development)
use Pps\Storage\FileStorage; $storage = new FileStorage('/var/lib/pps');
PDO Storage (Production)
use Pps\Storage\PdoStorage; $pdo = new PDO('mysql:host=localhost;dbname=pps', 'user', 'pass'); $storage = new PdoStorage($pdo);
Redis Storage (High Performance)
use Pps\Storage\RedisStorage; $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $storage = new RedisStorage($redis);
Framework Integration
Laravel
// app/Http/Controllers/PpsAuthController.php use Pps\Server\AuthenticationHandler; use Pps\Server\ChallengeGenerator; use Pps\Storage\PdoStorage; class PpsAuthController extends Controller { public function challenge(ChallengeGenerator $generator) { $challenge = $generator->create('example.com'); return response()->json([ 'nonce' => $challenge->nonceB64u, 'epoch' => $challenge->epoch, 'qr_payload' => $challenge->qrPayload, ]); } public function verify(Request $request, AuthenticationHandler $handler) { $result = $handler->verify( $request->input('pulse_token'), 'example.com', $request->input('nonce') ); if ($result->honey) { // Silent duress handling return response()->json(['status' => 'ok']); } if (!$result->ok) { return response()->json(['status' => 'error'], 401); } return response()->json(['status' => 'ok']); } }
Slim
use Pps\Server\AuthenticationHandler; use Slim\App; $app->post('/pps/challenge', function ($request, $response) { $generator = new ChallengeGenerator(); $challenge = $generator->create('example.com'); $response->getBody()->write(json_encode([ 'nonce' => $challenge->nonceB64u, 'epoch' => $challenge->epoch, ])); return $response->withHeader('Content-Type', 'application/json'); }); $app->post('/pps/verify', function ($request, $response) { $handler = new AuthenticationHandler($storage); $body = $request->getParsedBody(); $result = $handler->verify( $body['pulse_token'], 'example.com', $body['nonce'] ); $response->getBody()->write(json_encode(['status' => $result->ok ? 'ok' : 'error'])); return $response->withHeader('Content-Type', 'application/json'); });
API Reference
AuthenticatorClient
$client = new AuthenticatorClient(array $clientState); $pulse = $client ->rpId(string $rpId) ->nonce(string $nonceB64u) ->context(ContextObject $context) ->policy(PolicyObject $policy) ->amountMinor(int $amount) ->duress(bool $duress) ->offline(bool $offline) ->thresholdGroup(string $gid, int $threshold) ->createPulse(); // Returns PulseResult with: // - token: string (base64url Pulse Token) // - trustCode: string (8-digit Trust Code) // - trustPhrase: string (3-word Trust Phrase) // - epoch: int // - counter: int // - newClientState: array
AuthenticationHandler
$handler = new AuthenticationHandler(StorageInterface $storage); $result = $handler->verify( string $pulseToken, string $rpId, ?string $nonceB64u = null, ?int $amountMinor = null, ?ContextObject $expectedContext = null, ?PolicyObject $expectedPolicy = null ); // Returns VerificationResult with: // - ok: bool // - mode: string ('normal' | 'duress') // - honey: bool // - thresholdMet: bool // - error: ?string // - updates: array
Testing
# Run all tests composer test # Run unit tests only composer test:unit # Run integration tests only composer test:integration # Run with coverage composer test:coverage
Security
PPS-PHP is experimental and has not been independently audited.
Do not use in production without:
- cryptographic review
- security audit
- interoperability testing
- operational risk assessment
See SECURITY.md for vulnerability reporting.
License
Apache License 2.0
See LICENSE for details.
Links
- Specification: SPEC.md
- Documentation: docs/
- Internet-Draft: draft-hezami-pulseproof-sentinel
- Main Repository: pps-protocol/pulseproof-sentinel