Passed
Push — master ( 9bd940...1fd260 )
by Tim
02:19
created

ServiceProvider::verifyElementSignature()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 16
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\SAML2\Entity;
6
7
use Exception;
8
use Psr\Http\Message\ServerRequestInterface;
9
use SimpleSAML\Assert\Assert;
10
use SimpleSAML\SAML2\{
11
    Binding,
12
    Metadata,
13
    MetadataProviderInterface,
14
    StateProviderInterface,
15
    StorageProviderInterface,
16
    Utils,
17
};
18
use SimpleSAML\SAML2\Binding\HTTPArtifact,
19
use SimpleSAML\SAML2\Exception\{MetadataNotFoundException, RemoteException, RuntimeException};
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_USE, expecting T_STRING or T_NAME_QUALIFIED or T_NAME_FULLY_QUALIFIED on line 19 at column 0
Loading history...
20
use SimpleSAML\SAML2\Exception\Protocol\{RequestDeniedException, ResourceNotRecognizedException};
21
use SimpleSAML\SAML2\Process\Validator\ResponseValidator;
22
use SimpleSAML\SAML2\XML\saml\{
23
    Assertion,
24
    AttributeStatement,
25
    EncryptedAssertion,
26
    EncryptedAttribute,
27
    EncryptedID,
28
    Subject,
29
};
30
use SimpleSAML\SAML2\XML\samlp\Response;
31
use SimpleSAML\XMLSecurity\Alg\Encryption\EncryptionAlgorithmFactory;
32
use SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException;
33
use SimpleSAML\XMLSecurity\XML\{
34
    EncryptableElementInterface,
35
    EncryptedElementInterface,
36
    SignableElementInterface,
37
    SignedElementInterface,
38
};
39
40
use function sprintf;
41
42
/**
43
 * Class representing a SAML 2 Service Provider.
44
 *
45
 * @package simplesamlphp/saml2
46
 */
47
final class ServiceProvider
48
{
49
    protected ?StateProviderInterface $stateProvider = null;
50
    protected ?StorageProviderInterface $storageProvider = null;
51
    protected ?Metadata\IdentityProvider $idpMetadata = null;
52
53
54
    /**
55
     * @param bool $encryptedAssertions  Whether assertions must be encrypted
56
     * @param bool $disableScoping  Whether to send the samlp:Scoping element in requests
57
     * @param bool $enableUnsolicited  Whether to process unsolicited responses
58
     * @param bool $encryptNameId  Whether to encrypt the NameID sent
59
     * @param bool $signAuthnRequest  Whether to sign the AuthnRequest sent
60
     * @param bool $signLogout  Whether to sign the LogoutRequest/LogoutResponse sent
61
     * @param bool $validateLogout  Whether to validate the signature of LogoutRequest/LogoutResponse received
62
     */
63
    public function __construct(
64
        protected MetadataProviderInterface $metadataProvider,
65
        protected Metadata\ServiceProvider $spMetadata,
66
        protected readonly bool $encryptedAssertions = false,
67
        protected readonly bool $disableScoping = false,
68
        protected readonly bool $enableUnsolicited = false,
69
        protected readonly bool $encryptNameId = false,
70
        protected readonly bool $signAuthnRequest = false,
71
        protected readonly bool $signLogout = false,
72
        protected readonly bool $validateLogout = true,
73
        // Use with caution - will leave any form of signature verification or token decryption up to the implementer
74
        protected readonly bool $bypassResponseVerification = false,
75
        // Use with caution - will leave any form of constraint validation up to the implementer
76
        protected readonly bool $bypassConstraintValidation = false,
77
    ) {
78
    }
79
80
81
    /**
82
     */
83
    public function setStateProvider(StateProviderInterface $stateProvider): void
84
    {
85
        $this->stateProvider = $stateProvider;
86
    }
87
88
89
    /**
90
     */
91
    public function setStorageProvider(StorageProviderInterface $storageProvider): void
92
    {
93
        $this->storageProvider = $storageProvider;
94
    }
95
96
97
    /**
98
     * Receive a verified, and optionally validated Response.
99
     *
100
     * Upon receiving the response from the binding, the signature will be validated first.
101
     * Once the signature checks out, the assertions are decrypted, their signatures verified
102
     *  and then any encrypted NameID's and/or attributes are decrypted.
103
     *
104
     * @param \Psr\Http\Message\ServerRequestInterface $request
105
     * @return \SimpleSAML\SAML2\XML\samlp\Response The validated response.
106
     *
107
     * @throws \SimpleSAML\SAML2\Exception\Protocol\UnsupportedBindingException
108
     */
109
    public function receiveResponse(ServerRequestInterface $request): Response
110
    {
111
        $binding = Binding::getCurrentBinding($request);
112
113
        if ($binding instanceof HTTPArtifact) {
114
            if ($this->storageProvider === null) {
115
                throw new RuntimeException(
116
                    "A StorageProvider is required to use the HTTP-Artifact binding.",
117
                );
118
            }
119
120
            $artifact = $binding->receiveArtifact($request);
121
            $this->idpMetadata = $this->metadataProvider->getIdPMetadataForSha1($artifact->getSourceId());
122
123
            if ($this->idpMetadata === null) {
124
                throw new MetadataNotFoundException(sprintf(
125
                    'No metadata found for remote entity with SHA1 ID: %s',
126
                    $artifact->getSourceId(),
127
                ));
128
            }
129
130
            $binding->setIdpMetadata($this->idpMetadata);
131
            $binding->setSPMetadata($this->spMetadata);
132
        }
133
134
        $rawResponse = $binding->receive($request);
135
        Assert::isInstanceOf($rawResponse, Response::class, ResourceNotRecognizedException::class); // Wrong type of msg
136
137
        // Will return a raw Response prior to any form of verification
138
        if ($this->bypassResponseVerification === true) {
139
            return $rawResponse;
140
        }
141
142
        // Fetch the metadata for the remote entity
143
        if (!($binding instanceof HTTPArtifact)) {
144
            $this->idpMetadata = $this->metadataProvider->getIdPMetadata($rawResponse->getIssuer()->getContent());
145
146
            if ($this->idpMetadata === null) {
147
                throw new MetadataNotFoundException(sprintf(
148
                    'No metadata found for remote entity with entityID: %s',
149
                    $rawResponse->getIssuer()->getContent(),
150
                ));
151
            }
152
        }
153
154
        // Verify the signature (if any)
155
        $verifiedResponse = $rawResponse->isSigned() ? $this->verifyElementSignature($rawResponse) : $rawResponse;
156
157
        $state = null;
158
        $stateId = $verifiedResponse->getInResponseTo();
159
160
        if (!empty($stateId)) {
161
            if ($this->stateProvider === null) {
162
                throw new RuntimeException(
163
                    "A StateProvider is required to correlate responses to their initial request.",
164
                );
165
            }
166
167
            // this should be a response to a request we sent earlier
168
            try {
169
                $state = $this->stateProvider::loadState($stateId, 'saml:sp:sso');
170
            } catch (RuntimeException $e) {
171
                // something went wrong,
172
                Utils::getContainer()->getLogger()->warning(sprintf(
173
                    'Could not load state specified by InResponseTo: %s; processing response as unsolicited.',
174
                    $e->getMessage(),
175
                ));
176
            }
177
        }
178
179
        $issuer = $verifiedResponse->getIssuer()->getContent();
180
        if ($state === null) {
181
            if ($this->enableUnsolicited === false) {
182
                throw new RequestDeniedException('Unsolicited responses are denied by configuration.');
183
            }
184
        } else {
185
            // check that the issuer is the one we are expecting
186
            Assert::keyExists($state, 'ExpectedIssuer');
187
188
            if ($state['ExpectedIssuer'] !== $issuer) {
189
                throw new ResourceNotRecognizedException("Issuer doesn't match the one the AuthnRequest was sent to.");
190
            }
191
        }
192
193
        $this->idpMetadata = $this->metadataProvider->getIdPMetadata($issuer);
194
        if ($this->idpMetadata === null) {
195
            throw new MetadataNotFoundException(sprintf(
196
                'No metadata found for remote identity provider with entityID: %s',
197
                $issuer,
198
            ));
199
        }
200
201
        $responseValidator = ResponseValidator::createResponseValidator(
202
            $this->idpMetadata,
203
            $this->spMetadata,
204
            $binding,
205
        );
206
        $responseValidator->validate($verifiedResponse);
207
208
        // Decrypt and verify assertions, then rebuild the response.
209
        $verifiedAssertions = $this->decryptAndVerifyAssertions($verifiedResponse->getAssertions());
210
        $decryptedResponse = new Response(
211
            $verifiedResponse->getStatus(),
212
            $verifiedResponse->getIssueInstant(),
213
            $verifiedResponse->getIssuer(),
214
            $verifiedResponse->getID(),
215
            $verifiedResponse->getVersion(),
216
            $verifiedResponse->getInResponseTo(),
217
            $verifiedResponse->getDestination(),
218
            $verifiedResponse->getConsent(),
219
            $verifiedResponse->getExtensions(),
220
            $verifiedAssertions,
221
        );
222
223
224
        // Will return a verified and fully decrypted Response prior to any form of validation
225
        if ($this->bypassConstraintValidation === true) {
226
            return $decryptedResponse;
227
        }
228
229
        // TODO: Validate assertions
230
        return $decryptedResponse;
231
    }
232
233
234
    /**
235
     * Process the assertions and decrypt any encrypted elements inside.
236
     *
237
     * @param \SimpleSAML\SAML2\XML\saml\Assertion[] $unverifiedAssertions
238
     * @return \SimpleSAML\SAML2\XML\saml\Assertion[]
239
     *
240
     * @throws \SimpleSAML\SAML2\Exception\RuntimeException if none of the keys could be used to decrypt the element
241
     */
242
    protected function decryptAndVerifyAssertions(array $unverifiedAssertions): array
243
    {
244
        /**
245
         * See paragraph 6.2 of the SAML 2.0 core specifications for the applicable processing rules
246
         *
247
         * Long story short - Decrypt the assertion first, then validate it's signature
248
         * Once the signature is verified, decrypt any BaseID, NameID or Attribute that's encrypted
249
         */
250
        $verifiedAssertions = [];
251
        foreach ($unverifiedAssertions as $i => $assertion) {
252
            // Decrypt the assertions
253
            $decryptedAssertion = ($assertion instanceof EncryptedAssertion)
254
                ? $this->decryptElement($assertion)
255
                : $assertion;
256
257
            // Verify the signature on the assertions (if any)
258
            $verifiedAssertion = $this->verifyElementSignature($decryptedAssertion);
259
260
            // Decrypt the NameID and replace it inside the assertion's Subject
261
            $nameID = $verifiedAssertion->getSubject()?->getIdentifier();
262
263
            if ($nameID instanceof EncryptedID) {
264
                $decryptedNameID = $this->decryptElement($nameID);
265
                $subject = new Subject($decryptedNameID, $verifiedAssertion->getSubjectConfirmation());
266
            } else {
267
                $subject = $verifiedAssertion->getSubject();
268
            }
269
270
            // Decrypt any occurrences of EncryptedAttribute and replace them inside the assertion's AttributeStatement
271
            $statements = $verifiedAssertion->getStatements();
272
            foreach ($verifiedAssertion->getStatements() as $j => $statement) {
273
                if ($statement instanceof AttributeStatement) {
274
                    $attributes = $statement->getAttributes();
275
                    if ($statement->hasEncryptedAttributes()) {
276
                        foreach ($statement->getEncryptedAttributes() as $encryptedAttribute) {
277
                            $attributes[] = $this->decryptElement($encryptedAttribute);
278
                        }
279
                    }
280
281
                    $statements[$j] = new AttributeStatement($attributes);
282
                }
283
            }
284
285
            // Rebuild the Assertion
286
            $verifiedAssertions[] = new Assertion(
287
                $verifiedAssertion->getIssuer(),
288
                $verifiedAssertion->getIssueInstant(),
289
                $verifiedAssertion->getID(),
290
                $subject,
291
                $verifiedAssertion->getConditions(),
292
                $statements,
293
            );
294
        }
295
296
        return $verifiedAssertions;
297
    }
298
299
300
    /**
301
     * Decrypt the given element using the decryption keys provided to us.
302
     *
303
     * @param \SimpleSAML\XMLSecurity\XML\EncryptedElementInterface $element
304
     * @return \SimpleSAML\XMLSecurity\EncryptableElementInterface
305
     *
306
     * @throws \SimpleSAML\SAML2\Exception\RuntimeException if none of the keys could be used to decrypt the element
307
     */
308
    protected function decryptElement(EncryptedElementInterface $element): EncryptableElementInterface
309
    {
310
        $factory = $this->spMetadata->getEncryptionAlgorithmFactory();
311
312
        $encryptionAlgorithm = ($factory instanceof EncryptionAlgorithmFactory)
313
            ? $element->getEncryptedData()->getEncryptionMethod()
314
            : $element->getEncryptedKey()->getEncryptionMethod();
315
316
        foreach ($this->spMetadata->getDecriptionKeys() as $decryptionKey) {
317
            $decryptor = $factory->getAlgorithm($encryptionAlgorithm, $decryptionKey);
318
            try {
319
                return $element->decrypt($decryptor);
320
            } catch (Exception $e) {
321
                continue;
322
            }
323
        }
324
325
        throw new RuntimeException(sprintf(
326
            'Unable to decrypt %s with any of the available keys.',
327
            $element::class,
328
        ));
329
    }
330
331
332
    /**
333
     * Verify the signature of an element using the available validation keys.
334
     *
335
     * @param \SimpleSAML\XMLSecurity\XML\SignedElementInterface $element
336
     * @return \SimpleSAML\XMLSecurity\XML\SignableElementInterface The validated element.
337
     *
338
     * @throws \SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException
339
     */
340
    protected function verifyElementSignature(SignedElementInterface $element): SignableElementInterface
341
    {
342
        $factory = $this->spMetadata->getSignatureAlgorithmFactory();
343
        $signatureAlgorithm = $element->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm();
344
345
        foreach ($this->idpMetadata->getValidatingKeys() as $validatingKey) {
346
            $verifier = $factory->getAlgorithm($signatureAlgorithm, $validatingKey);
347
348
            try {
349
                return $element->verify($verifier);
350
            } catch (SignatureVerificationFailedException $e) {
351
                continue;
352
            }
353
        }
354
355
        throw new SignatureVerificationFailedException();
356
    }
357
}
358