Skip to content

Commit

Permalink
fix #35: authn requests validation on multiple certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
peppelinux committed May 28, 2021
1 parent 1959ce1 commit ad8da48
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 56 deletions.
4 changes: 4 additions & 0 deletions build_pypi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

PROJ_NAME=$(ls | grep *.egg-info | sed -e 's/.egg-info//g') ; rm -R build/ dist/* *.egg-info ; pip uninstall $PROJ_NAME ; python setup.py build sdist

115 changes: 59 additions & 56 deletions src/spid_sp_test/authn_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,77 +195,80 @@ def test_xsd(self):
return self.is_ok(f'{self.__class__.__name__}.test_xsd')

def test_xmldsig(self):
cert = self.md.xpath(
certs = self.md.xpath(
'//SPSSODescriptor/KeyDescriptor[@use="signing"]'
'/KeyInfo/X509Data/X509Certificate/text()')

desc = cert
desc = certs
error_kwargs = dict(description = desc) if desc else {}

msg = ('The AuthnRequest MUST validate against XSD '
'and MUST have a valid signature')

if not cert:
if not certs:
self.handle_result('error', '-> '.join(
(msg, 'AuthnRequest Signature validation failed'),
(msg, 'AuthnRequest Signature validation failed: certificates are missing.'),
**error_kwargs)
)
return self.is_ok(f'{self.__class__.__name__}.test_xsd_and_xmldsig')
else:
cert = cert[0]

if self.IS_HTTP_REDIRECT:
with NamedTemporaryFile(suffix='.xml') as cert_file:
if cert[-1] != '\n':
cert += '\n'
cert_file.write(
f'-----BEGIN CERTIFICATE-----\n{cert}-----END CERTIFICATE-----'.encode()
)
cert_file.seek(0)
_sigalg = self.authn_request.get('SigAlg', "")
quoted_req = urllib.parse.quote_plus(self.authn_request['SAMLRequest'])
quoted_rs = urllib.parse.quote_plus(self.authn_request.get('RelayState') or "")
quoted_sigalg = urllib.parse.quote_plus(_sigalg)
authn_req = (f"SAMLRequest={quoted_req}&"
f"RelayState={quoted_rs}&"
f"SigAlg={quoted_sigalg}")

payload_file = NamedTemporaryFile(suffix='.xml')
payload_file.write(authn_req.encode())
payload_file.seek(0)

signature_file = NamedTemporaryFile(suffix='.sign')
signature_file.write(base64.b64decode(self.authn_request['Signature'].encode()))
signature_file.seek(0)

pubkey_file = NamedTemporaryFile(suffix='.crt')
x509_cert = subprocess.getoutput(
f'openssl x509 -in {cert_file.name} -noout -pubkey'
)
pubkey_file.write(x509_cert.encode())
pubkey_file.seek(0)

dgst = _sigalg.split('-')[-1]
signature = signature_file.name

ver_cmd = (f'openssl dgst -{dgst} '
f'-verify {pubkey_file.name} '
f'-signature {signature} {payload_file.name}')
exit_msg = subprocess.getoutput(ver_cmd)
error_kwargs['description'] = exit_msg
if 'Verified OK' in exit_msg:
is_valid = True
is_valid = False
for cert in certs:
if self.IS_HTTP_REDIRECT:
with NamedTemporaryFile(suffix='.xml') as cert_file:
if cert[-1] != '\n':
cert += '\n'
cert_file.write(
f'-----BEGIN CERTIFICATE-----\n{cert}-----END CERTIFICATE-----'.encode()
)
cert_file.seek(0)
_sigalg = self.authn_request.get('SigAlg', "")
quoted_req = urllib.parse.quote_plus(self.authn_request['SAMLRequest'])
quoted_rs = urllib.parse.quote_plus(self.authn_request.get('RelayState') or "")
quoted_sigalg = urllib.parse.quote_plus(_sigalg)
authn_req = (f"SAMLRequest={quoted_req}&"
f"RelayState={quoted_rs}&"
f"SigAlg={quoted_sigalg}")

payload_file = NamedTemporaryFile(suffix='.xml')
payload_file.write(authn_req.encode())
payload_file.seek(0)

signature_file = NamedTemporaryFile(suffix='.sign')
signature_file.write(base64.b64decode(self.authn_request['Signature'].encode()))
signature_file.seek(0)

pubkey_file = NamedTemporaryFile(suffix='.crt')
x509_cert = subprocess.getoutput(
f'openssl x509 -in {cert_file.name} -noout -pubkey'
)
pubkey_file.write(x509_cert.encode())
pubkey_file.seek(0)

dgst = _sigalg.split('-')[-1]
signature = signature_file.name

ver_cmd = (f'openssl dgst -{dgst} '
f'-verify {pubkey_file.name} '
f'-signature {signature} {payload_file.name}')
exit_msg = subprocess.getoutput(ver_cmd)
error_kwargs['description'] = exit_msg
if 'Verified OK' in exit_msg:
is_valid = True
else:
is_valid = False

else:
is_valid = False
# pyXMLSecurity allows to pass a certificate without store it on a file
backend = CryptoBackendXMLSecurity()
is_valid = backend.validate_signature(self.authn_request_decoded,
cert_file=cert,
cert_type='pem',
node_name=constants.NODE_NAME,
node_id=None)
if is_valid:
break

else:
# pyXMLSecurity allows to pass a certificate without store it on a file
backend = CryptoBackendXMLSecurity()
is_valid = backend.validate_signature(self.authn_request_decoded,
cert_file=cert,
cert_type='pem',
node_name=constants.NODE_NAME,
node_id=None)
self._assertTrue(is_valid,
'AuthnRequest Signature validation failed',
**error_kwargs)
Expand Down
7 changes: 7 additions & 0 deletions tests/authn/spid_django_wrong_signature.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0"?>
<samlp:AuthnRequest xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="id-Cf8lCSUJm74Eu24D7" Version="2.0" IssueInstant="2021-03-23T13:35:50Z" Destination="http://localhost:54321" ForceAuthn="true" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="http://localhost:8000/spid/acs/" AttributeConsumingServiceIndex="0"><saml:Issuer NameQualifier="http://localhost:8000/spid/metadata" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://localhost:8000/spid/metadata</saml:Issuer><ds:Signature Id="Signature1"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#id-Cf8lCSUJm74Eu24D7"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>/fDGHJmS4y04d8k1pxW7SHKDqLMFqstvK0iF2F4fXwg=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>XXVKfEYfzI28f3ML3DgDpNwhduycsNWDBZjHvQe5+37TCCz0cyQnepZ2CODHIBNG
6Sl+OX16vbp+RgxnSutmkES7dgHxncBpIk6o+KwGCFKrNGE+YcgJxzkI1xBKBGvE
X6xVlw/gvqrjzHRKrp8/1ORXwIPJl4JOSUmNhiTFAOA3c8vWjbg6TgBTuH4pDVoi
WpZamSc/MY5UcBawGTK5ZJIEnZEnyFlvoeE4XUSU3+TyNuPNeAJ9k7oFWR01bXXN
LtWx6PmVnPur031vhHKGlAOR7FzNHF1BUGRFJom/7SONCiTOj/dKFb..........
v0TSGrl81Sfx0lTwUXkOaw==</ds:SignatureValue><ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIDazCCAlOgAwIBAgIUd3m78rxZI604Wb78Ofm0/N/ijNEwDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTAyMTMxMTQ4MjVaFw0zMTAyMTExMTQ4MjVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIU8VNwN7pRxmP5LoELxanu9bL8+jIoQwRfJIA/NTFLSW7Znhp70IFIHxgbdRv5GeSA+EXVxaQz5lnsXXlxYnDX/oVXCaFxw+YI5Jm6qpw/if9+aSv3y1bsKp3zyGUkPiaLdfVFz/VuZZvz8izjATU1EKAT19XKTdgG/SjGgYczouSL5MguGd/oWnzn5TpyjGWk6NyV6yVHcqW2QoSzhfqOtqUedzRAC+u946ZQteNswcJuJWJ6+uyziLrrE6MphwP+ALyWkUDaV/7Yg2+ang8/z051XXFeckQ5L7Y/o60Ga53saWFVqk7f8wfE56kqnnNaPP5cio2YVdijsM4MixxAgMBAAGjUzBRMB0GA1UdDgQWBBSZSKqD+zrKer2AUm4JGZNYjKKqEjAfBgNVHSMEGDAWgBSZSKqD+zrKer2AUm4JGZNYjKKqEjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBGqk8+W0CaPoYMp56q1svtQsQh3KGz2mwEjEJyGfyGYaQ8cBKY9yeSn55Hq+xJPDUgBJr7kPMWOfttAEHJZk9z/Y4KpGEn3Z1FPIZKkxdItUqEGwI13YjV+gmC27nqmuk0qdfO6rWYL90198q7u6KO5hqRLXR7ljdPXmiZ3qHHkAvnACQHzGU1UIl5I+PBQE8GcJqOE3YbO6n3f+4Grsjp2BJ1BkAH7eWvKDGhrmS0AfYDNDFfd0aFVT74w3I8aPaZwyIz9+qYKkEhkJeaRgGCSGzKjcKxiurBcAvqxyIfpCdSOtN7Grw5Cp6Qd/jB46ZgTAturvWjLOqWHMULAdoq</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/><samlp:RequestedAuthnContext Comparison="minimum"><saml:AuthnContextClassRef>https://www.spid.gov.it/SpidL1</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp:AuthnRequest>
4 changes: 4 additions & 0 deletions tests/test_02_authn.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ def test_spid_express_no_relaystate():
es = run_cmd("spid_express_no_relaystate_redirect.url",
metadata = "spid_express_no_relaystate_metadata.xml")
assert es != 0

def test_django_post_wrong_signature():
es = run_cmd('spid_django_wrong_signature.xml')
assert es != 0

0 comments on commit ad8da48

Please sign in to comment.