verifyblind / verifyblind-php
Server-side verification of VerifyBlind result tokens and webhooks (RSA-PSS SHA-256, salt 32) for PHP.
Requires
- php: >=7.4
- phpseclib/phpseclib: ~3.0
Requires (Dev)
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Kimliğinizi Kanıtlayın, Gizliliğinizi Koruyun · Prove Your Identity, Protect Your Privacy
Türkçe
VerifyBlind doğrulama sonucunu kendi PHP sunucunuzda birkaç satırla doğrulamanızı sağlayan resmi kütüphane.
En zor kısım olan RSA-PSS imza kontrolünü (PHP'nin openssl_verify() fonksiyonu PSS yapamaz) sizin yerinize yapar.
Mimari
[Web widget'ı / mobil SDK] ← Doğrulama bitince imzalı `token` verir
│
│ POST { token }
▼
[Sizin PHP sunucunuz] ← Bu kütüphane burada çalışır
1. Enclave imzası (RSA-PSS, SHA-256, salt 32) → Verifier::verifyToken()
2. Nonce bir kez tüketilir (sizin deponuzda) → sizin $consume fonksiyonunuz
3. Sonuç, SİZİN sorduğunuzla karşılaştırılır → Verifier::checkAsked()
(üçü birden: Verifier::verify())
│
▼
Karar: yaş uygun mu, bu kişi daha önce geldi mi…
Kütüphane yalnız doğrulama tarafını yapar. Generate aracısı (API anahtarınızı ekleyen ve validations'ı
kendi ayarınızdan koyan uç) hâlâ sizdedir; örnek: example-web-php → api/generate.php.
Kurulum
composer require verifyblind/verifyblind-php
PHP 7.4 veya üstü. Tek bağımlılık phpseclib/phpseclib ~3.0 (saf PHP; ek eklenti ya da shell_exec gerekmez,
paylaşımlı hostinglerde de çalışır).
Hızlı Başlangıç
Doğrulama ucu (POST /verifyblind/verify), yaklaşık 15 satır:
<?php require __DIR__ . '/vendor/autoload.php'; use VerifyBlind\Cache\FileCache; use VerifyBlind\Exception\VerifyBlindException; use VerifyBlind\Verifier; header('Content-Type: application/json'); $token = json_decode(file_get_contents('php://input'), true)['token'] ?? ''; $vb = new Verifier(['cache' => new FileCache()]); try { $result = $vb->verify((string) $token, $consume); // $consume: aşağıda "Nonce kuralı" } catch (VerifyBlindException $e) { http_response_code($e->getHttpStatus()); exit(json_encode(['error' => $e->getErrorCode()])); } // İmza, nonce ve sorulan doğrulamalar kontrol edildi. Artık cevabı kullanabilirsiniz: echo json_encode(['ok' => true, 'age' => $result->age()]);
$result->userId(), nsbdId(), docId() kişiye özel takma kodlardır: veritabanınıza yazın, karar için
userId() kullanın (ör. engel listesi), ama asla loglamayın (aşağıya bakın).
Nonce kuralı
Generate çağrısı size bir nonce döndürür. Onu sorduğunuz doğrulamalarla birlikte saklayın
(Redis/veritabanı, süre 960 saniye). Doğrulamada aynı nonce tam bir kez tüketilir (al ve sil);
bilinmiyorsa, süresi geçtiyse ya da daha önce kullanıldıysa sonuç reddedilir. Bu, aynı token'ın ikinci
kez gönderilmesini (replay) engeller. Depo sizin uygulamanızdadır; kütüphane yalnız bir fonksiyon ister:
// generate aracısında, VerifyBlind nonce döndürdükten sonra: $pdo->prepare('INSERT INTO vb_nonces (nonce, asked, expires_at) VALUES (?, ?, ?)') ->execute([$nonce, json_encode(['age' => '18+', 'user_id' => true]), time() + 960]); // doğrulamada: tek seferlik al-ve-sil $consume = function (string $nonce) use ($pdo): ?array { $q = $pdo->prepare('SELECT asked FROM vb_nonces WHERE nonce = ? AND expires_at > ?'); $q->execute([$nonce, time()]); $asked = $q->fetchColumn(); $d = $pdo->prepare('DELETE FROM vb_nonces WHERE nonce = ?'); $d->execute([$nonce]); // Satırı yalnız bir istek silebilir; aynı anda gelen tekrar 0 satır görür ve reddedilir. return ($asked !== false && $d->rowCount() === 1) ? json_decode($asked, true) : null; };
Generate ve doğrulama aynı tarayıcı oturumundan geliyorsa (web widget'ı) PHP oturumu da yeter:
$_SESSION['vb'][$nonce] = $asked; diye saklayın, doğrulamada okuyup hemen unset() edin.
Sorulan doğrulamalar kuralı
Sonucu sizin sunucunuzun sorduğuna göre okuyun; tarayıcının ya da uygulamanın söylediğine asla.
Generate isteğinin gövdesi DevTools'ta değiştirilebilir: "18+" yerine "1+" soran ziyaretçi, enclave'den
dürüstçe imzalanmış bir age: true alır, ama "1+" koşulu için. verify() (ve tek başına
Verifier::checkAsked($asked, $validations)) şunları denetler:
- Yaş sorduysanız:
validations.agetrue/false olmalı vevalidations.age_conditionsorduğunuz koşulla birebir aynı olmalı. Eksik ya da farklıysa → ret. - Yaş sormadıysanız: cevapta yaş bulunmamalı.
user_idsorduysanız: dolu biruser_idgelmeli.
checkAsked() sorun yoksa null, varsa hata metni döndürür.
Demo kart: validations.is_test: true ya da TEST_ ile başlayan kodlar demo karttan gelir (yalnız test
partner'larında). verify() bunları varsayılan olarak reddeder; test ortamında allowTestCards => true verin.
$result->isTest() ile de ayırabilirsiniz.
İptal webhook'u (revoke)
Kullanıcı VerifyBlind uygulamasında onayını geri çekerse ya da silme isterse VerifyBlind, partner portalı
Ayarlar'daki revoke adresinize imzalı bir JSON gönderir. İmza: "{X-Webhook-Timestamp}.{ham gövde}"
üzerinde RSA-PSS-SHA256 (salt 32), anahtar /api/public/webhook-signing-key. Bu, API anahtarınızla HMAC
değildir. Zaman damgası ±5 dakikadan eskiyse reddedilir.
$raw = file_get_contents('php://input'); // ham gövde; yeniden JSON'a çevirmeyin try { $body = $vb->verifyWebhook($raw, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null, $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? null); } catch (VerifyBlindException $e) { http_response_code($e->getHttpStatus()); exit; } // $body['nonce'] her zaman vardır; o nonce'a bağlı veriyi silin ya da işaretleyin. http_response_code(200);
Aynı fonksiyon B deseninin (sunucuda çözme) callback imzasını da doğrular; şifreli içeriği çözme bu kütüphanede yoktur (bkz. example-web-dotnet).
Konfigürasyon Parametreleri
| Parametre | Varsayılan | Açıklama |
|---|---|---|
apiUrl |
https://api.verifyblind.com |
VerifyBlind API adresi |
keyCacheTtl |
60 |
Enclave anahtarının önbellek süresi (sn). Kısa tutulur: enclave yeniden başlayınca anahtar değişir |
webhookKeyCacheTtl |
3600 |
Webhook anahtarının önbellek süresi (sn) |
refreshInterval |
30 |
İki zorunlu anahtar tazelemesi arasındaki en kısa süre (sn); bozuk imzalı istekler API'yi yoramaz |
cache |
süreç içi bellek | PSR-16 uyumlu get()/set() olan her nesne (Redis, APCu…) ya da new FileCache() |
httpGet |
curl / file_get_contents |
function (string $url): string — 200 gövdesini döndürür, aksi hâlde istisna atar |
timeout |
10 |
Yerleşik HTTP isteğinin zaman aşımı (sn) |
allowTestCards |
false |
verify() demo kart sonucunu kabul etsin mi |
webhookTolerance |
300 |
Webhook zaman damgası için izin verilen fark (sn) |
PHP-FPM/Apache'de bellek önbelleği tek istek yaşar; her doğrulama anahtarı bir kez çeker. Bunu önlemek için
FileCache ya da kendi PSR-16 önbelleğinizi verin. İmza tutmazsa anahtar bir kez tazelenip tekrar denenir.
Hata kodları
Tüm istisnalar VerifyBlind\Exception\VerifyBlindException'dan türer; getErrorCode() sabit bir kod,
getHttpStatus() önerilen HTTP durumunu verir.
| Sınıf | Kod | HTTP | Anlamı |
|---|---|---|---|
InvalidTokenException |
bad_token |
400 | Token biçimi bozuk (base64 JSON {payload, signature} değil) |
InvalidSignatureException |
bad_signature |
401 | İmza tutmuyor (anahtar tazelendikten sonra da) |
KeyUnavailableException |
key_unavailable |
503 | Anahtar alınamadı ya da okunamadı. Altyapı hatasıdır, token'ın reddi değil |
NonceException |
nonce_invalid |
401 | Nonce bilinmiyor, süresi geçmiş ya da kullanılmış |
AskedMismatchException |
asked_mismatch |
401 | Cevap, sorduğunuz doğrulamalarla uyuşmuyor |
TestCardException |
test_card |
403 | Demo kart sonucu ve allowTestCards kapalı |
Güvenlik Notları
- Kimlik kodlarını asla loglamayın.
user_id,nsbd_id,doc_idtakma adlı kişisel veridir: diğer kullanıcı verileriniz gibi koruyun, log'a, hata mesajına ya da analitiğe yazmayın. Kütüphane hiçbir şey loglamaz, hata mesajlarında bu kodlar geçmez,var_dump($result)/print_r($result)da onları gizler. - Karar yalnız sunucuda. Widget'ın
onSuccessverisine ya da uygulamanın gösterdiğine güvenmeyin;token'ı sunucunuza gönderip burada doğrulayın. - API anahtarı sunucu sırrıdır. Bu kütüphane API anahtarı istemez; generate aracınızda ortam değişkeninden okuyun.
verifySignature()phpseclib'in süreç genelindekiRSA::disableSaltLengthDiscovery()ayarını açar. Bu ayar kapalıyken phpseclib başka salt uzunluklarıyla yapılmış imzaları da kabul eder.
Sürüm Geçmişi
| Sürüm | Değişiklik |
|---|---|
| 1.0.0 | İlk sürüm: Verifier (token, nonce, sorulan doğrulamalar, webhook), Result, FileCache |
English
The official library for verifying a VerifyBlind result on your own PHP server in a few lines. It does the
hard part for you: RSA-PSS signature verification (PHP's openssl_verify() cannot do PSS).
Architecture
[Web widget / mobile SDK] ← Gives you a signed `token` when verification finishes
│
│ POST { token }
▼
[Your PHP server] ← This library runs here
1. Enclave signature (RSA-PSS, SHA-256, salt 32) → Verifier::verifyToken()
2. Nonce consumed exactly once (in your store) → your $consume function
3. Answer read against what YOU asked → Verifier::checkAsked()
(all three: Verifier::verify())
│
▼
Decision: old enough? has this person been here before? …
The library covers verification only. The generate proxy (the endpoint that adds your API key and sets
validations from your own configuration) stays in your app; see
example-web-php → api/generate.php.
Installation
composer require verifyblind/verifyblind-php
PHP 7.4 or newer. The only dependency is phpseclib/phpseclib ~3.0 (pure PHP; no extension or shell_exec
needed, so it works on shared hosting).
Quick Start
Verify endpoint (POST /verifyblind/verify), about 15 lines:
<?php require __DIR__ . '/vendor/autoload.php'; use VerifyBlind\Cache\FileCache; use VerifyBlind\Exception\VerifyBlindException; use VerifyBlind\Verifier; header('Content-Type: application/json'); $token = json_decode(file_get_contents('php://input'), true)['token'] ?? ''; $vb = new Verifier(['cache' => new FileCache()]); try { $result = $vb->verify((string) $token, $consume); // $consume: see "Nonce rule" below } catch (VerifyBlindException $e) { http_response_code($e->getHttpStatus()); exit(json_encode(['error' => $e->getErrorCode()])); } // Signature, nonce and asked validations are checked. Now use the answer: echo json_encode(['ok' => true, 'age' => $result->age()]);
$result->userId(), nsbdId() and docId() are pseudonymous person codes: store them, use userId() for
decisions (e.g. block lists), and never log them (see below).
Nonce rule
Generate returns a nonce. Store it together with the validations you asked (Redis/database, TTL
960 seconds). At verification the nonce is consumed exactly once (get and delete); unknown, expired
or already used means reject. This stops the same token from being replayed. The store lives in your app;
the library only needs a function:
// in the generate proxy, after VerifyBlind returned the nonce: $pdo->prepare('INSERT INTO vb_nonces (nonce, asked, expires_at) VALUES (?, ?, ?)') ->execute([$nonce, json_encode(['age' => '18+', 'user_id' => true]), time() + 960]); // at verification: one-time get-and-delete $consume = function (string $nonce) use ($pdo): ?array { $q = $pdo->prepare('SELECT asked FROM vb_nonces WHERE nonce = ? AND expires_at > ?'); $q->execute([$nonce, time()]); $asked = $q->fetchColumn(); $d = $pdo->prepare('DELETE FROM vb_nonces WHERE nonce = ?'); $d->execute([$nonce]); // Only one request can delete the row; a parallel replay sees 0 rows and is rejected. return ($asked !== false && $d->rowCount() === 1) ? json_decode($asked, true) : null; };
When generate and verify come from the same browser session (web widget), a PHP session also works:
store $_SESSION['vb'][$nonce] = $asked; and read it then unset() it at verification.
Asked-validations rule
Read the answer against what your server asked, never against what the browser or app says. The
generate request body can be edited in DevTools: a visitor who changes "18+" to "1+" gets an honestly
signed age: true — for "1+". verify() (and on its own Verifier::checkAsked($asked, $validations)) checks:
- Age asked:
validations.agemust be true/false andvalidations.age_conditionmust equal the asked condition exactly. Missing or different → reject. - Age not asked: no age answer may be present.
user_idasked: a non-emptyuser_idmust be present.
checkAsked() returns null when the answer matches, or an error text.
Demo card: validations.is_test: true or codes starting with TEST_ come from a demo card (test partners
only). verify() rejects them by default; pass allowTestCards => true on test setups. $result->isTest()
tells them apart too.
Revoke webhook
When a user withdraws consent or asks for erasure in the VerifyBlind app, VerifyBlind POSTs signed JSON to the
revoke URL set in the partner portal Settings. Signature: RSA-PSS-SHA256 (salt 32) over
"{X-Webhook-Timestamp}.{raw body}", key from /api/public/webhook-signing-key. It is not an HMAC with
your API key. Timestamps more than ±5 minutes off are rejected.
$raw = file_get_contents('php://input'); // the raw body; do not re-encode it try { $body = $vb->verifyWebhook($raw, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null, $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? null); } catch (VerifyBlindException $e) { http_response_code($e->getHttpStatus()); exit; } // $body['nonce'] is always present; delete or mark the data tied to that nonce. http_response_code(200);
The same method verifies pattern B (server decrypt) callback signatures; decrypting the encrypted response is not part of this library (see example-web-dotnet).
Configuration Parameters
| Option | Default | Description |
|---|---|---|
apiUrl |
https://api.verifyblind.com |
VerifyBlind API base URL |
keyCacheTtl |
60 |
Enclave key cache lifetime (s). Short on purpose: the key rotates when the enclave restarts |
webhookKeyCacheTtl |
3600 |
Webhook key cache lifetime (s) |
refreshInterval |
30 |
Minimum time between two forced key refreshes (s), so bad-signature requests cannot hammer the API |
cache |
in-process memory | Any object with PSR-16 get()/set() (Redis, APCu, …) or new FileCache() |
httpGet |
curl / file_get_contents |
function (string $url): string — returns the 200 body, throws otherwise |
timeout |
10 |
Timeout of the built-in HTTP request (s) |
allowTestCards |
false |
Whether verify() accepts demo-card results |
webhookTolerance |
300 |
Allowed webhook timestamp difference (s) |
Under PHP-FPM/Apache the memory cache lives for one request, so each verification fetches the key once. Pass
FileCache or your own PSR-16 cache to avoid that. When a signature fails, the key is refreshed once and the
check is retried.
Error codes
Every exception extends VerifyBlind\Exception\VerifyBlindException; getErrorCode() gives a stable code and
getHttpStatus() a suggested HTTP status.
| Class | Code | HTTP | Meaning |
|---|---|---|---|
InvalidTokenException |
bad_token |
400 | Malformed token (not base64 JSON {payload, signature}) |
InvalidSignatureException |
bad_signature |
401 | Signature does not verify (also after a key refresh) |
KeyUnavailableException |
key_unavailable |
503 | Key could not be fetched or read. An infrastructure error, not a rejection |
NonceException |
nonce_invalid |
401 | Nonce unknown, expired or already used |
AskedMismatchException |
asked_mismatch |
401 | The answer does not match the validations you asked |
TestCardException |
test_card |
403 | Demo-card result while allowTestCards is off |
Security Notes
- Never log identity codes.
user_id,nsbd_idanddoc_idare pseudonymous personal data: protect them like your other user data and keep them out of logs, error messages and analytics. The library logs nothing, its error messages never contain these codes, andvar_dump($result)/print_r($result)hide them. - Decide on the server only. Do not trust the widget's
onSuccessdata or what an app shows; send thetokento your server and verify it here. - The API key is a server secret. This library does not need it; read it from an environment variable in your generate proxy.
verifySignature()turns on phpseclib's process-wideRSA::disableSaltLengthDiscovery(). Without it phpseclib also accepts signatures made with other salt lengths.
Version History
| Version | Changes |
|---|---|
| 1.0.0 | First release: Verifier (token, nonce, asked validations, webhook), Result, FileCache |
Lisans · License
Apache License 2.0 — bkz. / see LICENSE.