pps-php

PPS-PHP

PHP implementation of the PulseProof Sentinel Protocol (PPS)

IETF Internet-Draft Protocol Version 1 PHP 8.1+ Status: Experimental License: Apache-2.0


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:


Features

Core Protocol

Advanced Features

Implementation Quality


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:

See SECURITY.md for vulnerability reporting.


License

Apache License 2.0

See LICENSE for details.