Better support for signing
This commit is contained in:
@@ -300,8 +300,10 @@ class Server(object):
|
|||||||
if not status:
|
if not status:
|
||||||
status = success_status_factory()
|
status = success_status_factory()
|
||||||
|
|
||||||
|
_issuer = self.issuer()
|
||||||
|
|
||||||
response = response_factory(
|
response = response_factory(
|
||||||
issuer=self.issuer(),
|
issuer=_issuer,
|
||||||
in_response_to = in_response_to,
|
in_response_to = in_response_to,
|
||||||
destination = consumer_url,
|
destination = consumer_url,
|
||||||
status = status,
|
status = status,
|
||||||
@@ -318,7 +320,7 @@ class Server(object):
|
|||||||
|
|
||||||
assertion = ast.construct(sp_entity_id, in_response_to, name_id,
|
assertion = ast.construct(sp_entity_id, in_response_to, name_id,
|
||||||
self.conf.attribute_converters(),
|
self.conf.attribute_converters(),
|
||||||
policy)
|
policy, issuer=_issuer)
|
||||||
|
|
||||||
if sign:
|
if sign:
|
||||||
assertion["signature"] = pre_signature_part(assertion["id"])
|
assertion["signature"] = pre_signature_part(assertion["id"])
|
||||||
@@ -379,7 +381,7 @@ class Server(object):
|
|||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
|
|
||||||
def authn_response(self, identity, in_response_to, destination,
|
def authn_response(self, identity, in_response_to, destination,
|
||||||
sp_entity_id, name_id_policy, userid):
|
sp_entity_id, name_id_policy, userid, sign=False):
|
||||||
""" Constructs an AuthenticationResponse
|
""" Constructs an AuthenticationResponse
|
||||||
|
|
||||||
:param identity: Information about an user
|
:param identity: Information about an user
|
||||||
@@ -407,11 +409,21 @@ class Server(object):
|
|||||||
in_response_to, # in_response_to
|
in_response_to, # in_response_to
|
||||||
sp_entity_id, # sp_entity_id
|
sp_entity_id, # sp_entity_id
|
||||||
identity, # identity as dictionary
|
identity, # identity as dictionary
|
||||||
name_id
|
name_id,
|
||||||
|
sign=sign
|
||||||
)
|
)
|
||||||
except MissingValue, exc:
|
except MissingValue, exc:
|
||||||
response = self.error_response(destination, in_response_to,
|
response = self.error_response(destination, in_response_to,
|
||||||
sp_entity_id, exc, name_id)
|
sp_entity_id, exc, name_id)
|
||||||
|
|
||||||
|
|
||||||
return ("%s" % response).split("\n")
|
if sign:
|
||||||
|
try:
|
||||||
|
return self.sc.sign_statement_using_xmlsec(response,
|
||||||
|
class_name(response))
|
||||||
|
except Exception, exc:
|
||||||
|
response = self.error_response(destination, in_response_to,
|
||||||
|
sp_entity_id, exc, name_id)
|
||||||
|
return ("%s" % response).split("\n")
|
||||||
|
else:
|
||||||
|
return ("%s" % response).split("\n")
|
||||||
|
|||||||
@@ -19,13 +19,15 @@
|
|||||||
Based on the use of xmlsec1 binaries and not the python xmlsec module.
|
Based on the use of xmlsec1 binaries and not the python xmlsec module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from saml2 import samlp, class_name, saml
|
from saml2 import samlp, class_name, saml, make_instance
|
||||||
|
from saml2 import create_class_from_xml_string
|
||||||
import xmldsig as ds
|
import xmldsig as ds
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
import base64
|
import base64
|
||||||
import random
|
import random
|
||||||
import os
|
import os
|
||||||
|
import copy
|
||||||
|
|
||||||
def get_xmlsec_binary():
|
def get_xmlsec_binary():
|
||||||
for path in os.environ["PATH"].split(":"):
|
for path in os.environ["PATH"].split(":"):
|
||||||
@@ -45,6 +47,103 @@ _TEST_ = True
|
|||||||
class SignatureError(Exception):
|
class SignatureError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def make_signed_instance(klass, spec, seccont, base64encode=False):
|
||||||
|
instance = make_instance(klass, spec, base64encode)
|
||||||
|
if "signature" in spec:
|
||||||
|
signed_xml = seccont.sign_statement_using_xmlsec("%s" % instance,
|
||||||
|
class_name(instance))
|
||||||
|
return create_class_from_xml_string(instance.__class__, signed_xml)
|
||||||
|
else:
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def _make_vals(val, klass, seccont, klass_inst=None, prop=None, part=False,
|
||||||
|
base64encode=False):
|
||||||
|
"""
|
||||||
|
Creates a class instance with a specified value, the specified
|
||||||
|
class instance may be a value on a property in a defined class instance.
|
||||||
|
|
||||||
|
:param val: The value
|
||||||
|
:param klass: The value class
|
||||||
|
:param klass_inst: The class instance which has a property on which
|
||||||
|
what this function returns is a value.
|
||||||
|
:param prop: The property which the value should be assigned to.
|
||||||
|
:param part: If the value is one of a possible list of values it should be
|
||||||
|
handled slightly different compared to if it isn't.
|
||||||
|
:return: Value class instance
|
||||||
|
"""
|
||||||
|
cinst = None
|
||||||
|
|
||||||
|
#print "make_vals(%s, %s)" % (val, klass)
|
||||||
|
|
||||||
|
if isinstance(val, dict):
|
||||||
|
cinst = signed_instance_factory(klass, val, seccont,
|
||||||
|
base64encode=base64encode)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
cinst = klass().set_text(val)
|
||||||
|
except ValueError, excp:
|
||||||
|
if not part:
|
||||||
|
cis = [_make_vals(sval, klass, seccont, klass_inst, prop,
|
||||||
|
True, base64encode) for sval in val]
|
||||||
|
setattr(klass_inst, prop, cis)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if part:
|
||||||
|
return cinst
|
||||||
|
else:
|
||||||
|
if cinst:
|
||||||
|
cis = [cinst]
|
||||||
|
setattr(klass_inst, prop, cis)
|
||||||
|
|
||||||
|
def signed_instance_factory(klass, ava, seccont, base64encode=False):
|
||||||
|
instance = klass()
|
||||||
|
|
||||||
|
for prop in instance.c_attributes.values():
|
||||||
|
#print "# %s" % (prop)
|
||||||
|
if prop in ava:
|
||||||
|
if isinstance(ava[prop], bool):
|
||||||
|
setattr(instance, prop, "%s" % ava[prop])
|
||||||
|
elif isinstance(ava[prop], int):
|
||||||
|
setattr(instance, prop, "%d" % ava[prop])
|
||||||
|
else:
|
||||||
|
setattr(instance, prop, ava[prop])
|
||||||
|
|
||||||
|
if "text" in ava:
|
||||||
|
instance.set_text(ava["text"], base64encode)
|
||||||
|
|
||||||
|
for prop, klassdef in instance.c_children.values():
|
||||||
|
#print "## %s, %s" % (prop, klassdef)
|
||||||
|
if prop in ava:
|
||||||
|
#print "### %s" % ava[prop]
|
||||||
|
if isinstance(klassdef, list): # means there can be a list of values
|
||||||
|
_make_vals(ava[prop], klassdef[0], seccont, instance, prop,
|
||||||
|
base64encode=base64encode)
|
||||||
|
else:
|
||||||
|
cis = _make_vals(ava[prop], klassdef, seccont, instance, prop,
|
||||||
|
True, base64encode)
|
||||||
|
setattr(instance, prop, cis)
|
||||||
|
|
||||||
|
if "extension_elements" in ava:
|
||||||
|
for item in ava["extension_elements"]:
|
||||||
|
instance.extension_elements.append(
|
||||||
|
ExtensionElement(item["tag"]).loadd(item))
|
||||||
|
|
||||||
|
if "extension_attributes" in ava:
|
||||||
|
for key, val in ava["extension_attributes"].items():
|
||||||
|
instance.extension_attributes[key] = val
|
||||||
|
|
||||||
|
if "signature" in ava:
|
||||||
|
signed_xml = seccont.sign_statement_using_xmlsec("%s" % instance,
|
||||||
|
class_name(instance))
|
||||||
|
return create_class_from_xml_string(instance.__class__, signed_xml)
|
||||||
|
else:
|
||||||
|
return instance
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
def create_id():
|
def create_id():
|
||||||
""" Create a string of 40 random characters from the set [a-p],
|
""" Create a string of 40 random characters from the set [a-p],
|
||||||
can be used as a unique identifier of objects.
|
can be used as a unique identifier of objects.
|
||||||
@@ -119,6 +218,8 @@ def _parse_xmlsec_output(output):
|
|||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
__DEBUG = 1
|
||||||
|
|
||||||
def verify_signature(enctext, xmlsec_binary, cert_file=None, cert_type="",
|
def verify_signature(enctext, xmlsec_binary, cert_file=None, cert_type="",
|
||||||
node_name=NODE_NAME, debug=False):
|
node_name=NODE_NAME, debug=False):
|
||||||
""" Verifies the signature of a XML document.
|
""" Verifies the signature of a XML document.
|
||||||
@@ -129,14 +230,14 @@ def verify_signature(enctext, xmlsec_binary, cert_file=None, cert_type="",
|
|||||||
:return: Boolean True if the signature was correct otherwise False.
|
:return: Boolean True if the signature was correct otherwise False.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_, fil = make_temp("%s" % enctext, decode=False)
|
_, fil = make_temp(enctext, decode=False)
|
||||||
|
|
||||||
com_list = [xmlsec_binary, "--verify",
|
com_list = [xmlsec_binary, "--verify",
|
||||||
"--pubkey-cert-%s" % cert_type, cert_file,
|
"--pubkey-cert-%s" % cert_type, cert_file,
|
||||||
"--id-attr:%s" % ID_ATTR,
|
"--id-attr:%s" % ID_ATTR,
|
||||||
node_name, fil]
|
node_name, fil]
|
||||||
|
|
||||||
if debug:
|
if __DEBUG:
|
||||||
try:
|
try:
|
||||||
print " ".join(com_list)
|
print " ".join(com_list)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@@ -151,7 +252,7 @@ def verify_signature(enctext, xmlsec_binary, cert_file=None, cert_type="",
|
|||||||
output = Popen(com_list, stderr=PIPE).communicate()[1]
|
output = Popen(com_list, stderr=PIPE).communicate()[1]
|
||||||
verified = _parse_xmlsec_output(output)
|
verified = _parse_xmlsec_output(output)
|
||||||
|
|
||||||
if debug:
|
if __DEBUG:
|
||||||
print output
|
print output
|
||||||
print os.stat(cert_file)
|
print os.stat(cert_file)
|
||||||
print "Verify result: '%s'" % (verified,)
|
print "Verify result: '%s'" % (verified,)
|
||||||
@@ -228,7 +329,9 @@ class SecurityContext(object):
|
|||||||
""" Verifies the signature of a XML document.
|
""" Verifies the signature of a XML document.
|
||||||
|
|
||||||
:param enctext: The XML document as a string
|
:param enctext: The XML document as a string
|
||||||
:param der_file: The public key that was used to sign the document
|
:param cert_file: The public key that was used to sign the document
|
||||||
|
:param cert_type: The file type of the certificate
|
||||||
|
:param node_name: The name of the class that is signed
|
||||||
:return: Boolean True if the signature was correct otherwise False.
|
:return: Boolean True if the signature was correct otherwise False.
|
||||||
"""
|
"""
|
||||||
if not cert_file:
|
if not cert_file:
|
||||||
@@ -236,8 +339,41 @@ class SecurityContext(object):
|
|||||||
cert_type = self.cert_type
|
cert_type = self.cert_type
|
||||||
|
|
||||||
return verify_signature(enctext, self.xmlsec, cert_file, cert_type,
|
return verify_signature(enctext, self.xmlsec, cert_file, cert_type,
|
||||||
node_name, True)
|
node_name, self.debug)
|
||||||
|
|
||||||
|
def _check_signature(self, decoded_xml, item, node_name=NODE_NAME):
|
||||||
|
#print item
|
||||||
|
try:
|
||||||
|
issuer = item.issuer.text.strip()
|
||||||
|
except AttributeError:
|
||||||
|
issuer = None
|
||||||
|
|
||||||
|
if self.metadata:
|
||||||
|
certs = self.metadata.certs(issuer)
|
||||||
|
else:
|
||||||
|
certs = []
|
||||||
|
|
||||||
|
if not certs:
|
||||||
|
print "==== Certs from instance ===="
|
||||||
|
certs = [make_temp("%s" % cert, ".der") \
|
||||||
|
for cert in cert_from_instance(item)]
|
||||||
|
else:
|
||||||
|
print "==== Certs from metadata ==== %s: %s ====" % (issuer,certs)
|
||||||
|
|
||||||
|
if not certs:
|
||||||
|
raise SignatureError("Missing signing certificate")
|
||||||
|
|
||||||
|
verified = False
|
||||||
|
for _, der_file in certs:
|
||||||
|
if self.verify_signature(decoded_xml, der_file, "der", node_name):
|
||||||
|
verified = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not verified:
|
||||||
|
raise SignatureError("Failed to verify signature")
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
def correctly_signed_authn_request(self, decoded_xml, must=False):
|
def correctly_signed_authn_request(self, decoded_xml, must=False):
|
||||||
""" Check if a request is correctly signed, if we have metadata for
|
""" Check if a request is correctly signed, if we have metadata for
|
||||||
the SP that sent the info use that, if not use the key that are in
|
the SP that sent the info use that, if not use the key that are in
|
||||||
@@ -255,30 +391,8 @@ class SecurityContext(object):
|
|||||||
raise SignatureError("Missing must signature")
|
raise SignatureError("Missing must signature")
|
||||||
else:
|
else:
|
||||||
return request
|
return request
|
||||||
|
|
||||||
issuer = request.issuer.text.strip()
|
|
||||||
|
|
||||||
if self.metadata:
|
return self._check_signature( decoded_xml, request )
|
||||||
certs = self.metadata.certs(issuer)
|
|
||||||
else:
|
|
||||||
certs = []
|
|
||||||
|
|
||||||
if not certs:
|
|
||||||
certs = [make_temp("%s" % cert, ".der") \
|
|
||||||
for cert in cert_from_instance(request)]
|
|
||||||
if not certs:
|
|
||||||
raise SignatureError("Missing signing certificate")
|
|
||||||
|
|
||||||
verified = False
|
|
||||||
for _, der_file in certs:
|
|
||||||
if verify_signature(self.xmlsec, decoded_xml, der_file):
|
|
||||||
verified = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not verified:
|
|
||||||
raise SignatureError("Failed to verify signature")
|
|
||||||
|
|
||||||
return request
|
|
||||||
|
|
||||||
def correctly_signed_response(self, decoded_xml, must=False):
|
def correctly_signed_response(self, decoded_xml, must=False):
|
||||||
""" Check if a instance is correctly signed, if we have metadata for
|
""" Check if a instance is correctly signed, if we have metadata for
|
||||||
@@ -303,34 +417,8 @@ class SecurityContext(object):
|
|||||||
else:
|
else:
|
||||||
if self.debug:
|
if self.debug:
|
||||||
self.log.debug("signed")
|
self.log.debug("signed")
|
||||||
|
|
||||||
issuer = assertion.issuer.text.strip()
|
|
||||||
|
|
||||||
if self.debug:
|
self._check_signature( decoded_xml, assertion )
|
||||||
self.log.debug("issuer: %s" % issuer)
|
|
||||||
|
|
||||||
if self.metadata:
|
|
||||||
certs = self.metadata.certs(issuer)
|
|
||||||
else:
|
|
||||||
certs = []
|
|
||||||
|
|
||||||
if self.debug:
|
|
||||||
self.log.debug("metadata certs: %s" % certs)
|
|
||||||
|
|
||||||
if not certs:
|
|
||||||
certs = [make_temp("%s" % cert, ".der") \
|
|
||||||
for cert in cert_from_instance(assertion)]
|
|
||||||
if not certs:
|
|
||||||
raise SignatureError("Missing certificate")
|
|
||||||
|
|
||||||
verified = False
|
|
||||||
for _, der_file in certs:
|
|
||||||
if self.verify_signature(decoded_xml, der_file, "der"):
|
|
||||||
verified = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not verified:
|
|
||||||
raise SignatureError("Could not verify")
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -395,7 +483,7 @@ PRE_SIGNATURE = {
|
|||||||
},
|
},
|
||||||
"reference": {
|
"reference": {
|
||||||
# must be replace by a uriref based on the assertion ID
|
# must be replace by a uriref based on the assertion ID
|
||||||
"uri": "#%s",
|
"uri": None,
|
||||||
"transforms": {
|
"transforms": {
|
||||||
"transform": [{
|
"transform": [{
|
||||||
"algorithm": ds.TRANSFORM_ENVELOPED,
|
"algorithm": ds.TRANSFORM_ENVELOPED,
|
||||||
@@ -417,7 +505,7 @@ PRE_SIGNATURE = {
|
|||||||
"signature_value": None,
|
"signature_value": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def pre_signature_part(ident):
|
def pre_signature_part(ident, public_key=None):
|
||||||
"""
|
"""
|
||||||
If an assertion is to be signed the signature part has to be preset
|
If an assertion is to be signed the signature part has to be preset
|
||||||
with which algorithms to be used, this function returns such a
|
with which algorithms to be used, this function returns such a
|
||||||
@@ -425,9 +513,17 @@ def pre_signature_part(ident):
|
|||||||
|
|
||||||
:param ident: The identifier of the assertion, so you know which assertion
|
:param ident: The identifier of the assertion, so you know which assertion
|
||||||
was signed
|
was signed
|
||||||
|
:param public_key: The base64 part of a PEM file
|
||||||
:return: A preset signature part
|
:return: A preset signature part
|
||||||
"""
|
"""
|
||||||
|
|
||||||
presig = PRE_SIGNATURE
|
presig = copy.deepcopy(PRE_SIGNATURE)
|
||||||
presig["signed_info"]["reference"]["uri"] = "#%s" % ident
|
presig["signed_info"]["reference"]["uri"] = "#%s" % ident
|
||||||
|
if public_key:
|
||||||
|
presig["key_info"] = {
|
||||||
|
"x509_data": {
|
||||||
|
"x509_certificate": public_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return presig
|
return presig
|
||||||
|
|||||||
Reference in New Issue
Block a user