Skip to content

Commit 352dd16

Browse files
ioigoumetvdijen
authored andcommitted
HTTP-Artifact: verify signatures using IdP metadata signing keys (fix TODO in verifier setup) (#419)
1 parent 80e5347 commit 352dd16

3 files changed

Lines changed: 428 additions & 11 deletions

File tree

.github/workflows/php.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ jobs:
156156
echo "Could not read extra.branch-alias.dev-master from composer.json" >&2
157157
exit 1
158158
fi
159-
echo "COMPOSER_ROOT_VERSION=$ROOT_VERSION" >> "$env:GITHUB_ENV"
159+
echo "COMPOSER_ROOT_VERSION=$ROOT_VERSION" >> "$GITHUB_ENV"
160160
161161
- name: Cache composer dependencies
162162
uses: actions/cache@v6

src/Binding/HTTPArtifact.php

Lines changed: 137 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
use SimpleSAML\Store\StoreFactory;
2626
use SimpleSAML\Utils\HTTP;
2727
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
28-
use SimpleSAML\XMLSecurity\TestUtils\PEMCertificatesMock;
28+
use SimpleSAML\XMLSecurity\Key\PublicKey;
2929

3030
use function array_key_exists;
3131
use 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

Comments
 (0)