2626use Optios \Payconiq \HeaderChecker \PayconiqJtiChecker ;
2727use Optios \Payconiq \HeaderChecker \PayconiqPathChecker ;
2828use Optios \Payconiq \HeaderChecker \PayconiqSubChecker ;
29+ use phpseclib3 \Crypt \EC \Formats \Signature \ASN1 as EcdsaAsn1 ;
30+ use phpseclib3 \Crypt \EC \Formats \Signature \IEEE as EcdsaP1363 ;
2931use Symfony \Component \Cache \Adapter \AdapterInterface ;
3032use Symfony \Component \Cache \Adapter \FilesystemAdapter ;
3133use Symfony \Contracts \Cache \ItemInterface ;
@@ -100,6 +102,7 @@ private function getCertificatesUrl(): string
100102 public function isValid (string $ token , ?string $ payload = null , ?int $ signature = 0 ): bool
101103 {
102104 try {
105+ $ token = self ::normalizeEcdsaSigIfNeeded ($ token , 32 );
103106 $ this ->jwsLoader ->loadAndVerifyWithKeySet ($ token , $ this ->getJWKSet (), $ signature , $ payload );
104107 } catch (\Throwable $ e ) {
105108 return false ;
@@ -114,6 +117,7 @@ public function isValid(string $token, ?string $payload = null, ?int $signature
114117 public function loadAndVerifyJWS (string $ token , ?string $ payload = null , ?int $ signature = 0 ): JWS
115118 {
116119 try {
120+ $ token = self ::normalizeEcdsaSigIfNeeded ($ token , 32 );
117121 return $ this ->jwsLoader ->loadAndVerifyWithKeySet ($ token , $ this ->getJWKSet (), $ signature , $ payload );
118122 } catch (\Throwable $ e ) {
119123 throw new PayconiqCallbackSignatureVerificationException (
@@ -156,6 +160,35 @@ private function getJWKSet(): JWKSet
156160 }
157161 }
158162
163+ /**
164+ * If the compact JWS uses a DER-encoded ECDSA signature, convert it to JOSE raw (r||s).
165+ * $partLen: 32 for ES256, 48 for ES384, 66 for ES512.
166+ */
167+ private static function normalizeEcdsaSigIfNeeded (string $ compactJws , int $ partLen = 32 ): string
168+ {
169+ [$ h , $ p , $ sB64u ] = explode ('. ' , $ compactJws , 3 ) + [null , null , null ];
170+ if ($ sB64u === null || $ sB64u === '' ) return $ compactJws ;
171+
172+ // base64url decode signature
173+ $ pad = (4 - strlen ($ sB64u ) % 4 ) % 4 ;
174+ $ sig = base64_decode (strtr ($ sB64u , '-_ ' , '+/ ' ) . str_repeat ('= ' , $ pad ), true );
175+ if ($ sig === false ) return $ compactJws ;
176+
177+ // already raw r||s of expected length? nothing to do.
178+ if (strlen ($ sig ) === 2 * $ partLen ) return $ compactJws ;
179+
180+ // Try DER → raw using phpseclib helpers
181+ try {
182+ $ rs = EcdsaAsn1::load ($ sig ); // ['r'=>BigInteger,'s'=>BigInteger]
183+ $ raw = EcdsaP1363::save ($ rs ['r ' ], $ rs ['s ' ], null , $ partLen ); // fixed-length P-1363
184+ $ sB64u = rtrim (strtr (base64_encode ($ raw ), '+/ ' , '-_ ' ), '= ' );
185+ return "$ h. $ p. $ sB64u " ;
186+ } catch (\Throwable ) {
187+ // Not DER / not parseable — leave as-is and let the verifier decide
188+ return $ compactJws ;
189+ }
190+ }
191+
159192 private function initializeJwsLoader (string $ paymentProfileId ): JWSLoader
160193 {
161194 return new JWSLoader (
0 commit comments