#!/usr/bin/env python

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# codesigning differs significantly between mozilla::pkix and
# classic NSS that actual testing on it is not very useful
eku_values = { 'SA': "serverAuth",
               'CA': "clientAuth",
               #'CS': "codeSigning",
               'EP': "emailProtection",
               'TS': "timeStamping",
               'NS': "nsSGC", # Netscape Server Gated Crypto.
               'OS': "OCSPSigning"
             }

cert_usages = [ "certificateUsageSSLClient",
                "certificateUsageSSLServer",
                "certificateUsageSSLCA",
                "certificateUsageEmailSigner",
                "certificateUsageEmailRecipient",
                #"certificateUsageObjectSigner",
                "certificateUsageStatusResponder"
              ]

js_file_header = """//// AUTOGENERATED FILE, DO NOT EDIT
// -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

"use strict";

do_get_profile(); // must be called before getting nsIX509CertDB
const certdb = Cc["@mozilla.org/security/x509certdb;1"]
                 .getService(Ci.nsIX509CertDB);

function cert_from_file(filename) {
  return constructCertFromFile(`test_cert_eku/${filename}.pem`);
}

function load_cert(cert_name, trust_string) {
  addCertFromFile(certdb, `test_cert_eku/${cert_name}.pem`, trust_string);
  return cert_from_file(cert_name);
}

function run_test() {
  load_cert("ca", "CT,CT,CT");
"""

js_file_footer = """}
"""

def gen_int_js_output(int_string):
    expectedResult = "SEC_ERROR_INADEQUATE_CERT_TYPE"
    # For a certificate to verify successfully as a SSL CA, it must either
    # have no EKU or have the Server Auth or Netscape Server Gated Crypto
    # usage (the second of which is deprecated but currently supported for
    # compatibility purposes).
    if ("NONE" in int_string or "SA" in int_string or "NS" in int_string):
      expectedResult = "PRErrorCodeSuccess"
    return ("  checkCertErrorGeneric(certdb, load_cert('" + int_string +
            "', ',,'), " + expectedResult + ", certificateUsageSSLCA);\n")

def single_test_output(ee_name, cert_usage, error):
    return ("  checkCertErrorGeneric(certdb, cert_from_file('" + ee_name +
            "'), " + error + ", " + cert_usage + ");\n")

def usage_to_abbreviation(usage):
    if usage is "certificateUsageStatusResponder":
        return "OS"
    if usage is "certificateUsageSSLServer":
        return "SA"
    if usage is "certificateUsageSSLClient":
        return "CA"
    if (usage is "certificateUsageEmailSigner" or
        usage is "certificateUsageEmailRecipient"):
        return "EP"
    raise Exception("unsupported usage: " + usage)

# In general, for a certificate to be compatible with a usage, it must either
# have no EKU at all or that usage must be present in its EKU extension.
def has_compatible_eku(name_string, usage_abbreviation):
    return ("NONE" in name_string or usage_abbreviation in name_string)

def gen_ee_js_output(int_string, ee_string, cert_usage, ee_name):
    if cert_usage is "certificateUsageSSLCA":
        # since none of these are CA certs (BC) these should all fail
        return single_test_output(ee_name, cert_usage,
                                  "SEC_ERROR_INADEQUATE_KEY_USAGE")

    usage_abbreviation = usage_to_abbreviation(cert_usage)
    if cert_usage is "certificateUsageStatusResponder":
        # For the Status Responder usage, the OSCP Signing usage must be
        # present in the end-entity's EKU extension (i.e. if the extension
        # is not present, the cert is not compatible with this usage).
        if "OS" not in ee_string:
            return single_test_output(ee_name, cert_usage,
                                      "SEC_ERROR_INADEQUATE_CERT_TYPE")
        if not has_compatible_eku(int_string, usage_abbreviation):
            return single_test_output(ee_name, cert_usage,
                                      "SEC_ERROR_INADEQUATE_CERT_TYPE")
        return single_test_output(ee_name, cert_usage, "PRErrorCodeSuccess")

    # If the usage isn't Status Responder, if the end-entity certificate has
    # the OCSP Signing usage in its EKU, it is not valid for any other usage.
    if "OS" in ee_string:
        return single_test_output(ee_name, cert_usage,
                                  "SEC_ERROR_INADEQUATE_CERT_TYPE")

    if cert_usage is "certificateUsageSSLServer":
        if not has_compatible_eku(ee_string, usage_abbreviation):
            return single_test_output(ee_name, cert_usage,
                                      "SEC_ERROR_INADEQUATE_CERT_TYPE")
        # If the usage is SSL Server, the intermediate certificate must either
        # have no EKU extension or it must have the Server Auth or Netscape
        # Server Gated Crypto (deprecated but allowed for compatibility).
        if ("SA" not in int_string and "NONE" not in int_string and
            "NS" not in int_string):
            return single_test_output(ee_name, cert_usage,
                                      "SEC_ERROR_INADEQUATE_CERT_TYPE")
        return single_test_output(ee_name, cert_usage, "PRErrorCodeSuccess")

    if not has_compatible_eku(ee_string, usage_abbreviation):
        return single_test_output(ee_name, cert_usage,
                                  "SEC_ERROR_INADEQUATE_CERT_TYPE")
    if not has_compatible_eku(int_string, usage_abbreviation):
        return single_test_output(ee_name, cert_usage,
                                  "SEC_ERROR_INADEQUATE_CERT_TYPE")

    return single_test_output(ee_name, cert_usage, "PRErrorCodeSuccess")

def generate_test_eku():
    outmap = { "NONE" : ""}
    # add each one by itself
    for eku_name in (eku_values.keys()):
        outmap[eku_name] = eku_values[eku_name]
    # now combo of duples
    eku_names = sorted(eku_values.keys())
    for i in range(len(eku_names)):
        for j in range(i + 1, len(eku_names)):
            name = eku_names[i] + "_" + eku_names[j]
            outmap[name] = (eku_values[eku_names[i]] + "," +
                            eku_values[eku_names[j]])
    all_names = eku_names[0]
    all_values = eku_values[eku_names[0]]
    for i in range (1, len(eku_names)):
        all_names = all_names + "_" + eku_names[i]
        all_values = all_values + "," + eku_values[eku_names[i]]
    outmap[all_names] = all_values
    return outmap

def generate_certspec_file(issuer_name, name, extensions_list):
    filename = name + ".pem.certspec"
    with open(filename, "w") as f:
        f.write("issuer:%s\n" % issuer_name)
        f.write("subject:%s\n" % name)
        for extension in extensions_list:
            f.write("extension:%s\n" % extension)

def generate_certs():
    generate_certspec_file("ca", "ca", ["basicConstraints:cA,"])
    cert_list = ["ca.pem"]

    # now we do it again for valid basic constraints but strange eku/ku at the
    # intermediate layer
    eku_dict = generate_test_eku()
    print eku_dict
    for eku_name in (sorted(eku_dict.keys())):
        # Divide the tests into multiple files to avoid time outs
        js_outfile = open("../test_cert_eku-" + eku_name + ".js", "w")
        js_outfile.write(js_file_header)

        # generate int
        int_name = "int-EKU-" + eku_name
        int_eku = "extKeyUsage:" + eku_dict[eku_name]
        int_extensions_list = [
            "basicConstraints:cA,",
            "keyUsage:keyCertSign,digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment,keyAgreement,cRLSign"
        ]
        if eku_name != "NONE":
            int_extensions_list.append(int_eku)
        generate_certspec_file("ca", int_name, int_extensions_list)
        cert_list.append(int_name + ".pem")

        js_outfile.write("\n")
        js_outfile.write(gen_int_js_output(int_name))

        for ee_eku_name in (sorted(eku_dict.keys())):
            ee_base_name = "ee-EKU-" + ee_eku_name
            ee_name = ee_base_name + "-" + int_name
            ee_eku = "extKeyUsage:" + eku_dict[ee_eku_name]
            ee_extensions_list = [
                "basicConstraints:,",
                "keyUsage:digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment,keyAgreement"
            ]
            if ee_eku_name != "NONE":
                ee_extensions_list.append(ee_eku)
            generate_certspec_file(int_name, ee_name, ee_extensions_list)
            cert_list.append(ee_name + ".pem")

            for cert_usage in (cert_usages):
                js_outfile.write(gen_ee_js_output(int_name, ee_base_name,
                                 cert_usage, ee_name))

        js_outfile.write(js_file_footer)
        js_outfile.close()

    return cert_list

def generate_mozbuild_file(generated_cert_filenames):
    MOZ_BUILD_HEADER = """# AUTOGENERATED FILE, DO NOT EDIT
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

test_certificates = (
"""

    MOZ_BUILD_FOOTER = """)

for test_certificate in test_certificates:
    input_file = test_certificate + '.certspec'
    GENERATED_FILES += [test_certificate]
    props = GENERATED_FILES[test_certificate]
    props.script = '../pycert.py'
    props.inputs = [input_file, '!/config/buildid']
    TEST_HARNESS_FILES.xpcshell.security.manager.ssl.tests.unit.test_cert_eku += ['!%s' % test_certificate]
"""

    with open("moz.build", "w") as f:
        f.write(MOZ_BUILD_HEADER)
        for cert_filename in sorted(generated_cert_filenames):
            f.write("    '%s',\n" % cert_filename)
        f.write(MOZ_BUILD_FOOTER)

generated_cert_filenames = generate_certs()
generate_mozbuild_file(generated_cert_filenames)
