2424use SimpleSAML \SAML2 \XML \samlp \ArtifactResponse ;
2525use SimpleSAML \Store \StoreFactory ;
2626use SimpleSAML \Utils \HTTP ;
27- use SimpleSAML \XMLSecurity \XMLSecurityKey ;
27+ use SimpleSAML \XMLSecurity \Alg \Signature \SignatureAlgorithmFactory ;
28+ use SimpleSAML \XMLSecurity \Key \PublicKey ;
2829
2930use function array_key_exists ;
3031use function base64_decode ;
@@ -204,7 +205,11 @@ public function receive(ServerRequestInterface $request): AbstractMessage
204205 $ this ->setRelayState ($ query ['RelayState ' ]);
205206 }
206207
207- return $ samlResponse ;
208+ if (!$ samlResponse ->isSigned ()) {
209+ return $ samlResponse ;
210+ }
211+
212+ return $ this ->verifyMessageSignature ($ samlResponse , $ idpMetadata );
208213 }
209214
210215
@@ -220,15 +225,136 @@ public function setSPMetadata(Configuration $sp): void
220225
221226
222227 /**
223- * A validator which returns true if the ArtifactResponse was signed with the given key
228+ * Verify the signature on a signed SAML message using IdP metadata keys.
229+ *
230+ * Returns the verified message instance.
231+ *
232+ * @throws \Exception When metadata has no signing keys, or when verification fails.
233+ */
234+ private function verifyMessageSignature (AbstractMessage $ message , Configuration $ idpMetadata ): AbstractMessage
235+ {
236+ $ container = ContainerSingleton::getInstance ();
237+ $ blacklist = $ container ->getBlacklistedEncryptionAlgorithms ();
238+
239+ // getPublicKeys(..., $required = true) throws if no signing cert/key material is present in metadata,
240+ // so $keys is guaranteed non-empty here (no additional empty($keys) guard needed).
241+ $ keys = $ idpMetadata ->getPublicKeys ('signing ' , true );
242+
243+ $ signatureMethod = $ message
244+ ->getSignature ()
245+ ->getSignedInfo ()
246+ ->getSignatureMethod ()
247+ ->getAlgorithm ()
248+ ->getValue ();
249+
250+ $ factory = new SignatureAlgorithmFactory ($ blacklist );
251+
252+ $ lastException = null ;
253+ foreach ($ keys as $ k ) {
254+ if (($ k ['type ' ] ?? null ) !== 'X509Certificate ' ) {
255+ continue ;
256+ }
257+
258+ $ pemCert = "-----BEGIN CERTIFICATE----- \n" .
259+ chunk_split ($ k ['X509Certificate ' ], 64 ) .
260+ "-----END CERTIFICATE----- \n" ;
261+
262+ $ opensslKey = openssl_pkey_get_public ($ pemCert );
263+ if ($ opensslKey === false ) {
264+ $ lastException = new Exception ('Unable to extract public key from X509 certificate. ' );
265+ continue ;
266+ }
267+
268+ $ keyInfo = openssl_pkey_get_details ($ opensslKey );
269+ if ($ keyInfo === false || !isset ($ keyInfo ['key ' ]) || !is_string ($ keyInfo ['key ' ])) {
270+ $ lastException = new Exception ('Unable to get public key details from X509 certificate. ' );
271+ continue ;
272+ }
273+
274+ $ pemPublicKey = $ keyInfo ['key ' ];
275+
276+ $ file = Utils::getContainer ()->getTempDir () . '/ ' . sha1 ($ pemPublicKey ) . '.pem ' ;
277+ if (!file_exists ($ file )) {
278+ Utils::getContainer ()->writeFile ($ file , $ pemPublicKey );
279+ }
280+
281+ try {
282+ $ verifier = $ factory ->getAlgorithm ($ signatureMethod , PublicKey::fromFile ($ file ));
283+ return $ message ->verify ($ verifier );
284+ } catch (Exception $ e ) {
285+ $ lastException = $ e ;
286+ }
287+ }
288+
289+ throw $ lastException ?? new Exception ('Unable to verify message signature. ' );
290+ }
291+
292+
293+ /**
294+ * Verify the ArtifactResponse signature using IdP metadata keys.
224295 *
225296 * @param \SimpleSAML\SAML2\XML\samlp\ArtifactResponse $message
226297 * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key
227298 * @return bool
228299 */
229- public static function validateSignature (ArtifactResponse $ message , XMLSecurityKey $ key ): bool
230- {
231- // @todo verify if this works and/or needs to do anything more. Ref. HTTPRedirect binding
232- return $ message ->validate ($ key );
300+ private function verifyArtifactResponseSignature (
301+ ArtifactResponse $ artifactResponse ,
302+ Configuration $ idpMetadata ,
303+ ): ArtifactResponse {
304+ if ($ artifactResponse ->isSigned () !== true ) {
305+ throw new Exception ('ArtifactResponse must be signed. ' );
306+ }
307+
308+ // getPublicKeys(..., $required = true) throws if no signing cert/key material is present in metadata,
309+ // so $keys is guaranteed non-empty here (no additional empty($keys) guard needed).
310+ $ keys = $ idpMetadata ->getPublicKeys ('signing ' , true );
311+
312+ $ signatureMethod = $ artifactResponse
313+ ->getSignature ()
314+ ->getSignedInfo ()
315+ ->getSignatureMethod ()
316+ ->getAlgorithm ()
317+ ->getValue ();
318+
319+ $ factory = new SignatureAlgorithmFactory ();
320+
321+ $ lastException = null ;
322+ foreach ($ keys as $ k ) {
323+ if (($ k ['type ' ] ?? null ) !== 'X509Certificate ' ) {
324+ continue ;
325+ }
326+
327+ $ pemCert = "-----BEGIN CERTIFICATE----- \n" .
328+ chunk_split ($ k ['X509Certificate ' ], 64 ) .
329+ "-----END CERTIFICATE----- \n" ;
330+
331+ $ opensslKey = openssl_pkey_get_public ($ pemCert );
332+ if ($ opensslKey === false ) {
333+ $ lastException = new Exception ('Unable to extract public key from X509 certificate. ' );
334+ continue ;
335+ }
336+
337+ $ keyInfo = openssl_pkey_get_details ($ opensslKey );
338+ if ($ keyInfo === false || !isset ($ keyInfo ['key ' ]) || !is_string ($ keyInfo ['key ' ])) {
339+ $ lastException = new Exception ('Unable to get public key details from X509 certificate. ' );
340+ continue ;
341+ }
342+
343+ $ pemPublicKey = $ keyInfo ['key ' ];
344+
345+ $ file = Utils::getContainer ()->getTempDir () . '/ ' . sha1 ($ pemPublicKey ) . '.pem ' ;
346+ if (!file_exists ($ file )) {
347+ Utils::getContainer ()->writeFile ($ file , $ pemPublicKey );
348+ }
349+
350+ try {
351+ $ verifier = $ factory ->getAlgorithm ($ signatureMethod , PublicKey::fromFile ($ file ));
352+ return $ artifactResponse ->verify ($ verifier );
353+ } catch (Exception $ e ) {
354+ $ lastException = $ e ;
355+ }
356+ }
357+
358+ throw $ lastException ?? new Exception ('Unable to verify ArtifactResponse signature. ' );
233359 }
234360}
0 commit comments