Download the PHP package report-uri/dbsc-php without Composer
On this page you can find all versions of the php package report-uri/dbsc-php. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download report-uri/dbsc-php
More information about report-uri/dbsc-php
Files in report-uri/dbsc-php
Package dbsc-php
Short Description A small, framework-agnostic PHP server library for Device Bound Session Credentials (DBSC)
License MIT
Homepage https://github.com/report-uri/dbsc-php
Informations about the package dbsc-php
dbsc-php
A small, framework-agnostic PHP server library for Device Bound Session Credentials (DBSC).
DBSC cryptographically binds an authenticated session to a hardware-backed device key (TPM / secure enclave). A stolen session cookie can no longer be replayed from another device: the short-lived bound cookie expires every few minutes and is only refreshable by signing a server challenge with a private key that never leaves the device.
It is pure HTTP headers — no JavaScript, no frontend assets, no database tables required. Non-DBSC browsers simply ignore the registration header and continue on normal cookie auth, so enabling it cannot lock anyone out.
This library is extracted from Report URI's production DBSC integration (report-uri/passkeys-php is its passkeys sibling). It carries the wire-protocol corrections that only surface when integrating against a real browser — see Wire-protocol notes.
Design
- Zero dependencies beyond
ext-openssl/ext-json. ~700 lines, auditable in one sitting. - Framework-agnostic. The library never reads superglobals, sends a header, or sets a cookie. Every operation takes a
RequestContextyou build from your framework's request and returns aDbscResponseyou apply to your framework's response. - Storage is yours. You implement
StoreInterface(Redis, a table, …). AnInMemoryStoreis bundled for tests and the demo. - The crypto is deliberately minimal — ES256 only, signature + single-use challenge nonce. See the class docblock on
JwtVerifierfor whyiat/exp/iss/audare intentionally not checked.
Installation
Autoloads under PSR-4 as ReportUri\Dbsc\. The entry point is ReportUri\Dbsc\DbscServer.
Flow
A complete reference front controller is in _test/server.php. DBSC is browser-native (no JS API to script), so exercise it with a DBSC-capable browser over HTTPS.
Enforcement gate
The library exposes the primitives but does not run the gate itself — where you enforce depends on your routing. The recommended policy (also in _test/server.php):
Enforce on document loads and on subresources past the registration grace — not document-only, which would let a stolen cookie exfiltrate via XHR within the cookie lifetime. Skip the gate on the /dbsc/* endpoints themselves.
Storage
Key DBSC state by your stable session id, in a dedicated key space — never in a read-modify-written shared session blob.
This is the one non-obvious correctness requirement. Report URI shipped DBSC with state in the PHP session blob; the post-login navigation races the
/dbsc/registerPOST, both rewrite the whole blob last-writer-wins, the binding is clobbered, and enforcement silently no-ops — leaving exactly the stolen-cookie hole DBSC exists to close.StoreInterfacedocuments the requirements; back it with Redis or a table keyed by session id.
Pending registrations expire on the challenge TTL; bindings expire with the session lifetime.
Wire-protocol notes
Baked into this library from integration testing against real Chrome — change with care:
- Registration is single-phase; refresh is two-phase (403 + challenge, then 200). This is the opposite of how the spec reads at first glance.
- The first refresh can optionally be made single-phase. Steady-state refreshes already are (every 200 hands back the next challenge). Call
advertiseRefreshChallenge($binding, $ctx)on an ordinary authenticated document response in the registration→first-refresh window and it attaches the seedSecure-Session-Challengeonce (recording a one-way mark on the binding so later responses stay silent), so the browser holds a challenge when its first/dbsc/refreshfires and skips the 403. Spec-mandated: never attach this to the registration response (§9.2.1/§8.7 — theidmust name an already-existing session); the method takes aBinding, which only exists post-registration, so misuse is structurally impossible. If the single delivery is missed the browser simply falls back to the two-phase path — no regression. A reactive 403 racing the advertised value is covered by single-depth challenge overlap (the immediately-previous challenge is accepted inrefresh()until its own TTL), mirroring the bound-cookie overlap. - No
Secure-Session-Challengeon the registration response — Chrome reports a Challenge Error. The first refresh-flow 403 issues the challenge; the binding seeds an internal one only to stay valid until then. - Both the cookie value and the challenge must rotate on every refresh. Re-emitting the existing cookie value makes Chrome treat it as "no refresh happened" and terminate.
Secure-Session-Challengemust carry theidsf-parameter naming the session.challengeTtlmust exceedcookieMaxAge(theConfigconstructor enforces this) so a challenge the browser cached just before cookie expiry is still valid when it is used.- The bound cookie uses
__Host-, soinclude_siteisfalse(no subdomain span).
Tests
A self-contained harness (no PHPUnit): it generates a real EC P-256 device key, builds the JWTs exactly as Chrome does, and drives the full register/refresh/enforce/revoke flow plus the attack cases (wrong device key, wrong/expired challenge, stale cookie, alg=none).
License
MIT — see LICENSE. © 2026 Report-URI Ltd.
All versions of dbsc-php with dependencies
ext-openssl Version *
ext-json Version *