#!/usr/bin/env python
# Copyright 2014-2019, The Tor Project, Inc
# See LICENSE for licensing information

# This script parses openssl headers to find ciphersuite names, determines
# which ones we should be willing to use as a server, and sorts them according
# to preference rules.
#
# Run it on all the files in your openssl include directory.

# Future imports for Python 2.7, mandatory in 3.0
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import re
import sys

EPHEMERAL_INDICATORS = [ "_EDH_", "_DHE_", "_ECDHE_" ]
BAD_STUFF = [ "_DES_40_", "MD5", "_RC4_", "_DES_64_",
              "_SEED_", "_CAMELLIA_", "_NULL",
              "_CCM_8", "_DES_", ]

# these never get #ifdeffed.
MANDATORY = [
    "TLS1_TXT_DHE_RSA_WITH_AES_256_SHA",
    "TLS1_TXT_DHE_RSA_WITH_AES_128_SHA",
]

def find_ciphers(filename):
    with open(filename) as f:
        for line in f:
            m = re.search(r'(?:SSL3|TLS1)_TXT_\w+', line)
            if m:
                yield m.group(0)

def usable_cipher(ciph):
    ephemeral = False
    for e in EPHEMERAL_INDICATORS:
        if e in ciph:
            ephemeral = True
    if not ephemeral:
        return False

    if "_RSA_" not in ciph:
        return False

    for b in BAD_STUFF:
        if b in ciph:
            return False
    return True

# All fields we sort on, in order of priority.
FIELDS = [ 'cipher', 'fwsec', 'mode',  'digest', 'bitlength' ]
# Map from sorted fields to recognized value in descending order of goodness
FIELD_VALS = { 'cipher' : [ 'AES', 'CHACHA20' ],
               'fwsec' : [ 'ECDHE', 'DHE' ],
               'mode' : [ 'POLY1305', 'GCM', 'CCM', 'CBC', ],
               'digest' : [ 'n/a', 'SHA384', 'SHA256', 'SHA', ],
               'bitlength' : [ '256', '128', '192' ],
}

class Ciphersuite(object):
    def __init__(self, name, fwsec, cipher, bitlength, mode, digest):
        if fwsec == 'EDH':
            fwsec = 'DHE'

        if mode in [ '_CBC3', '_CBC', '' ]:
            mode = 'CBC'
        elif mode == '_GCM':
            mode = 'GCM'

        self.name = name
        self.fwsec = fwsec
        self.cipher = cipher
        self.bitlength = bitlength
        self.mode = mode
        self.digest = digest

        for f in FIELDS:
            assert(getattr(self, f) in FIELD_VALS[f])

    def sort_key(self):
        return tuple(FIELD_VALS[f].index(getattr(self,f)) for f in FIELDS)


def parse_cipher(ciph):
    m = re.match('(?:TLS1|SSL3)_TXT_(EDH|DHE|ECDHE)_RSA(?:_WITH)?_(AES|DES)_(256|128|192)(|_CBC|_CBC3|_GCM)_(SHA|SHA256|SHA384)$', ciph)

    if m:
        fwsec, cipher, bits, mode, digest = m.groups()
        return Ciphersuite(ciph, fwsec, cipher, bits, mode, digest)

    m = re.match('(?:TLS1|SSL3)_TXT_(EDH|DHE|ECDHE)_RSA(?:_WITH)?_(AES|DES)_(256|128|192)_CCM', ciph)
    if m:
        fwsec, cipher, bits = m.groups()
        return Ciphersuite(ciph, fwsec, cipher, bits, "CCM", "n/a")

    m = re.match('(?:TLS1|SSL3)_TXT_(EDH|DHE|ECDHE)_RSA(?:_WITH)?_CHACHA20_POLY1305', ciph)
    if m:
        fwsec, = m.groups()
        return Ciphersuite(ciph, fwsec, "CHACHA20", "256", "POLY1305", "n/a")

    print("/* Couldn't parse %s ! */"%ciph)
    return None


ALL_CIPHERS = []

for fname in sys.argv[1:]:
    for c in find_ciphers(fname):
        if usable_cipher(c):
            parsed = parse_cipher(c)
            if parsed != None:
                ALL_CIPHERS.append(parsed)

ALL_CIPHERS.sort(key=Ciphersuite.sort_key)

indent = " "*7

for c in ALL_CIPHERS:
    if c is ALL_CIPHERS[-1]:
        colon = ''
    else:
        colon = ' ":"'

    if c.name in MANDATORY:
        print("%s/* Required */"%indent)
        print('%s%s%s'%(indent,c.name,colon))
    else:
        print("#ifdef %s"%c.name)
        print('%s%s%s'%(indent,c.name,colon))
        print("#endif")

print('%s;'%indent)

