forked from Trondheim-kommune/python-saml-idporten
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request Trondheim-kommune#1 in BAS/python-saml-idporten fr…
…om feature/data-557 to master * commit '9a1f4388dc8f4b3d4b70dea849465c4b48a43fda': (27 commits) parameter error to xmlsec1 Bumpe versjon DATA-557. Typo. DATA-557. Deleted test-file. DATA-557. Updated documentation. DATA-557. Some more checking of the HTTP-status. DATA-557. Ready for review. DATA-557. A working version. Need more clean-up in the code. DATA-557. Changed documentation. Added names of positional parameters just in case another python-version is used. DATA-557. Added modeline for vim. DATA-557. Added modeline for vim. DATA-557. Added some debug. DATA-557. Trouble with communication,- but commit these changes for later. Not finished! DATA-557. Fixed a bug. DATA-557. Added comment. DATA-557. Should be possible to choose assertion-binding. DATA-557. Getting ready for install. DATA-557. Possible to override the Elementree-object. Added lots for documentation. DATA-557. Fixed syntax-error in case the script should take options and parameters. DATA-557. Added documentation, and a small extension. ...
- Loading branch information
Showing
14 changed files
with
788 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
# -*- coding: utf-8 -*- | ||
# vim: et:ts=4:sw=4:sts=4 | ||
# | ||
# Copyright(c) 2016 Norwegian Univeristy of Science and Technology. | ||
# | ||
""" | ||
Creates an SAML2 ArtifactResolve message. | ||
""" | ||
import uuid | ||
|
||
from datetime import datetime | ||
from lxml.builder import ElementMaker | ||
|
||
from SignableDocument import SignableDocument | ||
|
||
|
||
class ArtifactResolve(SignableDocument): | ||
"""Creates an SAML2 ArtifactResolve message.""" | ||
|
||
def __init__(self, artifact, _etree=None, _clock=None, _uuid=None, | ||
_debug=False, **kwargs): | ||
"""A class that a SAML2 Artifactresolve-document. | ||
Keywords argument: | ||
artifact -- The artifact-string recieved from the IDP. | ||
_etree -- Override the default etree-object (default None). | ||
_clock -- Override the default datetime-object (default None). | ||
_uuid -- Override the defualt uuid-generator (default None). | ||
_debug -- Print debug (default False). | ||
issuer -- The name of your application. | ||
This class should produce an SAML2 ArtifactResolve protocol- | ||
message like this: | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<samlp:ArtifactResolve xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" | ||
ID="<some-id>" | ||
IssueInstant="<some-time>" | ||
Version="2.0"> | ||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><some-issuer></saml:Issuer> | ||
<ns1:Signature xmlns:ns1="http://www.w3.org/2000/09/xmldsig#"> | ||
<ns1:SignedInfo> | ||
<ns1:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> | ||
<ns1:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> | ||
<ns1:Reference URI="#<some-id>"> | ||
<ns1:Transforms> | ||
<ns1:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> | ||
<ns1:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> | ||
</ns1:Transforms> | ||
<ns1:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> | ||
<ns1:DigestValue /> | ||
</ns1:Reference> | ||
</ns1:SignedInfo> | ||
<ns1:SignatureValue /> | ||
<ns1:KeyInfo > | ||
<ns1:X509Data /> | ||
</ns1:KeyInfo> | ||
</ns1:Signature> | ||
<samlp:Artifact><some-artifact-string></samlp:Artifact> | ||
</samlp:ArtifactResolve>""" | ||
super(ArtifactResolve, self).__init__( | ||
_node_name='urn:oasis:names:tc:SAML:2.0:protocol:ArtifactResolve', | ||
_etree=_etree, _debug=_debug) | ||
|
||
if _clock is None: | ||
_clock = datetime.utcnow | ||
if _uuid is None: | ||
_uuid = uuid.uuid4 | ||
|
||
now = _clock() | ||
now = now.replace(microsecond=0) | ||
now_iso = now.isoformat() + ".875Z" | ||
|
||
unique_id = _uuid() | ||
unique_id = unique_id.hex | ||
issuer = kwargs.pop('issuer') | ||
|
||
samlp_maker = ElementMaker( | ||
namespace='urn:oasis:names:tc:SAML:2.0:protocol', | ||
nsmap=dict(saml2p='urn:oasis:names:tc:SAML:2.0:protocol'), | ||
) | ||
saml_maker = ElementMaker( | ||
namespace='urn:oasis:names:tc:SAML:2.0:assertion', | ||
nsmap=dict(saml2='urn:oasis:names:tc:SAML:2.0:assertion'), | ||
) | ||
artifact_resolve = samlp_maker.ArtifactResolve( | ||
Version='2.0', | ||
IssueInstant=now_iso, | ||
ID=unique_id, | ||
) | ||
|
||
saml_issuer = saml_maker.Issuer() | ||
saml_issuer.text = issuer | ||
artifact_resolve.append(saml_issuer) | ||
|
||
# Add Issuer and artifact under signature. | ||
artifact_resolve.append(self._create_signature(unique_id)) | ||
|
||
saml_artifact = samlp_maker.Artifact() | ||
saml_artifact.text = artifact | ||
artifact_resolve.append(saml_artifact) | ||
|
||
self.document = artifact_resolve | ||
|
||
|
||
@staticmethod | ||
def _create_signature(unique_id): | ||
"""Creates all XML-elements needed for an XML-signature.""" | ||
signature_maker = ElementMaker( | ||
namespace='http://www.w3.org/2000/09/xmldsig#', | ||
nsmap=dict(ns1='http://www.w3.org/2000/09/xmldsig#') | ||
) | ||
|
||
signature_elem = signature_maker.Signature() | ||
|
||
signed_info_elem = signature_maker.SignedInfo() | ||
signature_elem.append(signed_info_elem) | ||
|
||
signed_info_elem.append(signature_maker.CanonicalizationMethod( | ||
Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#' | ||
)) | ||
|
||
signed_info_elem.append(signature_maker.SignatureMethod( | ||
Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1' | ||
)) | ||
|
||
reference_elem = signature_maker.Reference( | ||
URI='#' + unique_id | ||
) | ||
|
||
signed_info_elem.append(reference_elem) | ||
|
||
transforms_elem = signature_maker.Transforms() | ||
reference_elem.append(transforms_elem) | ||
|
||
transforms_elem.append(signature_maker.Transform( | ||
Algorithm='http://www.w3.org/2000/09/xmldsig#enveloped-signature' | ||
)) | ||
transforms_elem.append(signature_maker.Transform( | ||
Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#' | ||
)) | ||
|
||
reference_elem.append(signature_maker.DigestMethod( | ||
Algorithm='http://www.w3.org/2000/09/xmldsig#sha1' | ||
)) | ||
reference_elem.append(signature_maker.DigestValue()) | ||
|
||
signature_elem.append(signature_maker.SignatureValue()) | ||
|
||
key_info_elem = signature_maker.KeyInfo() | ||
key_info_elem.append(signature_maker.X509Data()) | ||
|
||
signature_elem.append(key_info_elem) | ||
return signature_elem | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
# -*- coding: utf-8 -*- | ||
# vim: et:ts=4:sw=4:sts=4 | ||
# | ||
# Copyright(c) 2016 Norwegian Univeristy of Science and Technology. | ||
# | ||
""" | ||
Creates an SAML2 ArtifactResponse message. | ||
The soap-envelope is also a part of the response. | ||
""" | ||
import base64 | ||
|
||
from lxml import etree | ||
from datetime import datetime, timedelta | ||
|
||
from SignatureVerifier import SignatureVerifier | ||
|
||
from Response import ResponseValidationError, ResponseNameIDError | ||
|
||
|
||
namespaces = {'samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', | ||
'saml': 'urn:oasis:names:tc:SAML:2.0:assertion', | ||
'soap-env': 'http://schemas.xmlsoap.org/soap/envelope/'} | ||
|
||
|
||
class ArtifactResponse(object): | ||
""" | ||
Creates an SAML2 ArtifactResponse message. | ||
The soap-envelope is regarded as part of the response. | ||
""" | ||
|
||
def __init__(self, art_resp, _base64=None, _etree=None): | ||
super(ArtifactResponse, self).__init__() | ||
if _base64 is None: | ||
_base64 = base64 | ||
if _etree is None: | ||
_etree = etree | ||
|
||
self._signature = None | ||
self.decrypted = None | ||
self._decrypted_document = None | ||
self._document = _etree.fromstring(art_resp) | ||
|
||
|
||
def _parse_datetime(self, date_time): | ||
""" | ||
Parse a date as a string in ISO-format. | ||
""" | ||
return datetime.strptime(date_time, '%Y-%m-%dT%H:%M:%SZ') | ||
|
||
|
||
def _get_name_id(self): | ||
""" | ||
Parse out the SAML2 NameID element. | ||
""" | ||
result = self._decrypted_document.xpath( | ||
('/soap-env:Envelope/soap-env:Body/samlp:ArtifactResponse' | ||
'/samlp:Response/saml:EncryptedAssertion/saml:Assertion/' | ||
'saml:Subject/saml:NameID'), | ||
namespaces=namespaces, | ||
) | ||
length = len(result) | ||
if length > 1: | ||
raise ResponseNameIDError( | ||
'Found more than one name ID' | ||
) | ||
if length == 0: | ||
raise ResponseNameIDError( | ||
'Did not find a name ID' | ||
) | ||
|
||
node = result.pop() | ||
|
||
return node.text.strip() | ||
|
||
|
||
name_id = property( | ||
fget=_get_name_id, | ||
doc=("The value requested in the name_identifier_format, e.g.," | ||
"the user's email address"), | ||
) | ||
|
||
|
||
def get_session_index(self): | ||
""" | ||
Get the SAML2 session-index. | ||
""" | ||
result = self._decrypted_document.xpath( | ||
('/soap-env:Envelope/soap-env:Body/samlp:ArtifactResponse' | ||
'/samlp:Response/saml:EncryptedAssertion/saml:Assertion' | ||
'/saml:AuthnStatement/@SessionIndex'), | ||
namespaces=namespaces, | ||
) | ||
|
||
return result[0] | ||
|
||
|
||
def get_assertion_attribute_value(self, attribute_name): | ||
""" | ||
Get the value of an AssertionAttribute, located in an | ||
Assertion/AttributeStatement/Attribute[@Name=attribute_name | ||
/AttributeValue tag | ||
""" | ||
result = self._document.xpath( | ||
('/soap-env:Envelope/soap-env:Body/samlp:ArtifactResponse' | ||
'/samlp:Response/saml:Assertion/saml:AttributeStatement' | ||
'/saml:Attribute[@Name="%s"]/saml:AttributeValue' | ||
% attribute_name), namespaces=namespaces) | ||
return [n.text.strip() for n in result] | ||
|
||
|
||
def is_valid(self, idp_cert_filename, private_key_file, | ||
_clock=None, _verifier=None | ||
): | ||
""" | ||
Verify that the samlp:ArtifactResponse is valid. | ||
Return True if valid, otherwise False. | ||
""" | ||
if _clock is None: | ||
_clock = datetime.utcnow | ||
if _verifier is None: | ||
_verifier = SignatureVerifier(idp_cert_filename, private_key_file) | ||
|
||
conditions = self._document.xpath( | ||
('/soap-env:Envelope/soap-env:Body/samlp:ArtifactResponse' | ||
'/samlp:Response/saml:Assertion/saml:Conditions'), | ||
namespaces=namespaces, | ||
) | ||
|
||
now = _clock() | ||
|
||
not_before = None | ||
not_on_or_after = None | ||
for condition in conditions: | ||
not_on_or_after = condition.attrib.get('NotOnOrAfter', None) | ||
not_before = condition.attrib.get('NotBefore', None) | ||
|
||
if not_before is None: | ||
#notbefore condition is not mandatory. If it is not | ||
# specified, use yesterday as not_before condition | ||
not_before = (now-timedelta(1, 0, 0)).strftime('%Y-%m-%dT%H:%M:%SZ') | ||
#TODO: this is in the encrypted part in our case.. | ||
#if not_on_or_after is None: | ||
# raise ResponseConditionError('Did not find NotOnOrAfter condition') | ||
|
||
not_before = self._parse_datetime(not_before) | ||
#not_on_or_after = self._parse_datetime(not_on_or_after) | ||
|
||
if now < not_before: | ||
raise ResponseValidationError( | ||
'Current time is earlier than NotBefore condition' | ||
) | ||
#if now >= not_on_or_after: | ||
# raise ResponseValidationError( | ||
# 'Current time is on or after NotOnOrAfter condition' | ||
# ) | ||
|
||
is_valid, decrypted = _verifier.verify_and_decrypt( | ||
self._document, self._signature, | ||
_node_name=('urn:oasis:names:tc:SAML:2.0:protocol:' | ||
'ArtifactResponse')) | ||
|
||
self.decrypted = decrypted | ||
self._decrypted_document = etree.fromstring(self.decrypted) | ||
return is_valid | ||
|
||
|
||
def get_decrypted_assertion_attribute_value(self, attribute_name): | ||
""" | ||
Get the value of an AssertionAttribute, located in an | ||
Assertion/AttributeStatement/Attribute[@Name=attribute_name/AttributeValue tag | ||
""" | ||
result = self._decrypted_document.xpath( | ||
('/soap-env:Envelope/soap-env:Body/samlp:ArtifactResponse' | ||
'/samlp:Response/saml:EncryptedAssertion/saml:Assertion' | ||
'/saml:AttributeStatement/saml:Attribute[@Name="%s"]' | ||
'/saml:AttributeValue' % attribute_name), | ||
namespaces=namespaces) | ||
return [n.text.strip() for n in result] | ||
|
||
|
Oops, something went wrong.