Skip to content

Commit

Permalink
WIP: Service Provider; Deal with encrypted elements
Browse files Browse the repository at this point in the history
  • Loading branch information
tvdijen committed Jul 27, 2024
1 parent 7b7ad1a commit 3cbfa91
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 49 deletions.
54 changes: 46 additions & 8 deletions src/SAML2/Entity/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;


/**
Expand All @@ -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();
}


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
*
Expand All @@ -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);

Expand All @@ -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();
}
Expand All @@ -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;
}
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
53 changes: 26 additions & 27 deletions src/SAML2/Metadata/AbstractProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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));
}


Expand Down Expand Up @@ -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
{
Expand Down
12 changes: 6 additions & 6 deletions src/SAML2/Metadata/IdentityProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand Down
24 changes: 18 additions & 6 deletions src/SAML2/Metadata/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand All @@ -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;
}
}
3 changes: 1 addition & 2 deletions tests/SAML2/Entity/ServiceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -39,14 +38,14 @@ 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,
'Location' => 'https://example.org/metadata',
'Index' => 0,
]),
],
wantAssertionsSigned: true,
);

self::$idpMetadata = new Metadata\IdentityProvider(
Expand Down

0 comments on commit 3cbfa91

Please sign in to comment.