@@ -194,6 +194,213 @@ def test_request_encrypt_cert_only(self):
194194 response_parser = ResponseProcessor (self .source , http_request )
195195 response_parser .parse ()
196196
197+ def test_request_sign_response_and_encrypt (self ):
198+ """Test SAML with sign_response enabled AND encryption.
199+
200+ This tests the fix for signature invalidation when encryption is enabled.
201+ The response must be signed AFTER encryption, not before, because encryption
202+ replaces the Assertion with EncryptedAssertion which changes the response content.
203+ """
204+ self .provider .sign_response = True
205+ self .provider .sign_assertion = False
206+ self .provider .encryption_kp = self .cert
207+ self .provider .save ()
208+ self .source .encryption_kp = self .cert
209+ self .source .signed_response = True
210+ self .source .signed_assertion = False # Only response is signed, not assertion
211+ self .source .save ()
212+ http_request = self .request_factory .get ("/" , user = get_anonymous_user ())
213+
214+ # First create an AuthNRequest
215+ request_proc = RequestProcessor (self .source , http_request , "test_state" )
216+ request = request_proc .build_auth_n ()
217+
218+ # To get an assertion we need a parsed request (parsed by provider)
219+ parsed_request = AuthNRequestParser (self .provider ).parse (
220+ b64encode (request .encode ()).decode (), "test_state"
221+ )
222+ # Now create a response and convert it to string (provider)
223+ response_proc = AssertionProcessor (self .provider , http_request , parsed_request )
224+ response = response_proc .build_response ()
225+
226+ # Verify the response contains EncryptedAssertion and a signature
227+ response_xml = fromstring (response )
228+ self .assertEqual (len (response_xml .xpath ("//saml:EncryptedAssertion" , namespaces = NS_MAP )), 1 )
229+ self .assertEqual (
230+ len (response_xml .xpath ("//samlp:Response/ds:Signature" , namespaces = NS_MAP )), 1
231+ )
232+
233+ # Now parse the response (source) - this will verify the signature and decrypt
234+ http_request .POST = QueryDict (mutable = True )
235+ http_request .POST ["SAMLResponse" ] = b64encode (response .encode ()).decode ()
236+
237+ response_parser = ResponseProcessor (self .source , http_request )
238+ response_parser .parse ()
239+
240+ def test_request_sign_assertion_and_encrypt (self ):
241+ """Test SAML with sign_assertion enabled AND encryption.
242+
243+ The assertion signature should be inside the encrypted content and
244+ remain valid after decryption.
245+ """
246+ self .provider .sign_response = False
247+ self .provider .sign_assertion = True
248+ self .provider .encryption_kp = self .cert
249+ self .provider .save ()
250+ self .source .encryption_kp = self .cert
251+ self .source .signed_assertion = True
252+ self .source .save ()
253+ http_request = self .request_factory .get ("/" , user = get_anonymous_user ())
254+
255+ # First create an AuthNRequest
256+ request_proc = RequestProcessor (self .source , http_request , "test_state" )
257+ request = request_proc .build_auth_n ()
258+
259+ # To get an assertion we need a parsed request (parsed by provider)
260+ parsed_request = AuthNRequestParser (self .provider ).parse (
261+ b64encode (request .encode ()).decode (), "test_state"
262+ )
263+ # Now create a response and convert it to string (provider)
264+ response_proc = AssertionProcessor (self .provider , http_request , parsed_request )
265+ response = response_proc .build_response ()
266+
267+ # Verify the response contains EncryptedAssertion
268+ response_xml = fromstring (response )
269+ self .assertEqual (len (response_xml .xpath ("//saml:EncryptedAssertion" , namespaces = NS_MAP )), 1 )
270+
271+ # Now parse the response (source) - this will decrypt and verify assertion signature
272+ http_request .POST = QueryDict (mutable = True )
273+ http_request .POST ["SAMLResponse" ] = b64encode (response .encode ()).decode ()
274+
275+ response_parser = ResponseProcessor (self .source , http_request )
276+ response_parser .parse ()
277+
278+ def test_request_sign_both_and_encrypt (self ):
279+ """Test SAML with both sign_assertion and sign_response enabled AND encryption.
280+
281+ This is the most complex scenario: assertion is signed, then encrypted,
282+ then the response is signed. Both signatures should be valid.
283+ """
284+ self .provider .sign_response = True
285+ self .provider .sign_assertion = True
286+ self .provider .encryption_kp = self .cert
287+ self .provider .save ()
288+ self .source .encryption_kp = self .cert
289+ self .source .signed_assertion = True
290+ self .source .signed_response = True
291+ self .source .save ()
292+ http_request = self .request_factory .get ("/" , user = get_anonymous_user ())
293+
294+ # First create an AuthNRequest
295+ request_proc = RequestProcessor (self .source , http_request , "test_state" )
296+ request = request_proc .build_auth_n ()
297+
298+ # To get an assertion we need a parsed request (parsed by provider)
299+ parsed_request = AuthNRequestParser (self .provider ).parse (
300+ b64encode (request .encode ()).decode (), "test_state"
301+ )
302+ # Now create a response and convert it to string (provider)
303+ response_proc = AssertionProcessor (self .provider , http_request , parsed_request )
304+ response = response_proc .build_response ()
305+
306+ # Verify the response contains EncryptedAssertion and response signature
307+ response_xml = fromstring (response )
308+ self .assertEqual (len (response_xml .xpath ("//saml:EncryptedAssertion" , namespaces = NS_MAP )), 1 )
309+ self .assertEqual (
310+ len (response_xml .xpath ("//samlp:Response/ds:Signature" , namespaces = NS_MAP )), 1
311+ )
312+
313+ # Now parse the response (source) - this will verify response signature,
314+ # decrypt, then verify assertion signature
315+ http_request .POST = QueryDict (mutable = True )
316+ http_request .POST ["SAMLResponse" ] = b64encode (response .encode ()).decode ()
317+
318+ response_parser = ResponseProcessor (self .source , http_request )
319+ response_parser .parse ()
320+
321+ def test_encrypted_assertion_namespace_preservation (self ):
322+ """Test that encrypted assertions include namespace declarations.
323+
324+ When an assertion is encrypted, the resulting decrypted XML must include
325+ the necessary namespace declarations (xmlns:saml) since it's now a standalone
326+ document fragment, no longer inheriting namespaces from the parent Response.
327+ """
328+ self .provider .encryption_kp = self .cert
329+ self .provider .save ()
330+ self .source .encryption_kp = self .cert
331+ self .source .save ()
332+ http_request = self .request_factory .get ("/" , user = get_anonymous_user ())
333+
334+ # First create an AuthNRequest
335+ request_proc = RequestProcessor (self .source , http_request , "test_state" )
336+ request = request_proc .build_auth_n ()
337+
338+ # To get an assertion we need a parsed request (parsed by provider)
339+ parsed_request = AuthNRequestParser (self .provider ).parse (
340+ b64encode (request .encode ()).decode (), "test_state"
341+ )
342+ # Now create a response and convert it to string (provider)
343+ response_proc = AssertionProcessor (self .provider , http_request , parsed_request )
344+ response = response_proc .build_response ()
345+
346+ # Parse the encrypted response
347+ response_xml = fromstring (response )
348+ encrypted_assertion = response_xml .xpath ("//saml:EncryptedAssertion" , namespaces = NS_MAP )[0 ]
349+ encrypted_data = encrypted_assertion .xpath ("//xenc:EncryptedData" , namespaces = NS_MAP )[0 ]
350+
351+ # Decrypt the assertion manually to verify namespace is present
352+ import xmlsec
353+
354+ manager = xmlsec .KeysManager ()
355+ key = xmlsec .Key .from_memory (self .cert .key_data , xmlsec .constants .KeyDataFormatPem , None )
356+ manager .add_key (key )
357+ enc_ctx = xmlsec .EncryptionContext (manager )
358+ decrypted = enc_ctx .decrypt (encrypted_data )
359+
360+ # The decrypted assertion should have xmlns:saml namespace declaration
361+ decrypted_str = etree .tostring (decrypted ).decode ()
362+ self .assertIn ("xmlns:saml" , decrypted_str )
363+
364+ # Also verify full round-trip works (source can parse it)
365+ http_request .POST = QueryDict (mutable = True )
366+ http_request .POST ["SAMLResponse" ] = b64encode (response .encode ()).decode ()
367+
368+ response_parser = ResponseProcessor (self .source , http_request )
369+ response_parser .parse ()
370+
371+ def test_encrypted_response_schema_validation (self ):
372+ """Test that encrypted SAML responses validate against the SAML schema.
373+
374+ The response with EncryptedAssertion must be valid per saml-schema-protocol-2.0.xsd.
375+ This ensures we don't have invalid elements like EncryptedData inside Assertion.
376+ """
377+ self .provider .encryption_kp = self .cert
378+ self .provider .save ()
379+ http_request = self .request_factory .get ("/" , user = get_anonymous_user ())
380+
381+ # First create an AuthNRequest
382+ request_proc = RequestProcessor (self .source , http_request , "test_state" )
383+ request = request_proc .build_auth_n ()
384+
385+ # To get an assertion we need a parsed request (parsed by provider)
386+ parsed_request = AuthNRequestParser (self .provider ).parse (
387+ b64encode (request .encode ()).decode (), "test_state"
388+ )
389+ # Now create a response and convert it to string (provider)
390+ response_proc = AssertionProcessor (self .provider , http_request , parsed_request )
391+ response = response_proc .build_response ()
392+
393+ # Validate against SAML schema
394+ schema = etree .XMLSchema (
395+ etree .parse ("schemas/saml-schema-protocol-2.0.xsd" , parser = etree .XMLParser ()) # nosec
396+ )
397+ self .assertTrue (schema .validate (lxml_from_string (response )))
398+
399+ # Verify structure: should have EncryptedAssertion, not Assertion with EncryptedData inside
400+ response_xml = fromstring (response )
401+ self .assertEqual (len (response_xml .xpath ("//saml:EncryptedAssertion" , namespaces = NS_MAP )), 1 )
402+ self .assertEqual (len (response_xml .xpath ("//saml:Assertion" , namespaces = NS_MAP )), 0 )
403+
197404 def test_request_signed (self ):
198405 """Test full SAML Request/Response flow, fully signed"""
199406 http_request = self .request_factory .get ("/" , user = get_anonymous_user ())
0 commit comments