# coding: utf-8
from __future__ import unicode_literals, division, absolute_import, print_function
from base64 import b32encode
import os
import shutil
import tempfile

from .._asn1 import (
    Certificate as Asn1Certificate,
    ECDomainParameters,
    Integer,
    KeyExchangeAlgorithm,
    Null,
    PrivateKeyInfo,
    PublicKeyAlgorithm,
    PublicKeyInfo,
    RSAPublicKey,
)
from .._asymmetric import (
    _CertificateBase,
    _fingerprint,
    _parse_pkcs12,
    _PrivateKeyBase,
    _PublicKeyBase,
    _unwrap_private_key_info,
    parse_certificate,
    parse_private,
    parse_public,
)
from .._errors import pretty_message
from .._ffi import new, unwrap, bytes_from_buffer, buffer_from_bytes, deref, null, is_null, pointer_set
from ._security import Security, SecurityConst, handle_sec_error, osx_version_info
from ._core_foundation import CoreFoundation, CFHelpers, handle_cf_error
from .util import rand_bytes
from ..errors import AsymmetricKeyError, IncompleteAsymmetricKeyError, SignatureError
from .._pkcs1 import add_pss_padding, verify_pss_padding, remove_pkcs1v15_encryption_padding
from .._types import type_name, str_cls, byte_cls, int_types


__all__ = [
    'Certificate',
    'dsa_sign',
    'dsa_verify',
    'ecdsa_sign',
    'ecdsa_verify',
    'generate_pair',
    'load_certificate',
    'load_pkcs12',
    'load_private_key',
    'load_public_key',
    'parse_pkcs12',
    'PrivateKey',
    'PublicKey',
    'rsa_oaep_decrypt',
    'rsa_oaep_encrypt',
    'rsa_pkcs1v15_decrypt',
    'rsa_pkcs1v15_encrypt',
    'rsa_pkcs1v15_sign',
    'rsa_pkcs1v15_verify',
    'rsa_pss_sign',
    'rsa_pss_verify',
]


class PrivateKey(_PrivateKeyBase):
    """
    Container for the OS crypto library representation of a private key
    """

    sec_key_ref = None
    _public_key = None

    # A reference to the library used in the destructor to make sure it hasn't
    # been garbage collected by the time this object is garbage collected
    _lib = None

    def __init__(self, sec_key_ref, asn1):
        """
        :param sec_key_ref:
            A Security framework SecKeyRef value from loading/importing the
            key

        :param asn1:
            An asn1crypto.keys.PrivateKeyInfo object
        """

        self.sec_key_ref = sec_key_ref
        self.asn1 = asn1
        self._lib = CoreFoundation

    @property
    def public_key(self):
        """
        :return:
            A PublicKey object corresponding to this private key.
        """

        if self._public_key is None:
            cf_data_private = None
            try:
                # We export here so that Security.framework will fill in the EC
                # public key for us, instead of us having to compute it
                cf_data_private_pointer = new(CoreFoundation, 'CFDataRef *')
                result = Security.SecItemExport(self.sec_key_ref, 0, 0, null(), cf_data_private_pointer)
                handle_sec_error(result)
                cf_data_private = unwrap(cf_data_private_pointer)
                private_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_private)

                key = parse_private(private_key_bytes)

                if key.algorithm == 'rsa':
                    public_asn1 = PublicKeyInfo({
                        'algorithm': PublicKeyAlgorithm({
                            'algorithm': 'rsa',
                            'parameters': Null()
                        }),
                        'public_key': RSAPublicKey({
                            'modulus': key['private_key'].parsed['modulus'],
                            'public_exponent': key['private_key'].parsed['public_exponent'],
                        })
                    })

                elif key.algorithm == 'dsa':
                    params = key['private_key_algorithm']['parameters']
                    public_asn1 = PublicKeyInfo({
                        'algorithm': PublicKeyAlgorithm({
                            'algorithm': 'dsa',
                            'parameters': params.copy()
                        }),
                        'public_key': Integer(pow(
                            params['g'].native,
                            key['private_key'].parsed.native,
                            params['p'].native
                        ))
                    })

                elif key.algorithm == 'ec':
                    public_asn1 = PublicKeyInfo({
                        'algorithm': PublicKeyAlgorithm({
                            'algorithm': 'ec',
                            'parameters': ECDomainParameters(
                                name='named',
                                value=self.curve
                            )
                        }),
                        'public_key': key['private_key'].parsed['public_key'],
                    })

            finally:
                if cf_data_private:
                    CoreFoundation.CFRelease(cf_data_private)

            self._public_key = _load_key(public_asn1)

        return self._public_key

    @property
    def fingerprint(self):
        """
        Creates a fingerprint that can be compared with a public key to see if
        the two form a pair.

        This fingerprint is not compatible with fingerprints generated by any
        other software.

        :return:
            A byte string that is a sha256 hash of selected components (based
            on the key type)
        """

        if self._fingerprint is None:
            self._fingerprint = _fingerprint(self.asn1, load_private_key)
        return self._fingerprint

    def __del__(self):
        if self.sec_key_ref:
            self._lib.CFRelease(self.sec_key_ref)
            self._lib = None
            self.sec_key_ref = None


class PublicKey(_PublicKeyBase):
    """
    Container for the OS crypto library representation of a public key
    """

    sec_key_ref = None

    # A reference to the library used in the destructor to make sure it hasn't
    # been garbage collected by the time this object is garbage collected
    _lib = None

    def __init__(self, sec_key_ref, asn1):
        """
        :param sec_key_ref:
            A Security framework SecKeyRef value from loading/importing the
            key

        :param asn1:
            An asn1crypto.keys.PublicKeyInfo object
        """

        self.sec_key_ref = sec_key_ref
        self.asn1 = asn1
        self._lib = CoreFoundation

    def __del__(self):
        if self.sec_key_ref:
            self._lib.CFRelease(self.sec_key_ref)
            self._lib = None
            self.sec_key_ref = None


class Certificate(_CertificateBase):
    """
    Container for the OS crypto library representation of a certificate
    """

    sec_certificate_ref = None
    _public_key = None
    _self_signed = None

    def __init__(self, sec_certificate_ref, asn1):
        """
        :param sec_certificate_ref:
            A Security framework SecCertificateRef value from loading/importing
            the certificate

        :param asn1:
            An asn1crypto.x509.Certificate object
        """

        self.sec_certificate_ref = sec_certificate_ref
        self.asn1 = asn1

    @property
    def sec_key_ref(self):
        """
        :return:
            The SecKeyRef of the public key
        """

        return self.public_key.sec_key_ref

    @property
    def public_key(self):
        """
        :return:
            The PublicKey object for the public key this certificate contains
        """

        if not self._public_key and self.sec_certificate_ref:
            sec_public_key_ref_pointer = new(Security, 'SecKeyRef *')
            res = Security.SecCertificateCopyPublicKey(self.sec_certificate_ref, sec_public_key_ref_pointer)
            handle_sec_error(res)
            sec_public_key_ref = unwrap(sec_public_key_ref_pointer)
            self._public_key = PublicKey(sec_public_key_ref, self.asn1['tbs_certificate']['subject_public_key_info'])

        return self._public_key

    @property
    def self_signed(self):
        """
        :return:
            A boolean - if the certificate is self-signed
        """

        if self._self_signed is None:
            self._self_signed = False
            if self.asn1.self_signed in set(['yes', 'maybe']):

                signature_algo = self.asn1['signature_algorithm'].signature_algo
                hash_algo = self.asn1['signature_algorithm'].hash_algo

                if signature_algo == 'rsassa_pkcs1v15':
                    verify_func = rsa_pkcs1v15_verify
                elif signature_algo == 'dsa':
                    verify_func = dsa_verify
                elif signature_algo == 'ecdsa':
                    verify_func = ecdsa_verify
                else:
                    raise OSError(pretty_message(
                        '''
                        Unable to verify the signature of the certificate since
                        it uses the unsupported algorithm %s
                        ''',
                        signature_algo
                    ))

                try:
                    verify_func(
                        self.public_key,
                        self.asn1['signature_value'].native,
                        self.asn1['tbs_certificate'].dump(),
                        hash_algo
                    )
                    self._self_signed = True
                except (SignatureError):
                    pass

        return self._self_signed

    def __del__(self):
        if self._public_key:
            self._public_key.__del__()
            self._public_key = None

        if self.sec_certificate_ref:
            CoreFoundation.CFRelease(self.sec_certificate_ref)
            self.sec_certificate_ref = None


def generate_pair(algorithm, bit_size=None, curve=None):
    """
    Generates a public/private key pair

    :param algorithm:
        The key algorithm - "rsa", "dsa" or "ec"

    :param bit_size:
        An integer - used for "rsa" and "dsa". For "rsa" the value maye be 1024,
        2048, 3072 or 4096. For "dsa" the value may be 1024.

    :param curve:
        A unicode string - used for "ec" keys. Valid values include "secp256r1",
        "secp384r1" and "secp521r1".

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A 2-element tuple of (PublicKey, PrivateKey). The contents of each key
        may be saved by calling .asn1.dump().
    """

    if algorithm not in set(['rsa', 'dsa', 'ec']):
        raise ValueError(pretty_message(
            '''
            algorithm must be one of "rsa", "dsa", "ec", not %s
            ''',
            repr(algorithm)
        ))

    if algorithm == 'rsa':
        if bit_size not in set([1024, 2048, 3072, 4096]):
            raise ValueError(pretty_message(
                '''
                bit_size must be one of 1024, 2048, 3072, 4096, not %s
                ''',
                repr(bit_size)
            ))

    elif algorithm == 'dsa':
        if bit_size not in set([1024]):
            raise ValueError(pretty_message(
                '''
                bit_size must be 1024, not %s
                ''',
                repr(bit_size)
            ))

    elif algorithm == 'ec':
        if curve not in set(['secp256r1', 'secp384r1', 'secp521r1']):
            raise ValueError(pretty_message(
                '''
                curve must be one of "secp256r1", "secp384r1", "secp521r1", not %s
                ''',
                repr(curve)
            ))

    cf_dict = None
    public_key_ref = None
    private_key_ref = None
    cf_data_public = None
    cf_data_private = None
    cf_string = None
    sec_access_ref = None
    sec_keychain_ref = None
    temp_dir = None

    try:
        alg_id = {
            'dsa': SecurityConst.CSSM_ALGID_DSA,
            'ec': SecurityConst.CSSM_ALGID_ECDSA,
            'rsa': SecurityConst.CSSM_ALGID_RSA,
        }[algorithm]

        if algorithm == 'ec':
            key_size = {
                'secp256r1': 256,
                'secp384r1': 384,
                'secp521r1': 521,
            }[curve]
        else:
            key_size = bit_size

        private_key_pointer = new(Security, 'SecKeyRef *')
        public_key_pointer = new(Security, 'SecKeyRef *')

        cf_string = CFHelpers.cf_string_from_unicode("Temporary oscrypto key")

        # We used to use SecKeyGeneratePair() for everything but DSA keys, but due to changes
        # in macOS security, we can't reliably access the default keychain, and instead
        # get an "OSError: User interaction is not allowed." result. Because of this we now
        # use SecKeyCreatePair() for everything, but we even use a throw-away keychain.
        passphrase_len = 16
        rand_data = rand_bytes(10 + passphrase_len)
        passphrase = rand_data[10:]

        temp_filename = b32encode(rand_data[:10]).decode('utf-8')
        temp_dir = tempfile.mkdtemp()
        temp_path = os.path.join(temp_dir, temp_filename).encode('utf-8')

        sec_keychain_ref_pointer = new(Security, 'SecKeychainRef *')
        result = Security.SecKeychainCreate(
            temp_path,
            passphrase_len,
            passphrase,
            False,
            null(),
            sec_keychain_ref_pointer
        )
        handle_sec_error(result)
        sec_keychain_ref = unwrap(sec_keychain_ref_pointer)

        sec_access_ref_pointer = new(Security, 'SecAccessRef *')
        result = Security.SecAccessCreate(cf_string, null(), sec_access_ref_pointer)
        handle_sec_error(result)
        sec_access_ref = unwrap(sec_access_ref_pointer)

        result = Security.SecKeyCreatePair(
            sec_keychain_ref,
            alg_id,
            key_size,
            0,
            SecurityConst.CSSM_KEYUSE_VERIFY,
            SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT,
            SecurityConst.CSSM_KEYUSE_SIGN,
            SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT,
            sec_access_ref,
            public_key_pointer,
            private_key_pointer
        )
        handle_sec_error(result)

        public_key_ref = unwrap(public_key_pointer)
        private_key_ref = unwrap(private_key_pointer)

        cf_data_public_pointer = new(CoreFoundation, 'CFDataRef *')
        result = Security.SecItemExport(public_key_ref, 0, 0, null(), cf_data_public_pointer)
        handle_sec_error(result)
        cf_data_public = unwrap(cf_data_public_pointer)
        public_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_public)

        cf_data_private_pointer = new(CoreFoundation, 'CFDataRef *')
        result = Security.SecItemExport(private_key_ref, 0, 0, null(), cf_data_private_pointer)
        handle_sec_error(result)
        cf_data_private = unwrap(cf_data_private_pointer)
        private_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_private)

        # Clean the new keys out of the keychain
        result = Security.SecKeychainItemDelete(public_key_ref)
        handle_sec_error(result)
        result = Security.SecKeychainItemDelete(private_key_ref)
        handle_sec_error(result)

    finally:
        if cf_dict:
            CoreFoundation.CFRelease(cf_dict)
        if public_key_ref:
            CoreFoundation.CFRelease(public_key_ref)
        if private_key_ref:
            CoreFoundation.CFRelease(private_key_ref)
        if cf_data_public:
            CoreFoundation.CFRelease(cf_data_public)
        if cf_data_private:
            CoreFoundation.CFRelease(cf_data_private)
        if cf_string:
            CoreFoundation.CFRelease(cf_string)
        if sec_keychain_ref:
            Security.SecKeychainDelete(sec_keychain_ref)
            CoreFoundation.CFRelease(sec_keychain_ref)
        if temp_dir:
            shutil.rmtree(temp_dir)
        if sec_access_ref:
            CoreFoundation.CFRelease(sec_access_ref)

    return (load_public_key(public_key_bytes), load_private_key(private_key_bytes))


def generate_dh_parameters(bit_size):
    """
    Generates DH parameters for use with Diffie-Hellman key exchange. Returns
    a structure in the format of DHParameter defined in PKCS#3, which is also
    used by the OpenSSL dhparam tool.

    THIS CAN BE VERY TIME CONSUMING!

    :param bit_size:
        The integer bit size of the parameters to generate. Must be between 512
        and 4096, and divisible by 64. Recommended secure value as of early 2016
        is 2048, with an absolute minimum of 1024.

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        An asn1crypto.algos.DHParameters object. Use
        oscrypto.asymmetric.dump_dh_parameters() to save to disk for usage with
        web servers.
    """

    if not isinstance(bit_size, int_types):
        raise TypeError(pretty_message(
            '''
            bit_size must be an integer, not %s
            ''',
            type_name(bit_size)
        ))

    if bit_size < 512:
        raise ValueError('bit_size must be greater than or equal to 512')

    if bit_size > 4096:
        raise ValueError('bit_size must be less than or equal to 4096')

    if bit_size % 64 != 0:
        raise ValueError('bit_size must be a multiple of 64')

    public_key_ref = None
    private_key_ref = None
    cf_data_public = None
    cf_data_private = None
    cf_string = None
    sec_keychain_ref = None
    sec_access_ref = None
    temp_dir = None

    try:
        public_key_pointer = new(Security, 'SecKeyRef *')
        private_key_pointer = new(Security, 'SecKeyRef *')

        cf_string = CFHelpers.cf_string_from_unicode("Temporary oscrypto key")

        passphrase_len = 16
        rand_data = rand_bytes(10 + passphrase_len)
        passphrase = rand_data[10:]

        temp_filename = b32encode(rand_data[:10]).decode('utf-8')
        temp_dir = tempfile.mkdtemp()
        temp_path = os.path.join(temp_dir, temp_filename).encode('utf-8')

        sec_keychain_ref_pointer = new(Security, 'SecKeychainRef *')
        result = Security.SecKeychainCreate(
            temp_path,
            passphrase_len,
            passphrase,
            False,
            null(),
            sec_keychain_ref_pointer
        )
        handle_sec_error(result)
        sec_keychain_ref = unwrap(sec_keychain_ref_pointer)

        sec_access_ref_pointer = new(Security, 'SecAccessRef *')
        result = Security.SecAccessCreate(cf_string, null(), sec_access_ref_pointer)
        handle_sec_error(result)
        sec_access_ref = unwrap(sec_access_ref_pointer)

        result = Security.SecKeyCreatePair(
            sec_keychain_ref,
            SecurityConst.CSSM_ALGID_DH,
            bit_size,
            0,
            0,
            SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT,
            0,
            SecurityConst.CSSM_KEYATTR_EXTRACTABLE | SecurityConst.CSSM_KEYATTR_PERMANENT,
            sec_access_ref,
            public_key_pointer,
            private_key_pointer
        )
        handle_sec_error(result)

        public_key_ref = unwrap(public_key_pointer)
        private_key_ref = unwrap(private_key_pointer)

        cf_data_private_pointer = new(CoreFoundation, 'CFDataRef *')
        result = Security.SecItemExport(private_key_ref, 0, 0, null(), cf_data_private_pointer)
        handle_sec_error(result)
        cf_data_private = unwrap(cf_data_private_pointer)
        private_key_bytes = CFHelpers.cf_data_to_bytes(cf_data_private)

        # Clean the new keys out of the keychain
        result = Security.SecKeychainItemDelete(public_key_ref)
        handle_sec_error(result)

        result = Security.SecKeychainItemDelete(private_key_ref)
        handle_sec_error(result)

        return KeyExchangeAlgorithm.load(private_key_bytes)['parameters']

    finally:
        if public_key_ref:
            CoreFoundation.CFRelease(public_key_ref)
        if private_key_ref:
            CoreFoundation.CFRelease(private_key_ref)
        if cf_data_public:
            CoreFoundation.CFRelease(cf_data_public)
        if cf_data_private:
            CoreFoundation.CFRelease(cf_data_private)
        if cf_string:
            CoreFoundation.CFRelease(cf_string)
        if sec_keychain_ref:
            Security.SecKeychainDelete(sec_keychain_ref)
            CoreFoundation.CFRelease(sec_keychain_ref)
        if temp_dir:
            shutil.rmtree(temp_dir)
        if sec_access_ref:
            CoreFoundation.CFRelease(sec_access_ref)


def load_certificate(source):
    """
    Loads an x509 certificate into a Certificate object

    :param source:
        A byte string of file contents, a unicode string filename or an
        asn1crypto.x509.Certificate object

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A Certificate object
    """

    if isinstance(source, Asn1Certificate):
        certificate = source

    elif isinstance(source, byte_cls):
        certificate = parse_certificate(source)

    elif isinstance(source, str_cls):
        with open(source, 'rb') as f:
            certificate = parse_certificate(f.read())

    else:
        raise TypeError(pretty_message(
            '''
            source must be a byte string, unicode string or
            asn1crypto.x509.Certificate object, not %s
            ''',
            type_name(source)
        ))

    return _load_x509(certificate)


def _load_x509(certificate):
    """
    Loads an ASN.1 object of an x509 certificate into a Certificate object

    :param certificate:
        An asn1crypto.x509.Certificate object

    :return:
        A Certificate object
    """

    source = certificate.dump()

    cf_source = None
    try:
        cf_source = CFHelpers.cf_data_from_bytes(source)
        sec_key_ref = Security.SecCertificateCreateWithData(CoreFoundation.kCFAllocatorDefault, cf_source)
        return Certificate(sec_key_ref, certificate)

    finally:
        if cf_source:
            CoreFoundation.CFRelease(cf_source)


def load_private_key(source, password=None):
    """
    Loads a private key into a PrivateKey object

    :param source:
        A byte string of file contents, a unicode string filename or an
        asn1crypto.keys.PrivateKeyInfo object

    :param password:
        A byte or unicode string to decrypt the private key file. Unicode
        strings will be encoded using UTF-8. Not used is the source is a
        PrivateKeyInfo object.

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        oscrypto.errors.AsymmetricKeyError - when the private key is incompatible with the OS crypto library
        OSError - when an error is returned by the OS crypto library

    :return:
        A PrivateKey object
    """

    if isinstance(source, PrivateKeyInfo):
        private_object = source

    else:
        if password is not None:
            if isinstance(password, str_cls):
                password = password.encode('utf-8')
            if not isinstance(password, byte_cls):
                raise TypeError(pretty_message(
                    '''
                    password must be a byte string, not %s
                    ''',
                    type_name(password)
                ))

        if isinstance(source, str_cls):
            with open(source, 'rb') as f:
                source = f.read()

        elif not isinstance(source, byte_cls):
            raise TypeError(pretty_message(
                '''
                source must be a byte string, unicode string or
                asn1crypto.keys.PrivateKeyInfo object, not %s
                ''',
                type_name(source)
            ))

        private_object = parse_private(source, password)

    return _load_key(private_object)


def load_public_key(source):
    """
    Loads a public key into a PublicKey object

    :param source:
        A byte string of file contents, a unicode string filename or an
        asn1crypto.keys.PublicKeyInfo object

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        oscrypto.errors.AsymmetricKeyError - when the public key is incompatible with the OS crypto library
        OSError - when an error is returned by the OS crypto library

    :return:
        A PublicKey object
    """

    if isinstance(source, PublicKeyInfo):
        public_key = source

    elif isinstance(source, byte_cls):
        public_key = parse_public(source)

    elif isinstance(source, str_cls):
        with open(source, 'rb') as f:
            public_key = parse_public(f.read())

    else:
        raise TypeError(pretty_message(
            '''
            source must be a byte string, unicode string or
            asn1crypto.keys.PublicKeyInfo object, not %s
            ''',
            type_name(source)
        ))

    return _load_key(public_key)


def _load_key(key_object):
    """
    Common code to load public and private keys into PublicKey and PrivateKey
    objects

    :param key_object:
        An asn1crypto.keys.PublicKeyInfo or asn1crypto.keys.PrivateKeyInfo
        object

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        oscrypto.errors.AsymmetricKeyError - when the key is incompatible with the OS crypto library
        OSError - when an error is returned by the OS crypto library

    :return:
        A PublicKey or PrivateKey object
    """

    if key_object.algorithm == 'ec':
        curve_type, details = key_object.curve
        if curve_type != 'named':
            raise AsymmetricKeyError('OS X only supports EC keys using named curves')
        if details not in set(['secp256r1', 'secp384r1', 'secp521r1']):
            raise AsymmetricKeyError(pretty_message(
                '''
                OS X only supports EC keys using the named curves secp256r1,
                secp384r1 and secp521r1
                '''
            ))

    elif key_object.algorithm == 'dsa' and key_object.hash_algo == 'sha2':
        raise AsymmetricKeyError(pretty_message(
            '''
            OS X only supports DSA keys based on SHA1 (2048 bits or less) - this
            key is based on SHA2 and is %s bits
            ''',
            key_object.bit_size
        ))

    elif key_object.algorithm == 'dsa' and key_object.hash_algo is None:
        raise IncompleteAsymmetricKeyError(pretty_message(
            '''
            The DSA key does not contain the necessary p, q and g parameters
            and can not be used
            '''
        ))

    if isinstance(key_object, PublicKeyInfo):
        source = key_object.dump()
        item_type = SecurityConst.kSecItemTypePublicKey

    else:
        source = _unwrap_private_key_info(key_object).dump()
        item_type = SecurityConst.kSecItemTypePrivateKey

    cf_source = None
    keys_array = None
    attr_array = None

    try:
        cf_source = CFHelpers.cf_data_from_bytes(source)

        format_pointer = new(Security, 'uint32_t *')
        pointer_set(format_pointer, SecurityConst.kSecFormatOpenSSL)
        type_pointer = new(Security, 'uint32_t *')
        pointer_set(type_pointer, item_type)
        keys_pointer = new(CoreFoundation, 'CFArrayRef *')

        attr_array = CFHelpers.cf_array_from_list([
            Security.kSecAttrIsExtractable
        ])

        import_export_params_pointer = new(Security, 'SecItemImportExportKeyParameters *')
        import_export_params = unwrap(import_export_params_pointer)
        import_export_params.version = 0
        import_export_params.flags = 0
        import_export_params.passphrase = null()
        import_export_params.alertTitle = null()
        import_export_params.alertPrompt = null()
        import_export_params.accessRef = null()
        import_export_params.keyUsage = null()
        import_export_params.keyAttributes = attr_array

        res = Security.SecItemImport(
            cf_source,
            null(),
            format_pointer,
            type_pointer,
            0,
            import_export_params_pointer,
            null(),
            keys_pointer
        )
        handle_sec_error(res)
        keys_array = unwrap(keys_pointer)

        length = CoreFoundation.CFArrayGetCount(keys_array)
        if length > 0:
            sec_key_ref = CoreFoundation.CFArrayGetValueAtIndex(keys_array, 0)
            CoreFoundation.CFRetain(sec_key_ref)

        if item_type == SecurityConst.kSecItemTypePublicKey:
            return PublicKey(sec_key_ref, key_object)

        if item_type == SecurityConst.kSecItemTypePrivateKey:
            return PrivateKey(sec_key_ref, key_object)

    finally:
        if attr_array:
            CoreFoundation.CFRelease(attr_array)
        if keys_array:
            CoreFoundation.CFRelease(keys_array)
        if cf_source:
            CoreFoundation.CFRelease(cf_source)


def parse_pkcs12(data, password=None):
    """
    Parses a PKCS#12 ANS.1 DER-encoded structure and extracts certs and keys

    :param data:
        A byte string of a DER-encoded PKCS#12 file

    :param password:
        A byte string of the password to any encrypted data

    :raises:
        ValueError - when any of the parameters are of the wrong type or value
        OSError - when an error is returned by one of the OS decryption functions

    :return:
        A three-element tuple of:
         1. An asn1crypto.keys.PrivateKeyInfo object
         2. An asn1crypto.x509.Certificate object
         3. A list of zero or more asn1crypto.x509.Certificate objects that are
            "extra" certificates, possibly intermediates from the cert chain
    """

    return _parse_pkcs12(data, password, load_private_key)


def load_pkcs12(source, password=None):
    """
    Loads a .p12 or .pfx file into a PrivateKey object and one or more
    Certificates objects

    :param source:
        A byte string of file contents or a unicode string filename

    :param password:
        A byte or unicode string to decrypt the PKCS12 file. Unicode strings
        will be encoded using UTF-8.

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        oscrypto.errors.AsymmetricKeyError - when a contained key is incompatible with the OS crypto library
        OSError - when an error is returned by the OS crypto library

    :return:
        A three-element tuple containing (PrivateKey, Certificate, [Certificate, ...])
    """

    if password is not None:
        if isinstance(password, str_cls):
            password = password.encode('utf-8')
        if not isinstance(password, byte_cls):
            raise TypeError(pretty_message(
                '''
                password must be a byte string, not %s
                ''',
                type_name(password)
            ))

    if isinstance(source, str_cls):
        with open(source, 'rb') as f:
            source = f.read()

    elif not isinstance(source, byte_cls):
        raise TypeError(pretty_message(
            '''
            source must be a byte string or a unicode string, not %s
            ''',
            type_name(source)
        ))

    key_info, cert_info, extra_certs_info = parse_pkcs12(source, password)

    key = None
    cert = None

    if key_info:
        key = _load_key(key_info)

    if cert_info:
        cert = _load_x509(cert_info)

    extra_certs = [_load_x509(info) for info in extra_certs_info]

    return (key, cert, extra_certs)


def rsa_pkcs1v15_encrypt(certificate_or_public_key, data):
    """
    Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1
    v1.5 padding.

    :param certificate_or_public_key:
        A PublicKey or Certificate object

    :param data:
        A byte string, with a maximum length 11 bytes less than the key length
        (in bytes)

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the encrypted data
    """

    if not isinstance(certificate_or_public_key, (Certificate, PublicKey)):
        raise TypeError(pretty_message(
            '''
            certificate_or_public_key must be an instance of the Certificate or
            PublicKey class, not %s
            ''',
            type_name(certificate_or_public_key)
        ))

    if not isinstance(data, byte_cls):
        raise TypeError(pretty_message(
            '''
            data must be a byte string, not %s
            ''',
            type_name(data)
        ))

    key_length = certificate_or_public_key.byte_size
    buffer = buffer_from_bytes(key_length)
    output_length = new(Security, 'size_t *', key_length)
    result = Security.SecKeyEncrypt(
        certificate_or_public_key.sec_key_ref,
        SecurityConst.kSecPaddingPKCS1,
        data,
        len(data),
        buffer,
        output_length
    )
    handle_sec_error(result)

    return bytes_from_buffer(buffer, deref(output_length))


def rsa_pkcs1v15_decrypt(private_key, ciphertext):
    """
    Decrypts a byte string using an RSA private key. Uses PKCS#1 v1.5 padding.

    :param private_key:
        A PrivateKey object

    :param ciphertext:
        A byte string of the encrypted data

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the original plaintext
    """

    if not isinstance(private_key, PrivateKey):
        raise TypeError(pretty_message(
            '''
            private_key must an instance of the PrivateKey class, not %s
            ''',
            type_name(private_key)
        ))

    if not isinstance(ciphertext, byte_cls):
        raise TypeError(pretty_message(
            '''
            data must be a byte string, not %s
            ''',
            type_name(ciphertext)
        ))

    key_length = private_key.byte_size
    buffer = buffer_from_bytes(key_length)
    output_length = new(Security, 'size_t *', key_length)

    if osx_version_info < (10, 8):
        padding = SecurityConst.kSecPaddingNone
    else:
        padding = SecurityConst.kSecPaddingPKCS1

    result = Security.SecKeyDecrypt(
        private_key.sec_key_ref,
        padding,
        ciphertext,
        len(ciphertext),
        buffer,
        output_length
    )
    handle_sec_error(result)

    output = bytes_from_buffer(buffer, deref(output_length))

    if osx_version_info < (10, 8):
        output = remove_pkcs1v15_encryption_padding(key_length, output)

    return output


def rsa_oaep_encrypt(certificate_or_public_key, data):
    """
    Encrypts a byte string using an RSA public key or certificate. Uses PKCS#1
    OAEP padding with SHA1.

    :param certificate_or_public_key:
        A PublicKey or Certificate object

    :param data:
        A byte string, with a maximum length 41 bytes (or more) less than the
        key length (in bytes)

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the encrypted data
    """

    return _encrypt(certificate_or_public_key, data, Security.kSecPaddingOAEPKey)


def rsa_oaep_decrypt(private_key, ciphertext):
    """
    Decrypts a byte string using an RSA private key. Uses PKCS#1 OAEP padding
    with SHA1.

    :param private_key:
        A PrivateKey object

    :param ciphertext:
        A byte string of the encrypted data

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the original plaintext
    """

    return _decrypt(private_key, ciphertext, Security.kSecPaddingOAEPKey)


def _encrypt(certificate_or_public_key, data, padding):
    """
    Encrypts plaintext using an RSA public key or certificate

    :param certificate_or_public_key:
        A Certificate or PublicKey object

    :param data:
        The plaintext - a byte string

    :param padding:
        The padding mode to use, specified as a kSecPadding*Key value

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the ciphertext
    """

    if not isinstance(certificate_or_public_key, (Certificate, PublicKey)):
        raise TypeError(pretty_message(
            '''
            certificate_or_public_key must be an instance of the Certificate or
            PublicKey class, not %s
            ''',
            type_name(certificate_or_public_key)
        ))

    if not isinstance(data, byte_cls):
        raise TypeError(pretty_message(
            '''
            data must be a byte string, not %s
            ''',
            type_name(data)
        ))

    if not padding:
        raise ValueError('padding must be specified')

    cf_data = None
    sec_transform = None

    try:
        cf_data = CFHelpers.cf_data_from_bytes(data)

        error_pointer = new(CoreFoundation, 'CFErrorRef *')
        sec_transform = Security.SecEncryptTransformCreate(
            certificate_or_public_key.sec_key_ref,
            error_pointer
        )
        handle_cf_error(error_pointer)

        if padding:
            Security.SecTransformSetAttribute(
                sec_transform,
                Security.kSecPaddingKey,
                padding,
                error_pointer
            )
            handle_cf_error(error_pointer)

        Security.SecTransformSetAttribute(
            sec_transform,
            Security.kSecTransformInputAttributeName,
            cf_data,
            error_pointer
        )
        handle_cf_error(error_pointer)

        ciphertext = Security.SecTransformExecute(sec_transform, error_pointer)
        handle_cf_error(error_pointer)

        return CFHelpers.cf_data_to_bytes(ciphertext)

    finally:
        if cf_data:
            CoreFoundation.CFRelease(cf_data)
        if sec_transform:
            CoreFoundation.CFRelease(sec_transform)


def _decrypt(private_key, ciphertext, padding):
    """
    Decrypts RSA ciphertext using a private key

    :param private_key:
        A PrivateKey object

    :param ciphertext:
        The ciphertext - a byte string

    :param padding:
        The padding mode to use, specified as a kSecPadding*Key value

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the plaintext
    """

    if not isinstance(private_key, PrivateKey):
        raise TypeError(pretty_message(
            '''
            private_key must be an instance of the PrivateKey class, not %s
            ''',
            type_name(private_key)
        ))

    if not isinstance(ciphertext, byte_cls):
        raise TypeError(pretty_message(
            '''
            ciphertext must be a byte string, not %s
            ''',
            type_name(ciphertext)
        ))

    if not padding:
        raise ValueError('padding must be specified')

    cf_data = None
    sec_transform = None

    try:
        cf_data = CFHelpers.cf_data_from_bytes(ciphertext)

        error_pointer = new(CoreFoundation, 'CFErrorRef *')
        sec_transform = Security.SecDecryptTransformCreate(
            private_key.sec_key_ref,
            error_pointer
        )
        handle_cf_error(error_pointer)

        Security.SecTransformSetAttribute(
            sec_transform,
            Security.kSecPaddingKey,
            padding,
            error_pointer
        )
        handle_cf_error(error_pointer)

        Security.SecTransformSetAttribute(
            sec_transform,
            Security.kSecTransformInputAttributeName,
            cf_data,
            error_pointer
        )
        handle_cf_error(error_pointer)

        plaintext = Security.SecTransformExecute(sec_transform, error_pointer)
        handle_cf_error(error_pointer)

        return CFHelpers.cf_data_to_bytes(plaintext)

    finally:
        if cf_data:
            CoreFoundation.CFRelease(cf_data)
        if sec_transform:
            CoreFoundation.CFRelease(sec_transform)


def rsa_pkcs1v15_verify(certificate_or_public_key, signature, data, hash_algorithm):
    """
    Verifies an RSASSA-PKCS-v1.5 signature.

    When the hash_algorithm is "raw", the operation is identical to RSA
    public key decryption. That is: the data is not hashed and no ASN.1
    structure with an algorithm identifier of the hash algorithm is placed in
    the encrypted byte string.

    :param certificate_or_public_key:
        A Certificate or PublicKey instance to verify the signature with

    :param signature:
        A byte string of the signature to verify

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384", "sha512" or "raw"

    :raises:
        oscrypto.errors.SignatureError - when the signature is determined to be invalid
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library
    """

    if certificate_or_public_key.algorithm != 'rsa':
        raise ValueError('The key specified is not an RSA public key')

    return _verify(certificate_or_public_key, signature, data, hash_algorithm)


def rsa_pss_verify(certificate_or_public_key, signature, data, hash_algorithm):
    """
    Verifies an RSASSA-PSS signature. For the PSS padding the mask gen algorithm
    will be mgf1 using the same hash algorithm as the signature. The salt length
    with be the length of the hash algorithm, and the trailer field with be the
    standard 0xBC byte.

    :param certificate_or_public_key:
        A Certificate or PublicKey instance to verify the signature with

    :param signature:
        A byte string of the signature to verify

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512"

    :raises:
        oscrypto.errors.SignatureError - when the signature is determined to be invalid
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library
    """

    if not isinstance(certificate_or_public_key, (Certificate, PublicKey)):
        raise TypeError(pretty_message(
            '''
            certificate_or_public_key must be an instance of the Certificate or
            PublicKey class, not %s
            ''',
            type_name(certificate_or_public_key)
        ))

    if not isinstance(data, byte_cls):
        raise TypeError(pretty_message(
            '''
            data must be a byte string, not %s
            ''',
            type_name(data)
        ))

    if certificate_or_public_key.algorithm != 'rsa':
        raise ValueError('The key specified is not an RSA public key')

    hash_length = {
        'sha1': 20,
        'sha224': 28,
        'sha256': 32,
        'sha384': 48,
        'sha512': 64
    }.get(hash_algorithm, 0)

    key_length = certificate_or_public_key.byte_size
    buffer = buffer_from_bytes(key_length)
    output_length = new(Security, 'size_t *', key_length)
    result = Security.SecKeyEncrypt(
        certificate_or_public_key.sec_key_ref,
        SecurityConst.kSecPaddingNone,
        signature,
        len(signature),
        buffer,
        output_length
    )
    handle_sec_error(result)

    plaintext = bytes_from_buffer(buffer, deref(output_length))
    if not verify_pss_padding(hash_algorithm, hash_length, certificate_or_public_key.bit_size, data, plaintext):
        raise SignatureError('Signature is invalid')


def dsa_verify(certificate_or_public_key, signature, data, hash_algorithm):
    """
    Verifies a DSA signature

    :param certificate_or_public_key:
        A Certificate or PublicKey instance to verify the signature with

    :param signature:
        A byte string of the signature to verify

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512"

    :raises:
        oscrypto.errors.SignatureError - when the signature is determined to be invalid
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library
    """

    if certificate_or_public_key.algorithm != 'dsa':
        raise ValueError('The key specified is not a DSA public key')

    return _verify(certificate_or_public_key, signature, data, hash_algorithm)


def ecdsa_verify(certificate_or_public_key, signature, data, hash_algorithm):
    """
    Verifies an ECDSA signature

    :param certificate_or_public_key:
        A Certificate or PublicKey instance to verify the signature with

    :param signature:
        A byte string of the signature to verify

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512"

    :raises:
        oscrypto.errors.SignatureError - when the signature is determined to be invalid
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library
    """

    if certificate_or_public_key.algorithm != 'ec':
        raise ValueError('The key specified is not an EC public key')

    return _verify(certificate_or_public_key, signature, data, hash_algorithm)


def _verify(certificate_or_public_key, signature, data, hash_algorithm):
    """
    Verifies an RSA, DSA or ECDSA signature

    :param certificate_or_public_key:
        A Certificate or PublicKey instance to verify the signature with

    :param signature:
        A byte string of the signature to verify

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or "sha512"

    :raises:
        oscrypto.errors.SignatureError - when the signature is determined to be invalid
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library
    """

    if not isinstance(certificate_or_public_key, (Certificate, PublicKey)):
        raise TypeError(pretty_message(
            '''
            certificate_or_public_key must be an instance of the Certificate or
            PublicKey class, not %s
            ''',
            type_name(certificate_or_public_key)
        ))

    if not isinstance(signature, byte_cls):
        raise TypeError(pretty_message(
            '''
            signature must be a byte string, not %s
            ''',
            type_name(signature)
        ))

    if not isinstance(data, byte_cls):
        raise TypeError(pretty_message(
            '''
            data must be a byte string, not %s
            ''',
            type_name(data)
        ))

    valid_hash_algorithms = set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'])
    if certificate_or_public_key.algorithm == 'rsa':
        valid_hash_algorithms |= set(['raw'])

    if hash_algorithm not in valid_hash_algorithms:
        valid_hash_algorithms_error = '"md5", "sha1", "sha224", "sha256", "sha384", "sha512"'
        if certificate_or_public_key.algorithm == 'rsa':
            valid_hash_algorithms_error += ', "raw"'
        raise ValueError(pretty_message(
            '''
            hash_algorithm must be one of %s, not %s
            ''',
            valid_hash_algorithms_error,
            repr(hash_algorithm)
        ))

    if certificate_or_public_key.algorithm == 'rsa' and hash_algorithm == 'raw':
        if len(data) > certificate_or_public_key.byte_size - 11:
            raise ValueError(pretty_message(
                '''
                data must be 11 bytes shorter than the key size when
                hash_algorithm is "raw" - key size is %s bytes, but data
                is %s bytes long
                ''',
                certificate_or_public_key.byte_size,
                len(data)
            ))

        result = Security.SecKeyRawVerify(
            certificate_or_public_key.sec_key_ref,
            SecurityConst.kSecPaddingPKCS1,
            data,
            len(data),
            signature,
            len(signature)
        )
        # errSSLCrypto is returned in some situations on macOS 10.12
        if result == SecurityConst.errSecVerifyFailed or result == SecurityConst.errSSLCrypto:
            raise SignatureError('Signature is invalid')
        handle_sec_error(result)
        return

    cf_signature = None
    cf_data = None
    cf_hash_length = None
    sec_transform = None

    try:
        error_pointer = new(CoreFoundation, 'CFErrorRef *')
        cf_signature = CFHelpers.cf_data_from_bytes(signature)
        sec_transform = Security.SecVerifyTransformCreate(
            certificate_or_public_key.sec_key_ref,
            cf_signature,
            error_pointer
        )
        handle_cf_error(error_pointer)

        hash_constant = {
            'md5': Security.kSecDigestMD5,
            'sha1': Security.kSecDigestSHA1,
            'sha224': Security.kSecDigestSHA2,
            'sha256': Security.kSecDigestSHA2,
            'sha384': Security.kSecDigestSHA2,
            'sha512': Security.kSecDigestSHA2
        }[hash_algorithm]

        Security.SecTransformSetAttribute(
            sec_transform,
            Security.kSecDigestTypeAttribute,
            hash_constant,
            error_pointer
        )
        handle_cf_error(error_pointer)

        if hash_algorithm in set(['sha224', 'sha256', 'sha384', 'sha512']):
            hash_length = {
                'sha224': 224,
                'sha256': 256,
                'sha384': 384,
                'sha512': 512
            }[hash_algorithm]

            cf_hash_length = CFHelpers.cf_number_from_integer(hash_length)

            Security.SecTransformSetAttribute(
                sec_transform,
                Security.kSecDigestLengthAttribute,
                cf_hash_length,
                error_pointer
            )
            handle_cf_error(error_pointer)

        if certificate_or_public_key.algorithm == 'rsa':
            Security.SecTransformSetAttribute(
                sec_transform,
                Security.kSecPaddingKey,
                Security.kSecPaddingPKCS1Key,
                error_pointer
            )
            handle_cf_error(error_pointer)

        cf_data = CFHelpers.cf_data_from_bytes(data)
        Security.SecTransformSetAttribute(
            sec_transform,
            Security.kSecTransformInputAttributeName,
            cf_data,
            error_pointer
        )
        handle_cf_error(error_pointer)

        res = Security.SecTransformExecute(sec_transform, error_pointer)
        if not is_null(error_pointer):
            error = unwrap(error_pointer)
            if not is_null(error):
                raise SignatureError('Signature is invalid')

        res = bool(CoreFoundation.CFBooleanGetValue(res))

        if not res:
            raise SignatureError('Signature is invalid')

    finally:
        if sec_transform:
            CoreFoundation.CFRelease(sec_transform)
        if cf_signature:
            CoreFoundation.CFRelease(cf_signature)
        if cf_data:
            CoreFoundation.CFRelease(cf_data)
        if cf_hash_length:
            CoreFoundation.CFRelease(cf_hash_length)


def rsa_pkcs1v15_sign(private_key, data, hash_algorithm):
    """
    Generates an RSASSA-PKCS-v1.5 signature.

    When the hash_algorithm is "raw", the operation is identical to RSA
    private key encryption. That is: the data is not hashed and no ASN.1
    structure with an algorithm identifier of the hash algorithm is placed in
    the encrypted byte string.

    :param private_key:
        The PrivateKey to generate the signature with

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384",
        "sha512" or "raw"

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the signature
    """

    if private_key.algorithm != 'rsa':
        raise ValueError('The key specified is not an RSA private key')

    return _sign(private_key, data, hash_algorithm)


def rsa_pss_sign(private_key, data, hash_algorithm):
    """
    Generates an RSASSA-PSS signature. For the PSS padding the mask gen
    algorithm will be mgf1 using the same hash algorithm as the signature. The
    salt length with be the length of the hash algorithm, and the trailer field
    with be the standard 0xBC byte.

    :param private_key:
        The PrivateKey to generate the signature with

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or
        "sha512"

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the signature
    """

    if not isinstance(private_key, PrivateKey):
        raise TypeError(pretty_message(
            '''
            private_key must be an instance of the PrivateKey class, not %s
            ''',
            type_name(private_key)
        ))

    if not isinstance(data, byte_cls):
        raise TypeError(pretty_message(
            '''
            data must be a byte string, not %s
            ''',
            type_name(data)
        ))

    if private_key.algorithm != 'rsa':
        raise ValueError('The key specified is not an RSA private key')

    hash_length = {
        'sha1': 20,
        'sha224': 28,
        'sha256': 32,
        'sha384': 48,
        'sha512': 64
    }.get(hash_algorithm, 0)

    encoded_data = add_pss_padding(hash_algorithm, hash_length, private_key.bit_size, data)

    key_length = private_key.byte_size
    buffer = buffer_from_bytes(key_length)
    output_length = new(Security, 'size_t *', key_length)
    result = Security.SecKeyDecrypt(
        private_key.sec_key_ref,
        SecurityConst.kSecPaddingNone,
        encoded_data,
        len(encoded_data),
        buffer,
        output_length
    )
    handle_sec_error(result)

    return bytes_from_buffer(buffer, deref(output_length))


def dsa_sign(private_key, data, hash_algorithm):
    """
    Generates a DSA signature

    :param private_key:
        The PrivateKey to generate the signature with

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or
        "sha512"

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the signature
    """

    if private_key.algorithm != 'dsa':
        raise ValueError('The key specified is not a DSA private key')

    return _sign(private_key, data, hash_algorithm)


def ecdsa_sign(private_key, data, hash_algorithm):
    """
    Generates an ECDSA signature

    :param private_key:
        The PrivateKey to generate the signature with

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or
        "sha512"

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the signature
    """

    if private_key.algorithm != 'ec':
        raise ValueError('The key specified is not an EC private key')

    return _sign(private_key, data, hash_algorithm)


def _sign(private_key, data, hash_algorithm):
    """
    Generates an RSA, DSA or ECDSA signature

    :param private_key:
        The PrivateKey to generate the signature with

    :param data:
        A byte string of the data the signature is for

    :param hash_algorithm:
        A unicode string of "md5", "sha1", "sha224", "sha256", "sha384" or
        "sha512"

    :raises:
        ValueError - when any of the parameters contain an invalid value
        TypeError - when any of the parameters are of the wrong type
        OSError - when an error is returned by the OS crypto library

    :return:
        A byte string of the signature
    """

    if not isinstance(private_key, PrivateKey):
        raise TypeError(pretty_message(
            '''
            private_key must be an instance of PrivateKey, not %s
            ''',
            type_name(private_key)
        ))

    if not isinstance(data, byte_cls):
        raise TypeError(pretty_message(
            '''
            data must be a byte string, not %s
            ''',
            type_name(data)
        ))

    valid_hash_algorithms = set(['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'])
    if private_key.algorithm == 'rsa':
        valid_hash_algorithms |= set(['raw'])

    if hash_algorithm not in valid_hash_algorithms:
        valid_hash_algorithms_error = '"md5", "sha1", "sha224", "sha256", "sha384", "sha512"'
        if private_key.algorithm == 'rsa':
            valid_hash_algorithms_error += ', "raw"'
        raise ValueError(pretty_message(
            '''
            hash_algorithm must be one of %s, not %s
            ''',
            valid_hash_algorithms_error,
            repr(hash_algorithm)
        ))

    if private_key.algorithm == 'rsa' and hash_algorithm == 'raw':
        if len(data) > private_key.byte_size - 11:
            raise ValueError(pretty_message(
                '''
                data must be 11 bytes shorter than the key size when
                hash_algorithm is "raw" - key size is %s bytes, but
                data is %s bytes long
                ''',
                private_key.byte_size,
                len(data)
            ))

        key_length = private_key.byte_size
        buffer = buffer_from_bytes(key_length)
        output_length = new(Security, 'size_t *', key_length)
        result = Security.SecKeyRawSign(
            private_key.sec_key_ref,
            SecurityConst.kSecPaddingPKCS1,
            data,
            len(data),
            buffer,
            output_length
        )
        handle_sec_error(result)

        return bytes_from_buffer(buffer, deref(output_length))

    cf_signature = None
    cf_data = None
    cf_hash_length = None
    sec_transform = None

    try:
        error_pointer = new(CoreFoundation, 'CFErrorRef *')
        sec_transform = Security.SecSignTransformCreate(private_key.sec_key_ref, error_pointer)
        handle_cf_error(error_pointer)

        hash_constant = {
            'md5': Security.kSecDigestMD5,
            'sha1': Security.kSecDigestSHA1,
            'sha224': Security.kSecDigestSHA2,
            'sha256': Security.kSecDigestSHA2,
            'sha384': Security.kSecDigestSHA2,
            'sha512': Security.kSecDigestSHA2
        }[hash_algorithm]

        Security.SecTransformSetAttribute(
            sec_transform,
            Security.kSecDigestTypeAttribute,
            hash_constant,
            error_pointer
        )
        handle_cf_error(error_pointer)

        if hash_algorithm in set(['sha224', 'sha256', 'sha384', 'sha512']):
            hash_length = {
                'sha224': 224,
                'sha256': 256,
                'sha384': 384,
                'sha512': 512
            }[hash_algorithm]

            cf_hash_length = CFHelpers.cf_number_from_integer(hash_length)

            Security.SecTransformSetAttribute(
                sec_transform,
                Security.kSecDigestLengthAttribute,
                cf_hash_length,
                error_pointer
            )
            handle_cf_error(error_pointer)

        if private_key.algorithm == 'rsa':
            Security.SecTransformSetAttribute(
                sec_transform,
                Security.kSecPaddingKey,
                Security.kSecPaddingPKCS1Key,
                error_pointer
            )
            handle_cf_error(error_pointer)

        cf_data = CFHelpers.cf_data_from_bytes(data)
        Security.SecTransformSetAttribute(
            sec_transform,
            Security.kSecTransformInputAttributeName,
            cf_data,
            error_pointer
        )
        handle_cf_error(error_pointer)

        cf_signature = Security.SecTransformExecute(sec_transform, error_pointer)
        handle_cf_error(error_pointer)

        return CFHelpers.cf_data_to_bytes(cf_signature)

    finally:
        if sec_transform:
            CoreFoundation.CFRelease(sec_transform)
        if cf_signature:
            CoreFoundation.CFRelease(cf_signature)
        if cf_data:
            CoreFoundation.CFRelease(cf_data)
        if cf_hash_length:
            CoreFoundation.CFRelease(cf_hash_length)
