1515use SimpleSAML \Module \saml \Message as MSG ;
1616use SimpleSAML \SAML2 \Binding ;
1717use SimpleSAML \SAML2 \Binding \RelayStateTrait ;
18+ use SimpleSAML \SAML2 \Compat \ContainerSingleton ;
1819use SimpleSAML \SAML2 \SOAPClient ;
1920use SimpleSAML \SAML2 \Utils ;
2021use SimpleSAML \SAML2 \XML \saml \Issuer ;
2425use SimpleSAML \SAML2 \XML \samlp \ArtifactResponse ;
2526use SimpleSAML \Store \StoreFactory ;
2627use SimpleSAML \Utils \HTTP ;
27- use SimpleSAML \XMLSecurity \XMLSecurityKey ;
28+ use SimpleSAML \XMLSecurity \Alg \Signature \SignatureAlgorithmFactory ;
29+ use SimpleSAML \XMLSecurity \Key \PublicKey ;
2830
2931use function array_key_exists ;
3032use function base64_decode ;
@@ -204,7 +206,11 @@ public function receive(ServerRequestInterface $request): AbstractMessage
204206 $ this ->setRelayState ($ query ['RelayState ' ]);
205207 }
206208
207- return $ samlResponse ;
209+ if (!$ samlResponse ->isSigned ()) {
210+ return $ samlResponse ;
211+ }
212+
213+ return $ this ->verifyMessageSignature ($ samlResponse , $ idpMetadata );
208214 }
209215
210216
@@ -220,15 +226,139 @@ public function setSPMetadata(Configuration $sp): void
220226
221227
222228 /**
223- * A validator which returns true if the ArtifactResponse was signed with the given key
229+ * Verify the signature on a signed SAML message using IdP metadata keys.
230+ *
231+ * Returns the verified message instance.
232+ *
233+ * @throws \Exception When metadata has no signing keys, or when verification fails.
234+ */
235+ private function verifyMessageSignature (AbstractMessage $ message , Configuration $ idpMetadata ): AbstractMessage
236+ {
237+ $ container = ContainerSingleton::getInstance ();
238+ $ blacklist = $ container ->getBlacklistedEncryptionAlgorithms ();
239+
240+ // getPublicKeys(..., $required = true) throws if no signing cert/key material is present in metadata,
241+ // so $keys is guaranteed non-empty here (no additional empty($keys) guard needed).
242+ $ keys = $ idpMetadata ->getPublicKeys ('signing ' , true );
243+
244+ $ signatureMethod = $ message
245+ ->getSignature ()
246+ ->getSignedInfo ()
247+ ->getSignatureMethod ()
248+ ->getAlgorithm ()
249+ ->getValue ();
250+
251+ $ factory = new SignatureAlgorithmFactory ($ blacklist );
252+
253+ $ lastException = null ;
254+ foreach ($ keys as $ k ) {
255+ if (($ k ['type ' ] ?? null ) !== 'X509Certificate ' ) {
256+ continue ;
257+ }
258+
259+ $ pemCert = "-----BEGIN CERTIFICATE----- \n" .
260+ chunk_split ($ k ['X509Certificate ' ], 64 ) .
261+ "-----END CERTIFICATE----- \n" ;
262+
263+ $ opensslKey = openssl_pkey_get_public ($ pemCert );
264+ if ($ opensslKey === false ) {
265+ $ lastException = new Exception ('Unable to extract public key from X509 certificate. ' );
266+ continue ;
267+ }
268+
269+ $ keyInfo = openssl_pkey_get_details ($ opensslKey );
270+ if ($ keyInfo === false || !isset ($ keyInfo ['key ' ]) || !is_string ($ keyInfo ['key ' ])) {
271+ $ lastException = new Exception ('Unable to get public key details from X509 certificate. ' );
272+ continue ;
273+ }
274+
275+ $ pemPublicKey = $ keyInfo ['key ' ];
276+
277+ $ file = Utils::getContainer ()->getTempDir () . '/ ' . sha1 ($ pemPublicKey ) . '.pem ' ;
278+ if (!file_exists ($ file )) {
279+ Utils::getContainer ()->writeFile ($ file , $ pemPublicKey );
280+ }
281+
282+ try {
283+ $ verifier = $ factory ->getAlgorithm ($ signatureMethod , PublicKey::fromFile ($ file ));
284+ return $ message ->verify ($ verifier );
285+ } catch (Exception $ e ) {
286+ $ lastException = $ e ;
287+ }
288+ }
289+
290+ throw $ lastException ?? new Exception ('Unable to verify message signature. ' );
291+ }
292+
293+
294+ /**
295+ * Verify the ArtifactResponse signature using IdP metadata keys.
224296 *
225297 * @param \SimpleSAML\SAML2\XML\samlp\ArtifactResponse $message
226298 * @param \SimpleSAML\XMLSecurity\XMLSecurityKey $key
227- * @return bool
299+ *
300+ * Returns the verified ArtifactResponse instance.
301+ *
302+ * @throws \Exception When unsigned, when metadata has no signing keys, or when verification fails.
228303 */
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 );
304+ private function verifyArtifactResponseSignature (
305+ ArtifactResponse $ artifactResponse ,
306+ Configuration $ idpMetadata ,
307+ ): ArtifactResponse {
308+ if ($ artifactResponse ->isSigned () !== true ) {
309+ throw new Exception ('ArtifactResponse must be signed. ' );
310+ }
311+
312+ // getPublicKeys(..., $required = true) throws if no signing cert/key material is present in metadata,
313+ // so $keys is guaranteed non-empty here (no additional empty($keys) guard needed).
314+ $ keys = $ idpMetadata ->getPublicKeys ('signing ' , true );
315+
316+ $ signatureMethod = $ artifactResponse
317+ ->getSignature ()
318+ ->getSignedInfo ()
319+ ->getSignatureMethod ()
320+ ->getAlgorithm ()
321+ ->getValue ();
322+
323+ $ factory = new SignatureAlgorithmFactory ();
324+
325+ $ lastException = null ;
326+ foreach ($ keys as $ k ) {
327+ if (($ k ['type ' ] ?? null ) !== 'X509Certificate ' ) {
328+ continue ;
329+ }
330+
331+ $ pemCert = "-----BEGIN CERTIFICATE----- \n" .
332+ chunk_split ($ k ['X509Certificate ' ], 64 ) .
333+ "-----END CERTIFICATE----- \n" ;
334+
335+ $ opensslKey = openssl_pkey_get_public ($ pemCert );
336+ if ($ opensslKey === false ) {
337+ $ lastException = new Exception ('Unable to extract public key from X509 certificate. ' );
338+ continue ;
339+ }
340+
341+ $ keyInfo = openssl_pkey_get_details ($ opensslKey );
342+ if ($ keyInfo === false || !isset ($ keyInfo ['key ' ]) || !is_string ($ keyInfo ['key ' ])) {
343+ $ lastException = new Exception ('Unable to get public key details from X509 certificate. ' );
344+ continue ;
345+ }
346+
347+ $ pemPublicKey = $ keyInfo ['key ' ];
348+
349+ $ file = Utils::getContainer ()->getTempDir () . '/ ' . sha1 ($ pemPublicKey ) . '.pem ' ;
350+ if (!file_exists ($ file )) {
351+ Utils::getContainer ()->writeFile ($ file , $ pemPublicKey );
352+ }
353+
354+ try {
355+ $ verifier = $ factory ->getAlgorithm ($ signatureMethod , PublicKey::fromFile ($ file ));
356+ return $ artifactResponse ->verify ($ verifier );
357+ } catch (Exception $ e ) {
358+ $ lastException = $ e ;
359+ }
360+ }
361+
362+ throw $ lastException ?? new Exception ('Unable to verify ArtifactResponse signature. ' );
233363 }
234364}
0 commit comments