Skip to content

Commit

Permalink
Merge pull request Trondheim-kommune#1 in BAS/python-saml-idporten fr…
Browse files Browse the repository at this point in the history
…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
Steinar Kleven committed Apr 15, 2016
2 parents d76792f + 9a1f438 commit 506b2e7
Show file tree
Hide file tree
Showing 14 changed files with 788 additions and 60 deletions.
12 changes: 12 additions & 0 deletions example.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ idp_cert_fingerprint =
# Full path to the service provider's private server key.
private_key_file =

# SAML Artifact resolver URL at idporten's server.
# Hint: This is the element:
# ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
# In the metadata-file for ID-porten.
artifact_resolver_url =

# SAML assertion binding,- e.g. how to get the assertion. Either get it posted
# from idporten's server or use SOAP to fetch the assertion from ID-porten's
# server at the URL defined in "artifact_resolver_url".
# Possible values: HTTP-POST or HTTP-Artifact
# If not defined, the library will default to HTTP-Artifact
sp_assertion_binding =
155 changes: 155 additions & 0 deletions idporten/saml/ArtifactResolve.py
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

180 changes: 180 additions & 0 deletions idporten/saml/ArtifactResponse.py
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]


Loading

0 comments on commit 506b2e7

Please sign in to comment.