Skip to content

Instantly share code, notes, and snippets.

@joba-1
Forked from shtrom/fritz_cert_upload.py
Created August 3, 2018 23:41
Show Gist options
  • Save joba-1/6b2fe7294ebda3f6a5d058c6fd9ea44a to your computer and use it in GitHub Desktop.
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
#!/usr/bin/env python
"""
Upload a TLS key and cert to a FRITZ!Box, in pretty Python
Copyright (C) 2018 Olivier Mehani <shtrom@ssji.net>
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)
@joba-1
Copy link
Author

joba-1 commented Oct 15, 2018

for some reason, this does not work for me. I use the bash script in the other gist now

@guillempages
Copy link

guillempages commented Mar 10, 2020

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!

@joba-1
Copy link
Author

joba-1 commented Sep 11, 2020

Thanks for the feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment