From cd08663856d37202b9ac073038c6f26d8811dfb3 Mon Sep 17 00:00:00 2001 From: Daniel Alfaro Date: Sat, 8 Aug 2026 20:40:56 -0400 Subject: [PATCH] Harden SDK runtime compatibility and release governance --- CHANGELOG.md | 9 ++++++++- COMPATIBILITY.md | 16 ++++++++++++++++ README.md | 9 +++++---- UPGRADING.md | 23 +++++++++++++++++++++++ composer.json | 2 +- docs/RELEASE_2_5_PLAN.md | 16 ++++++++-------- docs/SDK_2_ADOPTION_AUDIT.md | 30 +++++++++++++----------------- src/Oidc/DpopKey.php | 4 ++++ tests/DpopKeyTest.php | 3 +++ 9 files changed, 81 insertions(+), 31 deletions(-) create mode 100644 COMPATIBILITY.md create mode 100644 UPGRADING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 773094c..40e9752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog -## 2.5.0 - Unreleased +## 2.5.1 - Unreleased + +- Restore ES256 DPoP key generation on OpenSSL 3.6 while asserting that the + generated named curve remains P-256 (`prime256v1`). +- Run PHPStan with an explicit bounded memory limit for reproducible local and + CI verification on smaller development machines. + +## 2.5.0 - 2026-08-01 - Add durable, opaque, exact-once login intents for state, nonce, PKCE verifier, allowlisted return paths, browser binding and correlation IDs. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 0000000..340b6aa --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,16 @@ +# Compatibility + +| SDK line | PHP | Identity contract | Laravel adapter | Status | +|---|---|---|---|---| +| 2.5.x | 8.2–8.4 | `^2.0` | `identity-laravel ^2.5` | Current | +| 2.0.x | 8.2–8.4 | `^2.0` | `identity-laravel ^2.0` | Security fixes only | +| 1.2.x | 8.2–8.4 | `^1.1` | Application integration | Migration only | + +The high-assurance profile requires server metadata and client registration for +PAR, JARM, RFC 9207 issuer binding, DPoP and `private_key_jwt`. Package versions +do not enable those capabilities by themselves. Consumers must validate their +exact issuer, callback, tenant binding and negative/replay cases before rollout. + +SDK 2.5 Laravel consumers must use shared atomic cache or database storage for +login intents. In-memory storage is supported only for tests and local, +single-process experiments. diff --git a/README.md b/README.md index 1469974..8430a9b 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,11 @@ Never infer high-assurance support from a successful login. Discover metadata, run `EnterpriseProfileValidator`, store the authorization transaction server-side, and process callbacks through `AuthorizationResponseProcessor`. See [the Laravel integration guide](docs/INTEGRATION_LARAVEL.md). -The first-party `novvor/identity-laravel` adapter is published for SDK 2.0. Its -2.5 release must adopt durable login intents before consumers can claim the -2.5 integration contract. Until then Laravel applications must keep protocol -orchestration in one tested application adapter, never in controllers. The +The first-party `novvor/identity-laravel` adapter publishes the 2.5 integration +boundary with shared-cache, encrypted, exact-once login intents. Applications +must keep protocol orchestration in that tested adapter, never in controllers. +Adopting the package still requires an application-specific runtime validation; +a compatible dependency constraint alone is not production evidence. The cross-package adoption audit and 2.5 release gates are in [`docs/SDK_2_ADOPTION_AUDIT.md`](docs/SDK_2_ADOPTION_AUDIT.md) and [`docs/RELEASE_2_5_PLAN.md`](docs/RELEASE_2_5_PLAN.md). diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..1bf9990 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,23 @@ +# Upgrading + +## From 2.0 to 2.5 + +Use `LoginIntentManager` with a shared, atomic `LoginIntentStore`. Laravel +applications should use `novvor/identity-laravel ^2.5`, whose browser session +contains only an opaque handle while PKCE, nonce, state, return destination and +DPoP private material remain encrypted server-side and are consumed once. + +Do not deploy a dependency-only upgrade. Validate exact issuer and redirect URI, +callback replay rejection, state and nonce mismatch, token signature and key +rotation, tenant binding, logout, and the selected standard or high-assurance +profile against the target Identity environment. + +## From 1.x to 2.x + +Treat the major upgrade as an authentication-boundary migration. The 2.x +high-assurance profile fails closed unless Discovery and client registration +support PAR, JARM, RFC 9207, DPoP and `private_key_jwt`. Keep the standard +profile unless all of those capabilities have been provisioned and tested. + +See `COMPATIBILITY.md`, `docs/INTEGRATION_LARAVEL.md` and +`docs/SECURITY_PROFILE.md` for the supported boundaries. diff --git a/composer.json b/composer.json index bee12b9..813dbd9 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "minimum-stability": "stable", "prefer-stable": true, "scripts": { - "analyse": "phpstan analyse --no-progress", + "analyse": "php -d memory_limit=512M vendor/bin/phpstan analyse --no-progress", "test": "phpunit", "verify": [ "@composer validate --strict", diff --git a/docs/RELEASE_2_5_PLAN.md b/docs/RELEASE_2_5_PLAN.md index 89d0295..664609f 100644 --- a/docs/RELEASE_2_5_PLAN.md +++ b/docs/RELEASE_2_5_PLAN.md @@ -8,14 +8,13 @@ Date: 2026-08-01 |---|---| | `novvor/identity-contracts` v2.0.0 | Published baseline | | `novvor/identity-sdk-php` v2.0.0 | Published baseline | -| SDK 2.5 core branch | Candidate; not tagged | -| First-party Laravel adapter | `v2.0.1` published; 2.5 durable-intent upgrade pending | +| SDK 2.5 core | Immutable `v2.5.0` tag exists; GitHub release record pending | +| First-party Laravel adapter | `v2.5.1` tagged with durable encrypted login intents | | Platform and FilaSign runtime upgrade | Not yet validated against 2.5 | | Console v1-to-v2 migration | Not started | -The published adapter is an SDK 2.0 integration boundary. Its missing 2.5 -durable-intent capability is a consumer rollout blocker, not a reason to weaken -the core release gate or duplicate protocol logic in controllers. +The adapter now implements the SDK 2.5 durable-intent boundary. Consumer runtime +validation remains a rollout blocker and is not replaced by package-level tests. ## Core release gate @@ -37,9 +36,10 @@ consumer-rollout approval. ## Consumer order -1. Publish the core SDK `v2.5.0` only after the core gate is green. -2. Publish a Laravel integration package that uses durable login intents and - makes the transaction lifecycle a single supported boundary. +1. Publish a GitHub release record for the existing immutable SDK `v2.5.0` tag + after attaching the successful core-gate evidence; never retag it. +2. Retain the Laravel adapter's durable-login-intent transaction lifecycle as + the single supported Laravel boundary. 3. Upgrade Enix Platform and FilaSign in independent branches; run their browser and negative callback flows against the new package. 4. Migrate Enix Console from `^1.1` to `^2.5` in a separate review because it diff --git a/docs/SDK_2_ADOPTION_AUDIT.md b/docs/SDK_2_ADOPTION_AUDIT.md index f825ea5..473633e 100644 --- a/docs/SDK_2_ADOPTION_AUDIT.md +++ b/docs/SDK_2_ADOPTION_AUDIT.md @@ -9,10 +9,10 @@ framework-neutral backend consumers. It now includes durable, opaque login intents so applications can keep a return destination, PKCE verifier, nonce and state on the server under exact-once consumption semantics. -The first-party Laravel adapter exists and is released as `v2.0.1`. It uses an -encrypted Laravel session transaction and has not adopted the SDK 2.5 durable, -opaque login-intent contract. Therefore a consumer-wide Laravel upgrade to 2.5 -is not releasable until the adapter is updated, tested and released. +The first-party Laravel adapter is tagged as `v2.5.1`. It adopts the SDK 2.5 +durable, opaque login-intent contract with encrypted shared-cache storage and +exact-once consumption. Consumer-wide rollout still requires per-application +runtime evidence; package compatibility is not deployment approval. This is `PASS_LOCAL_INTEGRATION`, not OpenID certification or production proof. @@ -22,7 +22,7 @@ This is `PASS_LOCAL_INTEGRATION`, not OpenID certification or production proof. |---|---|---| | `identity-contracts` | claim names and security profiles | transport, secrets | | `identity-sdk-php` | framework-neutral OAuth/OIDC protocol | Laravel session, admin APIs | -| `identity-laravel` | Laravel config, DI and transaction lifecycle; published `v2.0.1`, 2.5 durable-intent upgrade pending | tenant authorization policy | +| `identity-laravel` | Laravel config, DI and durable transaction lifecycle; `v2.5.1` tagged | tenant authorization policy | | `identity-admin-sdk-php` | privileged control-plane transport | user login/session logic | | `identity-sdk-testing` | truthful fakes and negative fixtures | real keys, tokens, customer data | @@ -42,10 +42,8 @@ This is `PASS_LOCAL_INTEGRATION`, not OpenID certification or production proof. 11. Bind UserInfo `sub` to the ID Token `sub`. 12. Map tenant and permissions in the application, then regenerate its session. -Laravel consumers must use the published adapter for SDK 2.0 or one tested -application integration service. The adapter requires a 2.5 update and release -against `LoginIntentManager` before it can be treated as the consumer-wide 2.5 -contract; controllers must not reconstruct this flow. +Laravel consumers on SDK 2.5 must use the tagged adapter and its +`LoginIntentManager` boundary; controllers must not reconstruct this flow. ## Capability truth @@ -68,13 +66,11 @@ contract; controllers must not reconstruct this flow. ## Remaining release blockers -1. Complete the 2.5 core release gate and publish an immutable `v2.5.0` tag. -2. Update, release and test the first-party Laravel adapter against durable - `LoginIntentManager` storage; retain its current 2.0 contract until then. -3. Run a clean Composer install using tags only. -4. Validate Platform and FilaSign as reference consumers end to end. -5. Migrate Console from the v1 line as a separate, explicitly reviewed change. -6. Run negative issuer, callback replay, tenant mismatch and key-rotation tests. -7. Validate staging runtime and an external OpenID conformance profile. +1. Publish a GitHub release record for the immutable `v2.5.0` tag with gate evidence. +2. Run a clean Composer install using tags only. +3. Validate Platform and FilaSign as reference consumers end to end. +4. Migrate Console from the v1 line as a separate, explicitly reviewed change. +5. Run negative issuer, callback replay, tenant mismatch and key-rotation tests. +6. Validate staging runtime and an external OpenID conformance profile. No package should claim `PASS_RUNTIME` until those external gates have evidence. diff --git a/src/Oidc/DpopKey.php b/src/Oidc/DpopKey.php index 3c68b88..14c95e7 100644 --- a/src/Oidc/DpopKey.php +++ b/src/Oidc/DpopKey.php @@ -31,6 +31,10 @@ public static function generateEs256(): self $resource = openssl_pkey_new([ 'private_key_type' => OPENSSL_KEYTYPE_EC, + // OpenSSL 3.6 requires an explicit minimum private-key size even + // when the selected named curve determines the actual EC size. + // The generated key remains prime256v1/P-256. + 'private_key_bits' => 384, 'curve_name' => 'prime256v1', ]); if ($resource === false || ! openssl_pkey_export($resource, $privateKey)) { diff --git a/tests/DpopKeyTest.php b/tests/DpopKeyTest.php index 5343b14..e63e8e8 100644 --- a/tests/DpopKeyTest.php +++ b/tests/DpopKeyTest.php @@ -15,6 +15,9 @@ public function test_generates_a_fresh_es256_key_without_private_jwk_material(): self::assertSame('ES256', $first->algorithm); self::assertSame('EC', $first->publicJwk['kty']); self::assertSame('P-256', $first->publicJwk['crv']); + $privateKey = openssl_pkey_get_private($first->privateKey); + self::assertInstanceOf(\OpenSSLAsymmetricKey::class, $privateKey); + self::assertSame('prime256v1', openssl_pkey_get_details($privateKey)['ec']['curve_name'] ?? null); self::assertArrayNotHasKey('d', $first->publicJwk); self::assertNotSame($first->privateKey, $second->privateKey); self::assertNotSame($first->publicThumbprint(), $second->publicThumbprint());