diff --git a/CHANGELOG.md b/CHANGELOG.md index de2859705..92a94aefc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ the EngineBlock wiki. Fixes: * Fix Dotenv never loading `.env.{ENV}` overrides. +* Fix login failing with `ValueNotConvertible: ... Undefined array key "certData"` when reading `certificates` metadata written by EngineBlock < 7.2 (#2044). The PHP 8.5 Rector upgrade changed `X509CertificateLazyProxy` serialization from `__sleep()` to `__serialize()`, which silently changed the serialized key format stored in `sso_provider_roles_eb5`. The serialized format is restored to the legacy `__sleep()` format so old and new versions can read each other's data (red/green safe); no metadata re-push or migration is needed. Features: * Added `coin:azure_domain_hint` configuration option for IdPs. When set, EngineBlock appends a `whr=` query parameter to the HTTP-Redirect AuthnRequest sent to the IdP, allowing Microsoft Azure / EntraID to skip the account picker (#1864). diff --git a/src/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxy.php b/src/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxy.php index 43cd1f9c1..c553c4733 100644 --- a/src/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxy.php +++ b/src/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxy.php @@ -66,15 +66,23 @@ public function __call($methodName, $methodArguments) /** * Take care not to serialize the openSSL resource ($this->certificate). + * + * Keys use the mangled private-property format that __sleep() produced, so the + * serialized representation stored in the database stays readable by older + * EngineBlock versions (red/green deployments). */ public function __serialize(): array { - return array('certData' => $this->certData, 'factory' => $this->factory); + $prefix = "\0" . self::class . "\0"; + + return array($prefix . 'certData' => $this->certData, $prefix . 'factory' => $this->factory); } public function __unserialize(array $data): void { - $this->certData = $data['certData']; - $this->factory = $data['factory']; + $prefix = "\0" . self::class . "\0"; + + $this->certData = $data[$prefix . 'certData'] ?? $data['certData']; + $this->factory = $data[$prefix . 'factory'] ?? $data['factory']; } } diff --git a/tests/unit/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxyTest.php b/tests/unit/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxyTest.php index 302edbcf8..7f9452ba9 100644 --- a/tests/unit/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxyTest.php +++ b/tests/unit/OpenConext/EngineBlock/Metadata/X509/X509CertificateLazyProxyTest.php @@ -91,4 +91,78 @@ public function test_survives_serialization_roundtrip() $this->assertEquals($pemBefore, $restored->toPem(), 'Certificate data survives serialization roundtrip'); } + + public function test_unserializes_legacy_sleep_format() + { + $certData = $this->validCertData(); + $expectedPem = (new X509CertificateLazyProxy(new X509CertificateFactory(), $certData))->toPem(); + + $restored = unserialize($this->legacySerializedPayload($certData)); + + $this->assertEquals( + $expectedPem, + $restored->toPem(), + 'Rows serialized by the pre-__serialize() code (mangled private property keys) must still unserialize' + ); + } + + public function test_unserializes_plain_key_format() + { + $certData = $this->validCertData(); + $expectedPem = (new X509CertificateLazyProxy(new X509CertificateFactory(), $certData))->toPem(); + + $proxyClass = X509CertificateLazyProxy::class; + $factoryClass = X509CertificateFactory::class; + $payload = sprintf( + 'O:%d:"%s":2:{%s%ss:7:"factory";O:%d:"%s":0:{}}', + strlen($proxyClass), + $proxyClass, + serialize('certData'), + serialize($certData), + strlen($factoryClass), + $factoryClass + ); + + $restored = unserialize($payload); + + $this->assertEquals( + $expectedPem, + $restored->toPem(), + 'Rows serialized with plain __serialize() keys must also unserialize' + ); + } + + public function test_serializes_to_legacy_sleep_format() + { + $certData = $this->validCertData(); + $proxy = new X509CertificateLazyProxy(new X509CertificateFactory(), $certData); + + $this->assertSame( + $this->legacySerializedPayload($certData), + serialize($proxy), + 'Serialized format must stay identical to the legacy __sleep() format so older EngineBlock versions can read it' + ); + } + + private function legacySerializedPayload(string $certData): string + { + $proxyClass = X509CertificateLazyProxy::class; + $factoryClass = X509CertificateFactory::class; + + return sprintf( + 'O:%d:"%s":2:{%s%s%sO:%d:"%s":0:{}}', + strlen($proxyClass), + $proxyClass, + serialize("\0" . $proxyClass . "\0certData"), + serialize($certData), + serialize("\0" . $proxyClass . "\0factory"), + strlen($factoryClass), + $factoryClass + ); + } + + private function validCertData(): string + { + return "MIIDXzCCAkegAwIBAgIJAM4CwNsdIhJ3MA0GCSqGSIb3DQEBBQUAMEYxDzANBgNVBAMMBkVuZ2luZTERMA8GA1UECwwIU2VydmljZXMxEzARBgNVBAoMCk9wZW5Db25leHQxCzAJBgNVBAYTAk5MMB4XDTE0MDUxMjEzMjIxNloXDTI0MDUxMTEzMjIxNlowRjEPMA0GA1UEAwwGRW5naW5lMREwDwYDVQQLDAhTZXJ2aWNlczETMBEGA1UECgwKT3BlbkNvbmV4dDELMAkGA1UEBhMCTkwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2aQ9OYsAASWR/aN5NB2mQqFsBc13uN0nSbjkk1Um8VouGo7OmSM0eiur5my8UvPYth1DXQM6u2wiFq19RtfVZWJOmrzAfVHc9VRj9Xj4T+MVpR4bDWctvxVT1OPm9L23KQKvaqDmUo7uSPsBD36EIH7dFOBydDtSXfZTW0ien+lZr6C4nPuxzDbHJ+Jlo2brieimUBQNetX/ettnAglJ9536sJDkhsa120mkYPhVnvepbOtxPyU5ZDUpDNmMQR2/SORCBJcfvLSVZ4It4O67l6/EJnkFRLerIqOpk/W8jY3USQaLM2WM7sWBGxEFKDVcTFgrOH50Z94K2M/KweY2bAgMBAAGjUDBOMB0GA1UdDgQWBBSewI9OzfzbIxnl6XMkaQkYY1hHPjAfBgNVHSMEGDAWgBSewI9OzfzbIxnl6XMkaQkYY1hHPjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAFfYPZbsYPHz4ypV/aO59do7CtHPnAMWr0NcQt4h9IW8gjihaNHt12V30QtHrVaXejXybB/LaGbPPyA64+l/SeC7ksrRxlitCwFqnws6ISXJaYU0iEFHGUD/cAj1iGloIsOm5IOdb3sdG/SsBv49G8es2wG0rDd0/s2fBVvXd4qUoXzKJAjYk1MFQxnGHomlt67SBrr2QLh+m2VHg+mkdi6yrdm9B9ylF8V55Vl82pPZXxphIRgqdos5YWeALS7dr5dSw9s5smFBxyy8IfCQMxagfNC59w22w2ULC/J7au/oP8ylusuxncxizdR/+5UazzAlOWtkjzaABzzBWM4hEK"; + } }