2525use SimpleSAML \Store \StoreFactory ;
2626use SimpleSAML \Utils \HTTP ;
2727use SimpleSAML \XMLSecurity \Alg \Signature \SignatureAlgorithmFactory ;
28- use SimpleSAML \XMLSecurity \TestUtils \ PEMCertificatesMock ;
28+ use SimpleSAML \XMLSecurity \Key \ PublicKey ;
2929
3030use function array_key_exists ;
3131use function base64_decode ;
@@ -199,15 +199,7 @@ public function receive(ServerRequestInterface $request): AbstractMessage
199199 return $ samlResponse ;
200200 }
201201
202- $ container = ContainerSingleton::getInstance ();
203- $ blacklist = $ container ->getBlacklistedEncryptionAlgorithms ();
204- $ verifier = (new SignatureAlgorithmFactory ($ blacklist ))->getAlgorithm (
205- $ samlResponse ->getSignature ()->getSignedInfo ()->getSignatureMethod ()->getAlgorithm (),
206- // TODO: Need to use the key from the metadata
207- PEMCertificatesMock::getPublicKey (PEMCertificatesMock::SELFSIGNED_PUBLIC_KEY ),
208- );
209-
210- return $ samlResponse ->verify ($ verifier );
202+ return $ this ->verifyMessageSignature ($ samlResponse , $ idpMetadata );
211203 }
212204
213205
@@ -218,4 +210,139 @@ public function setSPMetadata(Configuration $sp): void
218210 {
219211 $ this ->spMetadata = $ sp ;
220212 }
213+
214+
215+ /**
216+ * Verify the signature on a signed SAML message using IdP metadata keys.
217+ *
218+ * Returns the verified message instance.
219+ *
220+ * @throws \Exception When metadata has no signing keys, or when verification fails.
221+ */
222+ private function verifyMessageSignature (AbstractMessage $ message , Configuration $ idpMetadata ): AbstractMessage
223+ {
224+ $ container = ContainerSingleton::getInstance ();
225+ $ blacklist = $ container ->getBlacklistedEncryptionAlgorithms ();
226+
227+ // getPublicKeys(..., $required = true) throws if no signing cert/key material is present in metadata,
228+ // so $keys is guaranteed non-empty here (no additional empty($keys) guard needed).
229+ $ keys = $ idpMetadata ->getPublicKeys ('signing ' , true );
230+
231+ $ signatureMethod = $ message
232+ ->getSignature ()
233+ ->getSignedInfo ()
234+ ->getSignatureMethod ()
235+ ->getAlgorithm ()
236+ ->getValue ();
237+
238+ $ factory = new SignatureAlgorithmFactory ($ blacklist );
239+
240+ $ lastException = null ;
241+ foreach ($ keys as $ k ) {
242+ if (($ k ['type ' ] ?? null ) !== 'X509Certificate ' ) {
243+ continue ;
244+ }
245+
246+ $ pemCert = "-----BEGIN CERTIFICATE----- \n" .
247+ chunk_split ($ k ['X509Certificate ' ], 64 ) .
248+ "-----END CERTIFICATE----- \n" ;
249+
250+ $ opensslKey = openssl_pkey_get_public ($ pemCert );
251+ if ($ opensslKey === false ) {
252+ $ lastException = new Exception ('Unable to extract public key from X509 certificate. ' );
253+ continue ;
254+ }
255+
256+ $ keyInfo = openssl_pkey_get_details ($ opensslKey );
257+ if ($ keyInfo === false || !isset ($ keyInfo ['key ' ]) || !is_string ($ keyInfo ['key ' ])) {
258+ $ lastException = new Exception ('Unable to get public key details from X509 certificate. ' );
259+ continue ;
260+ }
261+
262+ $ pemPublicKey = $ keyInfo ['key ' ];
263+
264+ $ file = Utils::getContainer ()->getTempDir () . '/ ' . sha1 ($ pemPublicKey ) . '.pem ' ;
265+ if (!file_exists ($ file )) {
266+ Utils::getContainer ()->writeFile ($ file , $ pemPublicKey );
267+ }
268+
269+ try {
270+ $ verifier = $ factory ->getAlgorithm ($ signatureMethod , PublicKey::fromFile ($ file ));
271+ return $ message ->verify ($ verifier );
272+ } catch (Exception $ e ) {
273+ $ lastException = $ e ;
274+ }
275+ }
276+
277+ throw $ lastException ?? new Exception ('Unable to verify message signature. ' );
278+ }
279+
280+
281+ /**
282+ * Verify the ArtifactResponse signature using IdP metadata keys.
283+ *
284+ * Returns the verified ArtifactResponse instance.
285+ *
286+ * @throws \Exception When unsigned, when metadata has no signing keys, or when verification fails.
287+ */
288+ private function verifyArtifactResponseSignature (
289+ ArtifactResponse $ artifactResponse ,
290+ Configuration $ idpMetadata ,
291+ ): ArtifactResponse {
292+ if ($ artifactResponse ->isSigned () !== true ) {
293+ throw new Exception ('ArtifactResponse must be signed. ' );
294+ }
295+
296+ // getPublicKeys(..., $required = true) throws if no signing cert/key material is present in metadata,
297+ // so $keys is guaranteed non-empty here (no additional empty($keys) guard needed).
298+ $ keys = $ idpMetadata ->getPublicKeys ('signing ' , true );
299+
300+ $ signatureMethod = $ artifactResponse
301+ ->getSignature ()
302+ ->getSignedInfo ()
303+ ->getSignatureMethod ()
304+ ->getAlgorithm ()
305+ ->getValue ();
306+
307+ $ factory = new SignatureAlgorithmFactory ();
308+
309+ $ lastException = null ;
310+ foreach ($ keys as $ k ) {
311+ if (($ k ['type ' ] ?? null ) !== 'X509Certificate ' ) {
312+ continue ;
313+ }
314+
315+ $ pemCert = "-----BEGIN CERTIFICATE----- \n" .
316+ chunk_split ($ k ['X509Certificate ' ], 64 ) .
317+ "-----END CERTIFICATE----- \n" ;
318+
319+ $ opensslKey = openssl_pkey_get_public ($ pemCert );
320+ if ($ opensslKey === false ) {
321+ $ lastException = new Exception ('Unable to extract public key from X509 certificate. ' );
322+ continue ;
323+ }
324+
325+ $ keyInfo = openssl_pkey_get_details ($ opensslKey );
326+ if ($ keyInfo === false || !isset ($ keyInfo ['key ' ]) || !is_string ($ keyInfo ['key ' ])) {
327+ $ lastException = new Exception ('Unable to get public key details from X509 certificate. ' );
328+ continue ;
329+ }
330+
331+ $ pemPublicKey = $ keyInfo ['key ' ];
332+
333+ $ file = Utils::getContainer ()->getTempDir () . '/ ' . sha1 ($ pemPublicKey ) . '.pem ' ;
334+ if (!file_exists ($ file )) {
335+ Utils::getContainer ()->writeFile ($ file , $ pemPublicKey );
336+ }
337+
338+ try {
339+ $ verifier = $ factory ->getAlgorithm ($ signatureMethod , PublicKey::fromFile ($ file ));
340+ return $ artifactResponse ->verify ($ verifier );
341+ } catch (Exception $ e ) {
342+ $ lastException = $ e ;
343+ }
344+ }
345+
346+ throw $ lastException ?? new Exception ('Unable to verify ArtifactResponse signature. ' );
347+ }
221348}
0 commit comments