99use Nyholm \Psr7 \Response ;
1010use Psr \Http \Message \ResponseInterface ;
1111use Psr \Http \Message \ServerRequestInterface ;
12- use RobRichards \XMLSecLibs \XMLSecurityKey ;
1312use SimpleSAML \Configuration ;
1413use SimpleSAML \Metadata \MetaDataStorageHandler ;
1514use SimpleSAML \Module \saml \Message as MSG ;
2423use SimpleSAML \SAML2 \XML \samlp \ArtifactResponse ;
2524use SimpleSAML \Store \StoreFactory ;
2625use SimpleSAML \Utils \HTTP ;
26+ use SimpleSAML \XMLSchema \Type \AnyURIValue ;
27+ use SimpleSAML \XMLSecurity \Alg \Signature \SignatureAlgorithmFactory ;
28+ use SimpleSAML \XMLSecurity \Key \PublicKey ;
2729
2830use function array_key_exists ;
2931use function base64_decode ;
3032use function base64_encode ;
3133use function bin2hex ;
34+ use function chunk_split ;
35+ use function file_exists ;
3236use function hexdec ;
37+ use function openssl_pkey_get_details ;
38+ use function openssl_pkey_get_public ;
3339use function openssl_random_pseudo_bytes ;
3440use function pack ;
3541use function sha1 ;
@@ -74,7 +80,9 @@ public function getRedirectURL(AbstractMessage $message): string
7480 if ($ issuer === null ) {
7581 throw new Exception ('Cannot get redirect URL, no Issuer set in the message. ' );
7682 }
77- $ artifact = base64_encode ("\x00\x04\x00\x00" . sha1 ($ issuer ->getContent (), true ) . $ generatedId );
83+ $ artifact = base64_encode (
84+ "\x00\x04\x00\x00" . sha1 ((string )$ issuer ->getContent (), true ) . $ generatedId ,
85+ );
7886 $ artifactData = $ message ->toXML ();
7987 $ artifactDataString = $ artifactData ->ownerDocument ?->saveXML($ artifactData );
8088
@@ -94,7 +102,7 @@ public function getRedirectURL(AbstractMessage $message): string
94102 }
95103
96104 $ httpUtils = new HTTP ();
97- return $ httpUtils ->addURLparameters ($ destination , $ params );
105+ return $ httpUtils ->addURLparameters (( string ) $ destination , $ params );
98106 }
99107
100108
@@ -180,15 +188,14 @@ public function receive(ServerRequestInterface $request): AbstractMessage
180188 throw new Exception ('Received error from ArtifactResolutionService. ' );
181189 }
182190
191+ $ artifactResponse = $ this ->verifyArtifactResponseSignature ($ artifactResponse , $ idpMetadata );
192+
183193 $ samlResponse = $ artifactResponse ->getMessage ();
184194 if ($ samlResponse === null ) {
185195 /* Empty ArtifactResponse - possibly because of Artifact replay? */
186-
187196 throw new Exception ('Empty ArtifactResponse received, maybe a replay? ' );
188197 }
189198
190- $ samlResponse ->addValidator ([get_class ($ this ), 'validateSignature ' ], $ artifactResponse );
191-
192199 $ query = $ request ->getQueryParams ();
193200 if (isset ($ query ['RelayState ' ])) {
194201 $ this ->setRelayState ($ query ['RelayState ' ]);
@@ -208,14 +215,67 @@ public function setSPMetadata(Configuration $sp): void
208215
209216
210217 /**
211- * A validator which returns true if the ArtifactResponse was signed with the given key
218+ * Verify the ArtifactResponse signature using IdP metadata keys.
212219 *
213- * @param \SimpleSAML\SAML2\XML\samlp\ArtifactResponse $message
214- * @param \RobRichards\XMLSecLibs\XMLSecurityKey $key
220+ * Returns the verified ArtifactResponse instance.
221+ *
222+ * @throws \Exception When unsigned, when metadata has no signing keys, or when verification fails.
215223 */
216- public static function validateSignature (ArtifactResponse $ message , XMLSecurityKey $ key ): bool
217- {
218- // @todo verify if this works and/or needs to do anything more. Ref. HTTPRedirect binding
219- return $ message ->validate ($ key );
224+ private function verifyArtifactResponseSignature (
225+ ArtifactResponse $ artifactResponse ,
226+ Configuration $ idpMetadata ,
227+ ): ArtifactResponse {
228+ if ($ artifactResponse ->isSigned () !== true ) {
229+ throw new Exception ('ArtifactResponse must be signed. ' );
230+ }
231+
232+ $ keys = $ idpMetadata ->getPublicKeys ('signing ' , true );
233+ if (empty ($ keys )) {
234+ throw new Exception ('No signing keys found in IdP metadata. ' );
235+ }
236+
237+ $ alg = $ artifactResponse ->getSignature ()->getSignedInfo ()->getSignatureMethod ()->getAlgorithm ();
238+ $ signatureMethod = $ alg instanceof AnyURIValue ? $ alg ->getValue () : (string ) $ alg ;
239+
240+ $ factory = new SignatureAlgorithmFactory ();
241+
242+ $ lastException = null ;
243+ foreach ($ keys as $ k ) {
244+ if (($ k ['type ' ] ?? null ) !== 'X509Certificate ' ) {
245+ continue ;
246+ }
247+
248+ $ pemCert = "-----BEGIN CERTIFICATE----- \n" .
249+ chunk_split ($ k ['X509Certificate ' ], 64 ) .
250+ "-----END CERTIFICATE----- \n" ;
251+
252+ $ opensslKey = openssl_pkey_get_public ($ pemCert );
253+ if ($ opensslKey === false ) {
254+ $ lastException = new Exception ('Unable to extract public key from X509 certificate. ' );
255+ continue ;
256+ }
257+
258+ $ keyInfo = openssl_pkey_get_details ($ opensslKey );
259+ if ($ keyInfo === false || !isset ($ keyInfo ['key ' ]) || !is_string ($ keyInfo ['key ' ])) {
260+ $ lastException = new Exception ('Unable to get public key details from X509 certificate. ' );
261+ continue ;
262+ }
263+
264+ $ pemPublicKey = $ keyInfo ['key ' ];
265+
266+ $ file = Utils::getContainer ()->getTempDir () . '/ ' . sha1 ($ pemPublicKey ) . '.pem ' ;
267+ if (!file_exists ($ file )) {
268+ Utils::getContainer ()->writeFile ($ file , $ pemPublicKey );
269+ }
270+
271+ try {
272+ $ verifier = $ factory ->getAlgorithm ($ signatureMethod , PublicKey::fromFile ($ file ));
273+ return $ artifactResponse ->verify ($ verifier );
274+ } catch (\Exception $ e ) {
275+ $ lastException = $ e ;
276+ }
277+ }
278+
279+ throw $ lastException ?? new Exception ('Unable to verify ArtifactResponse signature. ' );
220280 }
221281}
0 commit comments