-
-
Save joba-1/6b2fe7294ebda3f6a5d058c6fd9ea44a to your computer and use it in GitHub Desktop.
Upload a TLS key and cert to a FRITZ!Box, in pretty Python
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
#!/usr/bin/env python | |
""" | |
Upload a TLS key and cert to a FRITZ!Box, in pretty Python | |
Copyright (C) 2018 Olivier Mehani <[email protected]> | |
This program is free software; you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation; either version 2 of the License, or | |
(at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
""" | |
from hashlib import md5 | |
import requests | |
import re | |
class FritzBoxAuthService: | |
'''A service to authenticate and retrieve a session ID from a FRITZ!Box | |
Based on hsmade/get_sid.py [0]. | |
[0] https://gist.github.com/hsmade/b9e8aed176be28abe7a2e5bdbec52a8d''' | |
INVALID_SID = '0000000000000000' | |
@classmethod | |
def password_auth(cls, host, username, password): | |
'''Authenticate using a password to get a token, returns a SID''' | |
challenge = cls.get_challenge(host) | |
token = cls.compute_token(challenge, password) | |
return cls.token_auth(host, username, token) | |
@classmethod | |
def get_challenge(cls, host): | |
'''Get the challenge, do very ugly xml parsing''' | |
return requests.get('http://{host}/login_sid.lua'.format(host=host)).content.split('Challenge>')[1].split('<')[0] | |
@classmethod | |
def compute_token(cls, challenge, password): | |
'''Calculate the response token''' | |
hash = md5( | |
'{challenge}-{password}'.format( | |
challenge=challenge, | |
password=password, | |
).encode('utf16')[2:] | |
).hexdigest() | |
token = '{challenge}-{hash}'.format( | |
challenge=challenge, | |
hash=hash, | |
) | |
return token | |
@classmethod | |
def token_auth(cls, host, username, token): | |
'''Authenticate using a token, returns a SID''' | |
response = requests.get('http://{host}/login_sid.lua?sid={sid}username={username}&response={token}'.format( | |
host=host, | |
sid=cls.INVALID_SID, | |
username=username, | |
token=token, | |
)) | |
sid = response.content.split('<SID>')[1].split('<')[0] | |
if sid == cls.INVALID_SID: | |
raise FritzBoxLoginException('Failed to get valid SID') | |
return sid | |
class FritzBoxLoginException(Exception): | |
pass | |
class FritzBoxCertService: | |
'''Service allowing to upload a TLS key and certificate to a FRITZ!Box | |
With inspiration from wikrie/fritzbox-cert-update.sh [0]. | |
[0] https://gist.github.com/wikrie/f1d5747a714e0a34d0582981f7cb4cfb''' | |
@classmethod | |
def upload_key_cert(cls, host, sid, key, cert, key_password=None): | |
'''Upload the key and certificate using a valid SID''' | |
params = { | |
'sid': sid, | |
} | |
if key_password is not None: | |
# XXX: If it is unconditionally included, even if empty, | |
# this seems to throw the upload off. Could also be an ordering issue, | |
# as request puts this parameters first, before the sid | |
params['BoxCertPassword'] = key_password | |
certfile = { | |
'BoxCertImportFile': ( | |
'BoxCert.pem', | |
'{key}{cert}'.format( | |
key=key, | |
cert=cert | |
), | |
'application/x-x509-ca-cert', | |
), | |
} | |
response = requests.post( | |
'http://{host}/cgi-bin/firmwarecfg'.format(host=host), | |
data=params, | |
files=certfile | |
) | |
if response.status_code != 200: | |
raise FritzBoxCertUploadException('Failed to upload certificate') | |
for line in response.text.split('\n'): | |
if re.search('SSL', line): | |
return line | |
elif re.search('ErrorMsg', line): | |
raise FritzBoxCertUploadException( | |
line.split('"ErrorMsg">')[1].split('</')[0] | |
) | |
raise FritzBoxCertUploadException('Uploaded certificate, but the FRITZ!Box did not acknowledge the attempt') | |
class FritzBoxCertUploadException(Exception): | |
pass | |
if __name__ == '__main__': | |
import argparse | |
from getpass import getpass | |
import sys | |
def parse_arguments(): | |
LE_PATH='/etc/letsencrypt/live/{domain}' | |
parser = argparse.ArgumentParser(description='Upload Let\'s Encrypt certificate to FRITZ!Box') | |
parser.add_argument('-H', '--host', default='fritz.box') | |
parser.add_argument('-U', '--username', default='') | |
parser.add_argument('-p', '--password', default='') | |
parser.add_argument('-P', '--ask-password', default=False) | |
parser.add_argument('-d', '--domain', | |
help='Look for Let\'s Encrypt keys in ' + LE_PATH.format(domain="DOMAIN")) | |
parser.add_argument('-K', '--key-file') | |
parser.add_argument('-f', '--key-passphrase', default=None) | |
parser.add_argument('-F', '--ask-key-passphrase', default=False) | |
parser.add_argument('-C', '--cert-file') | |
args = vars(parser.parse_args()) | |
if args['ask_password']: | |
args['password'] = getpass('Password for {username}@{host}: '.format( | |
username=args['username'], | |
host=args['host'] | |
)) | |
if args['domain'] is not None: | |
certpath = LE_PATH.format(domain=args['domain']) | |
args['key_file'] = '{certpath}/privkey.pem'.format(certpath=certpath) | |
args['cert_file'] = '{certpath}/fullchain.pem'.format(certpath=certpath) | |
if args['key_file'] is None or args['cert_file'] is None: | |
sys.exit(1) | |
if args['ask_key_passphrase']: | |
args['key_passphrase'] = getpass('Passphrase for {key}: '.format( | |
key=args['key_file'] | |
)) | |
return args | |
args = parse_arguments() | |
sid = FritzBoxAuthService.password_auth( | |
args['host'], | |
args['username'], | |
args['password'] | |
) | |
with open(args['key_file']) as key: | |
with open(args['cert_file']) as cert: | |
result = FritzBoxCertService.upload_key_cert(args['host'], | |
sid, | |
key.read(), | |
cert.read(), | |
args['key_passphrase']) | |
print(result) |
Using python3, replacing "content" with "text" on both lines 39 and 65, and adding the missing "&" between {sid} and username in line 59 worked for me on a Fritzbox 6590 with Fritz!OS 7.12.
Thanks a lot for the script!
Thanks for the feedback
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
for some reason, this does not work for me. I use the bash script in the other gist now