diff --git a/src/SAML2/Entity/ServiceProvider.php b/src/SAML2/Entity/ServiceProvider.php index 21d9478cb..a87dced19 100644 --- a/src/SAML2/Entity/ServiceProvider.php +++ b/src/SAML2/Entity/ServiceProvider.php @@ -29,6 +29,8 @@ }; use SimpleSAML\SAML2\XML\samlp\Response; use SimpleSAML\XMLSecurity\Alg\Encryption\EncryptionAlgorithmFactory; +use SimpleSAML\XMLSecurity\Alg\KeyTransport\KeyTransportAlgorithmFactory; +use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException; use SimpleSAML\XMLSecurity\XML\{ EncryptableElementInterface, @@ -49,6 +51,9 @@ final class ServiceProvider protected ?StateProviderInterface $stateProvider = null; protected ?StorageProviderInterface $storageProvider = null; protected ?Metadata\IdentityProvider $idpMetadata = null; + protected SignatureAlgorithmFactory $signatureAlgorithmFactory; + protected EncryptionAlgorithmFactory $encryptionAlgorithmFactory; + protected KeyTransportAlgorithmFactory $keyTransportAlgorithmFactory; /** @@ -75,6 +80,9 @@ public function __construct( // Use with caution - will leave any form of constraint validation up to the implementer protected readonly bool $bypassConstraintValidation = false, ) { + $this->signatureAlgorithmFactory = new SignatureAlgorithmFactory(); + $this->encryptionAlgorithmFactory = new EncryptionAlgorithmFactory(); + $this->keyTransportAlgorithmFactory = new KeyTransportAlgorithmFactory(); } @@ -205,6 +213,10 @@ public function receiveResponse(ServerRequestInterface $request): Response ); $responseValidator->validate($verifiedResponse); + if ($this->encryptedAssertions === true) { + Assert::allIsInstanceOf($verifiedResponse->getAssertions(), EncryptedAssertion::class); + } + // Decrypt and verify assertions, then rebuild the response. $verifiedAssertions = $this->decryptAndVerifyAssertions($verifiedResponse->getAssertions()); $decryptedResponse = new Response( @@ -241,6 +253,8 @@ public function receiveResponse(ServerRequestInterface $request): Response */ protected function decryptAndVerifyAssertions(array $unverifiedAssertions): array { + $wantAssertionsSigned = $this->spMetadata->getWantAssertionsSigned(); + /** * See paragraph 6.2 of the SAML 2.0 core specifications for the applicable processing rules * @@ -254,6 +268,11 @@ protected function decryptAndVerifyAssertions(array $unverifiedAssertions): arra ? $this->decryptElement($assertion) : $assertion; + // Verify that the request is signed, if we require this by configuration + if ($wantAssertionsSigned === true) { + Assert::true($decryptedAssertion->isSigned(), RuntimeException::class); + } + // Verify the signature on the assertions (if any) $verifiedAssertion = $this->verifyElementSignature($decryptedAssertion); @@ -262,7 +281,12 @@ protected function decryptAndVerifyAssertions(array $unverifiedAssertions): arra if ($nameID instanceof EncryptedID) { $decryptedNameID = $this->decryptElement($nameID); - $subject = new Subject($decryptedNameID, $verifiedAssertion->getSubjectConfirmation()); + // Anything we can't decrypt, we leave up for the application to deal with + try { + $subject = new Subject($decryptedNameID, $verifiedAssertion->getSubjectConfirmation()); + } catch (RuntimeException) { + $subject = $verifiedAssertion->getSubject(); + } } else { $subject = $verifiedAssertion->getSubject(); } @@ -274,7 +298,12 @@ protected function decryptAndVerifyAssertions(array $unverifiedAssertions): arra $attributes = $statement->getAttributes(); if ($statement->hasEncryptedAttributes()) { foreach ($statement->getEncryptedAttributes() as $encryptedAttribute) { - $attributes[] = $this->decryptElement($encryptedAttribute); + // Anything we can't decrypt, we leave up for the application to deal with + try { + $attributes[] = $this->decryptElement($encryptedAttribute); + } catch (RuntimeException) { + $attributes[] = $encryptedAttribute; + } } } @@ -307,12 +336,22 @@ protected function decryptAndVerifyAssertions(array $unverifiedAssertions): arra */ protected function decryptElement(EncryptedElementInterface $element): EncryptableElementInterface { - $factory = $this->spMetadata->getEncryptionAlgorithmFactory(); + $factory = $this->encryptionAlgorithmFactory; - $encryptionAlgorithm = ($factory instanceof EncryptionAlgorithmFactory) - ? $element->getEncryptedData()->getEncryptionMethod() - : $element->getEncryptedKey()->getEncryptionMethod(); + // If the IDP has a pre-shared key, try decrypting with that + $preSharedKey = $this->idpMetadata->getPreSharedKey(); + if ($preSharedKey !== null) { + $encryptionAlgorithm = $element?->getEncryptedKey()?->getEncryptionMethod() ?? $this->preSharedKeyAlgorithm; + + $decryptor = $factory->getAlgorithm($encryptionAlgorithm, $preSharedKey); + try { + return $element->decrypt($decryptor); + } catch (Exception $e) { + // Continue to try decrypting with asymmetric keys. + } + } + $encryptionAlgorithm = $element->getEncryptedData()->getEncryptionMethod(); foreach ($this->spMetadata->getDecriptionKeys() as $decryptionKey) { $decryptor = $factory->getAlgorithm($encryptionAlgorithm, $decryptionKey); try { @@ -339,11 +378,10 @@ protected function decryptElement(EncryptedElementInterface $element): Encryptab */ protected function verifyElementSignature(SignedElementInterface $element): SignableElementInterface { - $factory = $this->spMetadata->getSignatureAlgorithmFactory(); $signatureAlgorithm = $element->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm(); foreach ($this->idpMetadata->getValidatingKeys() as $validatingKey) { - $verifier = $factory->getAlgorithm($signatureAlgorithm, $validatingKey); + $verifier = $this->signatureAlgorithmFactory->getAlgorithm($signatureAlgorithm, $validatingKey); try { return $element->verify($verifier); diff --git a/src/SAML2/Metadata/AbstractProvider.php b/src/SAML2/Metadata/AbstractProvider.php index e598a5920..f7a570d3e 100644 --- a/src/SAML2/Metadata/AbstractProvider.php +++ b/src/SAML2/Metadata/AbstractProvider.php @@ -8,8 +8,11 @@ use SimpleSAML\XMLSecurity\Alg\Encryption\EncryptionAlgorithmFactory; use SimpleSAML\XMLSecurity\Alg\KeyTransport\KeyTransportAlgorithmFactory; use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; +use SimpleSAML\XMLSecurity\Constants as C; use SimpleSAML\XMLSecurity\Key\{PrivateKey, PublicKey, SymmetricKey}; +use function array_keys; + /** * Class holding common configuration for SAML2 entities. * @@ -21,38 +24,23 @@ abstract class AbstractProvider */ protected function __construct( protected string $entityId, - protected EncryptionAlgorithmFactory|KeyTransportAlgorithmFactory|null $encryptionAlgorithmFactory, - protected SignatureAlgorithmFactory|null $signatureAlgorithmFactory, protected string $signatureAlgorithm, protected array $validatingKeys, - protected PrivateKey|null $signingKey, - protected PublicKey|SymmetricKey|null $encryptionKey, + protected ?PrivateKey $signingKey, + protected ?PublicKey $encryptionKey, protected array $decryptionKeys, + protected ?SymmetricKey $preSharedKey, + protected string $preSharedKeyAlgorithm, protected array $IDPList, ) { Assert::validURI($entityId); Assert::validURI($signatureAlgorithm); - Assert::allIsInstanceOfAny($decryptionKeys, [SymmetricKey::class, PrivateKey::class]); + Assert::oneOf($signatureAlgorithm, array_keys(C::$RSA_DIGESTS)); + Assert::allIsInstanceOf($decryptionKeys, PrivateKey::class); Assert::allIsInstanceOf($validatingKeys, PublicKey::class); Assert::allValidURI($IDPList); - } - - - /** - * Retrieve the SignatureAlgorithmFactory used for signing and verifying messages. - */ - public function getSignatureAlgorithmFactory(): ?SignatureAlgorithmFactory - { - return $this->signatureAlgorithmFactory; - } - - - /** - * Retrieve the EncryptionAlgorithmFactory used for encrypting and decrypting messages. - */ - public function getEncryptionAlgorithmFactory(): EncryptionAlgorithmFactory|KeyTransportAlgorithmFactory|null - { - return $this->encryptionAlgorithmFactory; + Assert::nullOrValidURI($preSharedKeyAlgorithm); + Assert::oneOf($preSharedKeyAlgorithm, array_keys(C::$BLOCK_CIPHER_ALGORITHMS)); } @@ -88,20 +76,31 @@ public function getValidatingKeys(): array /** - * Get the private key to use for signing messages. + * Get the public key to use for encrypting messages. * - * @return \SimpleSAML\XMLSecurity\Key\PublicKey|\SimpleSAML\XMLSecurity\Key\SymmetricKey|null + * @return \SimpleSAML\XMLSecurity\Key\PublicKey|null */ - public function getEncryptionKey(): PublicKey|SymmetricKey|null + public function getEncryptionKey(): ?PublicKey { return $this->encryptionKey; } + /** + * Get the symmetric key to use for encrypting/decrypting messages. + * + * @return \SimpleSAML\XMLSecurity\Key\SymmetricKey|null + */ + public function getPreSharedKey(): ?SymmetricKey + { + return $this->preSharedKey; + } + + /** * Get the decryption keys to decrypt the assertion with. * - * @return array<\SimpleSAML\XMLSecurity\Key\PrivateKey|\SimpleSAML\XMLSecurity\Key\SymmetricKey> + * @return array<\SimpleSAML\XMLSecurity\Key\PrivateKey> */ public function getDecryptionKeys(): array { diff --git a/src/SAML2/Metadata/IdentityProvider.php b/src/SAML2/Metadata/IdentityProvider.php index c25f9c3f3..654a38f99 100644 --- a/src/SAML2/Metadata/IdentityProvider.php +++ b/src/SAML2/Metadata/IdentityProvider.php @@ -21,24 +21,24 @@ class IdentityProvider extends AbstractProvider */ public function __construct( string $entityId, - EncryptionAlgorithmFactory|KeyTransportAlgorithmFactory|null $encryptionAlgorithmFactory = null, - SignatureAlgorithmFactory|null $signatureAlgorithmFactory = null, string $signatureAlgorithm = C::SIG_RSA_SHA256, array $validatingKeys = [], - PrivateKey|null $signingKey = null, - PublicKey|SymmetricKey|null $encryptionKey = null, + ?PrivateKey $signingKey = null, + ?PublicKey $encryptionKey = null, array $decryptionKeys = [], + ?SymmetricKey $preSharedKey = null, + string $preSharedKeyAlgorithm = C::BLOCK_ENC_AES256_GCM, array $IDPList = [], ) { parent::__construct( $entityId, - $encryptionAlgorithmFactory, - $signatureAlgorithmFactory, $signatureAlgorithm, $validatingKeys, $signingKey, $encryptionKey, $decryptionKeys, + $preSharedKey, + $preSharedKeyAlgorithm, $IDPList, ); } diff --git a/src/SAML2/Metadata/ServiceProvider.php b/src/SAML2/Metadata/ServiceProvider.php index 92c2ed323..1ef0df7ce 100644 --- a/src/SAML2/Metadata/ServiceProvider.php +++ b/src/SAML2/Metadata/ServiceProvider.php @@ -23,27 +23,28 @@ class ServiceProvider extends AbstractProvider */ public function __construct( string $entityId, - EncryptionAlgorithmFactory|KeyTransportAlgorithmFactory|null $encryptionAlgorithmFactory = null, - SignatureAlgorithmFactory|null $signatureAlgorithmFactory = null, string $signatureAlgorithm = C::SIG_RSA_SHA256, array $validatingKeys = [], - PrivateKey|null $signingKey = null, - PublicKey|SymmetricKey|null $encryptionKey = null, + ?PrivateKey $signingKey = null, + ?PublicKey $encryptionKey = null, protected array $assertionConsumerService = [], array $decryptionKeys = [], + ?SymmetricKey $preSharedKey = null, + string $preSharedKeyAlgorithm = C::BLOCK_ENC_AES256_GCM, array $IDPList = [], + protected bool $wantAssertionsSigned = false, // Default false by specification ) { Assert::allIsInstanceOf($assertionConsumerService, AssertionConsumerService::class); parent::__construct( $entityId, - $encryptionAlgorithmFactory, - $signatureAlgorithmFactory, $signatureAlgorithm, $validatingKeys, $signingKey, $encryptionKey, $decryptionKeys, + $preSharedKey, + $preSharedKeyAlgorithm, $IDPList, ); } @@ -58,4 +59,15 @@ public function getAssertionConsumerService(): array { return $this->assertionConsumerService; } + + + /** + * Retrieve the configured value for whether assertions must be signed. + * + * @return bool + */ + public function getWantAssertionsSigned(): bool + { + return $this->wantAssertionsSigned; + } } diff --git a/tests/SAML2/Entity/ServiceProviderTest.php b/tests/SAML2/Entity/ServiceProviderTest.php index 76cbbb2ca..74350a986 100644 --- a/tests/SAML2/Entity/ServiceProviderTest.php +++ b/tests/SAML2/Entity/ServiceProviderTest.php @@ -18,7 +18,6 @@ use SimpleSAML\SAML2\XML\samlp\Response; use SimpleSAML\Test\SAML2\MockMetadataProvider; use SimpleSAML\Test\SAML2\MockStateProvider; -use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory; use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException; use SimpleSAML\XMLSecurity\TestUtils\PEMCertificatesMock; @@ -39,7 +38,6 @@ public static function setUpBeforeClass(): void { self::$spMetadata = new Metadata\ServiceProvider( entityId: 'https://simplesamlphp.org/sp/metadata', - signatureAlgorithmFactory: new SignatureAlgorithmFactory(), assertionConsumerService: [ AssertionConsumerService::fromArray([ 'Binding' => C::BINDING_HTTP_POST, @@ -47,6 +45,7 @@ public static function setUpBeforeClass(): void 'Index' => 0, ]), ], + wantAssertionsSigned: true, ); self::$idpMetadata = new Metadata\IdentityProvider(