diff --git a/.gitignore b/.gitignore index 30f0e05e896f8273ad7e982e67bbbace82ca86d9..6b99e4e745b949ea523a78580ce80111859315e7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ meta.key hsm.crt hsm.der *.xml -mdserver.yaml +mdsigner.yaml mdproxy.yaml diff --git a/README.md b/README.md index a9c0b0a452ec259db36977605dfd59a9fdea10ef..90105aee2fba9bfb2643c3ffe0547f0d67e1f498 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Alternate MDX research project - Download metadata file(s) - Run one or more of the tools below -## ```mdsigner.py [mdfile] [mdfile] [mdfile] ...``` +## ```mdwriter.py [mdfile] [mdfile] [mdfile] ...``` Reads source metadata file(s) and outputs them signed to filesystem -## ```mdserver.py``` +## ```mdsigner.py``` Starts a metadata signer server. Reads source metadata files(s) from mdsigner.yaml configuration, see example. Reloads metadata on inotify CLOSE_WRITE of metadata file. @@ -23,12 +23,12 @@ Serves and caches signed by realm signer from memory, on request ## ```mdproxy.py``` Reads config from mdproxy.yaml configuration, see example. -Caches signed and cached ```mdserver.py``` metadata requests +Caches signed and cached ```mdsigner.py``` metadata requests ## Queries MDQ Queries can then be pointed at -- ```http://mdserver:5001/<realm>/entities/<entityid>``` +- ```http://mdsigner:5001/<realm>/entities/<entityid>``` - ```http://mdproxy:5002/<realm>/entities/<entityid>``` ## Bootstrap softHSM2 diff --git a/mdserver.py b/mdserver.py deleted file mode 100755 index f76b9d845a1c9e377c18a0e4cff4c2bb2574bd5a..0000000000000000000000000000000000000000 --- a/mdserver.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python -from utils import read_config, Realm, Server -from flask import Flask, Response -from datetime import datetime -from dateutil import tz -from email.utils import formatdate -from time import mktime - -import logging -log = logging.getLogger('werkzeug') -log.setLevel(logging.ERROR) - -config = read_config('mdserver.yaml') -app = Flask(__name__) -server = Server() - - -@app.route('/<realm>/entities', - strict_slashes=False, - methods=['GET']) -def serve_all(realm): - print(f"all in {realm}") - response = Response() - response.headers['Content-Type'] = "application/samlmetadata+xml" - response.headers['Content-Disposition'] = "filename = \"metadata.xml\"" - - data = server[realm].all_entities() - response.data = data.md - max_age = int((data.valid_until - - datetime.now(tz.tzutc())).total_seconds()) - - response.headers['Cache-Control'] = f"max-age={max_age}" - response.headers['Last-Modified'] = formatdate( - timeval=mktime(data.last_modified.timetuple()), - localtime=False, - usegmt=True) - return response - - -@app.route('/<realm>/entities/<path:entity_id>', - strict_slashes=False, - methods=['GET']) -def serve_one(realm, entity_id): - print(f"entity_id: {entity_id}") - response = Response() - response.headers['Content-Type'] = "application/samlmetadata+xml" - response.headers['Content-Disposition'] = "filename = \"metadata.xml\"" - - try: - data = server[realm][entity_id] - response.data = data.md - max_age = data.max_age - last_modified = data.last_modified - except Exception: - response.data = "No valid metadata\n" - response.headers['Content-type'] = "text/html" - response.status = 404 - max_age = 60 - last_modified = datetime.now(tz.tzutc()) - - response.headers['Cache-Control'] = f"max-age={max_age}" - response.headers['Last-Modified'] = formatdate( - timeval=mktime(last_modified.timetuple()), - localtime=False, - usegmt=True) - return response - - -for realm, values in config.items(): - print(f"realm: {realm}") - location = values['metadir'] - signer = values['signer'] - server[realm] = Realm(location, signer) - -if __name__ == "__main__": - app.run(host='127.0.0.1', port=5001, debug=False) diff --git a/mdsigner.py b/mdsigner.py index 7c3e160e9dca64a96264c88b4a42b3afbc785bf2..11b06149738ca91d25eb8c8c8472f6addcec2811 100755 --- a/mdsigner.py +++ b/mdsigner.py @@ -1,52 +1,76 @@ #!/usr/bin/env python -import sys -import copy -from concurrent.futures import ThreadPoolExecutor +from utils import read_config, Realm, Server +from flask import Flask, Response +from datetime import datetime +from dateutil import tz +from email.utils import formatdate +from time import mktime -from lxml import etree as ET -# import traceback +import logging +log = logging.getLogger('werkzeug') +log.setLevel(logging.ERROR) -from utils import hasher -from signers import Signers +config = read_config('mdsigner.yaml') +app = Flask(__name__) +server = Server() -# Find all IdP's in edugain metadata -idps = [] -success = 0 -failed = 0 -maxthreads = 8 -signer = Signers('normal_signer') +@app.route('/<realm>/entities', + strict_slashes=False, + methods=['GET']) +def serve_all(realm): + print(f"all in {realm}") + response = Response() + response.headers['Content-Type'] = "application/samlmetadata+xml" + response.headers['Content-Disposition'] = "filename = \"metadata.xml\"" + + data = server[realm].all_entities() + response.data = data.md + max_age = int((data.valid_until - + datetime.now(tz.tzutc())).total_seconds()) + + response.headers['Cache-Control'] = f"max-age={max_age}" + response.headers['Last-Modified'] = formatdate( + timeval=mktime(data.last_modified.timetuple()), + localtime=False, + usegmt=True) + return response + + +@app.route('/<realm>/entities/<path:entity_id>', + strict_slashes=False, + methods=['GET']) +def serve_one(realm, entity_id): + print(f"entity_id: {entity_id}") + response = Response() + response.headers['Content-Type'] = "application/samlmetadata+xml" + response.headers['Content-Disposition'] = "filename = \"metadata.xml\"" -def sign(xml, name): - global success, failed, cert, key - # print("Signer") try: - sha1 = hasher(name) - signed = signer(xml) - out = ET.tostring(signed, pretty_print=True).decode() - # XMLVerifier().verify(out, x509_cert=cert) - with open(f'output/{sha1}.xml', 'w') as f: - f.write(out) - success += 1 - except Exception as e: - print(name) - print(f" {e}") - # traceback.print_exc() - failed += 1 - - -with ThreadPoolExecutor(max_workers=maxthreads) as executor: - for mdfile in sys.argv[1:]: - tree = ET.ElementTree(file=mdfile) - root = tree.getroot() - ns = copy.deepcopy(root.nsmap) - ns['xml'] = 'http://www.w3.org/XML/1998/namespace' - - for idp in root.findall('md:EntityDescriptor', ns): - entityID = idp.attrib.get('entityID', 'none') - if entityID not in idps: - idps.append(entityID) - executor.submit(sign, idp, entityID) - -print(f"Succeeded: {success}") -print(f"Failed: {failed}") + data = server[realm][entity_id] + response.data = data.md + max_age = data.max_age + last_modified = data.last_modified + except Exception: + response.data = "No valid metadata\n" + response.headers['Content-type'] = "text/html" + response.status = 404 + max_age = 60 + last_modified = datetime.now(tz.tzutc()) + + response.headers['Cache-Control'] = f"max-age={max_age}" + response.headers['Last-Modified'] = formatdate( + timeval=mktime(last_modified.timetuple()), + localtime=False, + usegmt=True) + return response + + +for realm, values in config.items(): + print(f"realm: {realm}") + location = values['metadir'] + signer = values['signer'] + server[realm] = Realm(location, signer) + +if __name__ == "__main__": + app.run(host='127.0.0.1', port=5001, debug=False) diff --git a/mdserver.service b/mdsigner.service similarity index 66% rename from mdserver.service rename to mdsigner.service index affb8ef4837eb6a3fe8bc8a4291da032276e064c..8de380e4fd581f246d94e1ffe6598bd99f065128 100644 --- a/mdserver.service +++ b/mdsigner.service @@ -1,15 +1,15 @@ [Unit] -Description=MDServer +Description=MDSigner After=syslog.target network.target [Service] Type=simple WorkingDirectory=/opt/alternate-mdx -ExecStart=/opt/alternate-mdx/bin/python -u mdserver.py +ExecStart=/opt/alternate-mdx/bin/python -u mdsigner.py ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartSec=10 -SyslogIdentifier=mdserver +SyslogIdentifier=mdsigner [Install] WantedBy=multi-user.target diff --git a/mdwriter.py b/mdwriter.py new file mode 100755 index 0000000000000000000000000000000000000000..7c3e160e9dca64a96264c88b4a42b3afbc785bf2 --- /dev/null +++ b/mdwriter.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +import sys +import copy +from concurrent.futures import ThreadPoolExecutor + +from lxml import etree as ET +# import traceback + +from utils import hasher +from signers import Signers + +# Find all IdP's in edugain metadata +idps = [] +success = 0 +failed = 0 +maxthreads = 8 +signer = Signers('normal_signer') + + +def sign(xml, name): + global success, failed, cert, key + # print("Signer") + try: + sha1 = hasher(name) + signed = signer(xml) + out = ET.tostring(signed, pretty_print=True).decode() + # XMLVerifier().verify(out, x509_cert=cert) + with open(f'output/{sha1}.xml', 'w') as f: + f.write(out) + success += 1 + except Exception as e: + print(name) + print(f" {e}") + # traceback.print_exc() + failed += 1 + + +with ThreadPoolExecutor(max_workers=maxthreads) as executor: + for mdfile in sys.argv[1:]: + tree = ET.ElementTree(file=mdfile) + root = tree.getroot() + ns = copy.deepcopy(root.nsmap) + ns['xml'] = 'http://www.w3.org/XML/1998/namespace' + + for idp in root.findall('md:EntityDescriptor', ns): + entityID = idp.attrib.get('entityID', 'none') + if entityID not in idps: + idps.append(entityID) + executor.submit(sign, idp, entityID) + +print(f"Succeeded: {success}") +print(f"Failed: {failed}")