# signature.py - module for handling signed XML files
# coding: utf-8
#
# Copyright (C) 2017-2018 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""Module for handling signed PSKC files.

This module defines a Signature class that handles the signature checking,
keys and certificates.
"""


def sign_x509(xml, key, certificate, algorithm=None, digest_algorithm=None,
              canonicalization_method=None):
    """Sign PSKC data using X.509 certificate and private key.

    xml: an XML document
    key: the private key in binary format
    certificate: the X.509 certificate
    """
    import signxml
    algorithm = algorithm or 'rsa-sha256'
    digest_algorithm = digest_algorithm or 'sha256'
    canonicalization_method = (
        canonicalization_method or
        getattr(signxml.XMLSignatureProcessor, 'default_c14n_algorithm', None) or
        'http://www.w3.org/2006/12/xml-c14n11')
    return signxml.XMLSigner(
        method=signxml.methods.enveloped,
        signature_algorithm=algorithm.rsplit('#', 1)[-1].lower(),
        digest_algorithm=digest_algorithm.rsplit('#', 1)[-1].lower(),
        c14n_algorithm=canonicalization_method,
    ).sign(xml, key=key, cert=certificate)


def verify_x509(tree, certificate=None, ca_pem_file=None):
    """Verify signature in PSKC data against a trusted X.509 certificate.

    If a certificate is supplied it is used to validate the signature,
    otherwise any embedded certificate is used and validated against a
    certificate in ca_pem_file if it specified and otherwise the operating
    system CA certificates.
    """
    from signxml import XMLVerifier
    return XMLVerifier().verify(
        tree, x509_cert=certificate, ca_pem_file=ca_pem_file).signed_xml


class Signature(object):
    """Class for handling signature checking of the PSKC file.

    Instances of this class provide the following properties:

      is_signed: boolean to indicate whether a signature is present
      algorithm: identifier of the signing algorithm used
      canonicalization_method: identifier of the XML canonicalization used
      digest_algorithm: algorithm used for creating the hash
      issuer: issuer of the certificate
      serial: serial number of the certificate
      key: key that will be used when creating a signed PSKC file
      certificate: the certificate that is embedded in the signature
      signed_pskc: a PSKC instance with the signed information
    """

    def __init__(self, pskc):
        self.pskc = pskc
        self._algorithm = None
        self.canonicalization_method = None
        self.digest_algorithm = None
        self.issuer = None
        self.serial = None
        self.key = None
        self.certificate = None

    @property
    def is_signed(self):
        """Test whether the PSKC file contains a signature.

        This method does not check whether the signature is valid but only if
        one was present in the PSKC file.
        """
        return bool(
            self.algorithm or self.canonicalization_method or
            self.digest_algorithm or self.issuer or self.certificate)

    @property
    def algorithm(self):
        """Provide the signing algorithm used."""
        if self._algorithm:
            return self._algorithm

    @algorithm.setter
    def algorithm(self, value):
        from pskc.algorithms import normalise_algorithm
        self._algorithm = normalise_algorithm(value)

    @property
    def signed_pskc(self):
        """Provide the signed PSKC information."""
        if not hasattr(self, '_signed_pskc'):
            self.verify()
        return self._signed_pskc

    def verify(self, certificate=None, ca_pem_file=None):
        """Check that the signature was made with the specified certificate.

        If no certificate is provided the signature is expected to contain a
        signature that is signed by the CA certificate (or the CA standard CA
        certificates when ca_pem_file is absent).
        """
        from pskc import PSKC
        from pskc.parser import PSKCParser
        signed_xml = verify_x509(self.tree, certificate, ca_pem_file)
        pskc = PSKC()
        PSKCParser.parse_document(pskc, signed_xml)
        self._signed_pskc = pskc
        return True

    def sign(self, key, certificate=None):
        """Add an XML signature to the file."""
        self.key = key
        self.certificate = certificate

    def sign_xml(self, xml):
        """Sign an XML document with the configured key and certificate."""
        return sign_x509(
            xml, self.key, self.certificate, self.algorithm,
            self.digest_algorithm, self.canonicalization_method)
