Blob Blame History Raw
# -*- coding: utf-8 -*-
#
# Generate OpenSSL configuration file for AusweisApp2 from settings found
# in the application's 'config.json' file.
#
# Copyright (c) 2020 Björn Esser <besser82@fedoraproject.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import json, sys


def constant(f):
    def fset(self, value):
        raise TypeError
    def fget(self):
        return f()
    return property(fget, fset)


class _Const(object):
    @constant
    def CONF_OPTIONS():
        return [
                   'ciphers',
                   'ellipticCurves',
                   'signatureAlgorithms',
               ]

    @constant
    def CONF_SECTIONS():
        return [
                   'tlsSettings',
                   'tlsSettingsPsk',
                   'tlsSettingsRemoteReader',
                   'tlsSettingsRemoteReaderPairing',
               ]

    @constant
    def DEFAULT_CIPHERS_TLS13():
        return [
                   'TLS_AES_256_GCM_SHA384',
                   'TLS_AES_128_GCM_SHA256',
               ]

    @constant
    def KEYSIZE_EC_OPTION():
        return 'Ec'

    @constant
    def KEYSIZE_OPTIONS():
        return [
                   'Rsa',
                   'Dsa',
                   'Dh',
               ]

    @constant
    def KEYSIZE_SECTIONS():
        return [
                   'minStaticKeySizes',
                   'minEphemeralKeySizes',
               ]

    @constant
    def TLS_VERSIONS():
        return {
                   'TlsV1_2': (2, 'TLSv1.2'),
                   'TlsV1_3': (3, 'TLSv1.3'),
               }


CONST = _Const()


def get_min_ssl_sec_level(json_data):
    sec_level = 0
    min_keysize = sys.maxsize
    min_ecsize = sys.maxsize
    for section in CONST.KEYSIZE_SECTIONS:
        if section in json_data:
            for option in CONST.KEYSIZE_OPTIONS:
                if option in json_data[section]:
                    if min_keysize > json_data[section][option]:
                        min_keysize = json_data[section][option]
            if CONST.KEYSIZE_EC_OPTION in json_data[section]:
                    if min_ecsize > json_data[section][CONST.KEYSIZE_EC_OPTION]:
                        min_ecsize = json_data[section][CONST.KEYSIZE_EC_OPTION]

    if min_keysize >=  1000 and min_ecsize >= 160:
        sec_level = 1
    if min_keysize >=  2000 and min_ecsize >= 224:
        sec_level = 2
    if min_keysize >=  3000 and min_ecsize >= 256:
        sec_level = 3
    if min_keysize >=  7000 and min_ecsize >= 384:
        sec_level = 4
    if min_keysize >= 15000 and min_ecsize >= 512:
        sec_level = 5

    return sec_level


def get_proto_ver(json_data):
    conf_dict = {
                    'minProtocolVersion': list(CONST.TLS_VERSIONS.keys())[-1],
                    'maxProtocolVersion': list(CONST.TLS_VERSIONS.keys())[0],
                }
    for section in CONST.CONF_SECTIONS:
        if section in json_data:
            if 'protocolVersion' in json_data[section]:
                have = conf_dict['minProtocolVersion']
                want = json_data[section]['protocolVersion']
                if CONST.TLS_VERSIONS[want][0] < CONST.TLS_VERSIONS[have][0]:
                    conf_dict['minProtocolVersion'] = want
                have = conf_dict['maxProtocolVersion']
                if CONST.TLS_VERSIONS[want][0] > CONST.TLS_VERSIONS[have][0]:
                    conf_dict['maxProtocolVersion'] = want

    return conf_dict


def get_ssl_cipher_config(json_data):
    conf_dict = dict.fromkeys(CONST.CONF_OPTIONS)
    for option in CONST.CONF_OPTIONS:
        conf_dict[option] = list()
    for section in CONST.CONF_SECTIONS:
        if section in json_data:
            for option in CONST.CONF_OPTIONS:
                if option in json_data[section]:
                    for value in json_data[section][option]:
                        if option == 'ciphers' and value.startswith('TLS_'):
                            if not 'ciphers_tls13' in conf_dict:
                                conf_dict['ciphers_tls13'] = list()
                            if not value in conf_dict['ciphers_tls13']:
                                conf_dict['ciphers_tls13'].append(value)
                        else:
                            if not value in conf_dict[option]:
                                conf_dict[option].append(value)

    return conf_dict


def print_config_file(conf_dict, sec_level):
    max_tls_proto = CONST.TLS_VERSIONS[conf_dict['maxProtocolVersion']][0]
    prelude = (
                  '# This application specific OpenSSL configuration enables all cipher',
                  '# algorithms, elliptic curves, and signature algorithms, which are',
                  '# needed for AusweisApp2 to provide full functionality to the end-user.',
                  '# The order of the algorithms in the list is of no importance, as the',
                  '# application chooses the algorithm used for a connection from a preset',
                  '# list, that is ordered in descending preference.  This configuration',
                  '# also limits the minimum and maximum cryptographic protocol versions',
                  '# to a range needed by AusweisApp2.',
                  '# The settings used to generate this file have been taken from the',
                  '# \'config.json\' file, which can be found in the same directory as this',
                  '# configuration file.',
                  '',
                  'openssl_conf = AusweisApp2_conf',
                  '',
                  '[AusweisApp2_conf]',
                  'ssl_conf = AusweisApp2_OpenSSL',
                  '',
                  '[AusweisApp2_OpenSSL]',
                  'alg_section = AusweisApp2_evp',
                  'system_default = AusweisApp2_ciphers',
                  '',
                  '[AusweisApp2_evp]',
                  'fips_mode = no',
                  '',
                  '[AusweisApp2_ciphers]',
              )
    print('%s' % '\n'.join(prelude))
    print('MinProtocol = %s' % (CONST.TLS_VERSIONS[conf_dict['minProtocolVersion']][1]))
    print('MaxProtocol = %s' % (CONST.TLS_VERSIONS[conf_dict['maxProtocolVersion']][1]))
    if max_tls_proto >= CONST.TLS_VERSIONS['TlsV1_3'][0]:
        if 'ciphers_tls13' in conf_dict:
            print('Cipherlist = %s' % (':'.join(conf_dict['ciphers_tls13'])))
        else:
            print('Cipherlist = %s' % (':'.join(CONST.DEFAULT_CIPHERS_TLS13)))
    print('CipherString = @SECLEVEL=%d:%s' % (sec_level, ':'.join(conf_dict['ciphers'])))
    print('Curves = %s' % (':'.join(conf_dict['ellipticCurves'])))
    print('SignatureAlgorithms = %s' % (':'.join(conf_dict['signatureAlgorithms'])))


def main():
    if not len(sys.argv) == 2:
        sys.exit('Usage: %s <path_to_config.json>' % sys.argv[0])

    with open(sys.argv[1], 'r') as conf_file:
        conf = json.load(conf_file)

    ssl_conf = get_proto_ver(conf)
    ssl_conf.update(get_ssl_cipher_config(conf))

    print_config_file(ssl_conf, get_min_ssl_sec_level(conf))


if __name__ == '__main__':
    main()