#! python3
# -*- coding: utf8 -*-

"""Some tests for `sc14n.py` the Python interface to SC14N using CryptoSys PKI."""

# test_sc14n_pki.py: version 2.1.1
# $Date: 2019-12-28 20:53:00 $

# ************************** LICENSE *****************************************
# Copyright (C) 2017-19 David Ireland, DI Management Services Pty Limited.
# <http://www.di-mgt.com.au/contact/> <www.cryptosys.net>
# The code in this module is licensed under the terms of the MIT license.
# For a copy, see <https://opensource.org/licenses/MIT>
# ****************************************************************************

# Requires CryptoSys PKI to be installed.
# Get a Trial Edition from http://www.cryptosys.net/pki/.

# import context   # test: setup path to module in parent
from sc14n import *  # @UnusedWildImport
import cryptosyspki as pki
import locale

# Show some info about the core DLL
print("DLL version =", Gen.version())
print("PKI DLL version =", pki.Gen.version())
print('locale.getpreferredencoding() =', locale.getpreferredencoding())

######################
# HARD-CODED PKI STUFF
######################
# Alice's PKCS8 encrypted key and X.509 certificate
# from RFC 4134 "Examples of S/MIME Messages"
# Private key password is "password"
myprikey = '''-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICojAcBgoqhkiG9w0BDAEDMA4ECFleZ90vhGrRAgIEAASCAoA9rti16XVH
K4AJVe1CNf61NIpIogu/Xs4Yn4hXflvewiOwe6/9FkxBXLbhKdbQWn1Z4p3C
njVns2VYEO/qpJR3LciHMwp5dsqedUVVia//CqFHtEV9WfvCKWgmlkkT1YEm
1aChZnPP5i6IhwVT9qvFluTZhvVmjW0YyF86OrOp0uxxVic7phPbnPrOMelf
ZPc3A3EGpzDPkxN+o0obw87tUgCL+s0KtUOr3c6Si4KQ3IQjrjZxQF4Se3t/
4PEpqUl5EpYiCx9q5uqb0Lr1kWiiQ5/inZm5ETc+qO+ENcp0KjnX523CATYd
U5iOjl/X9XZeJrMpOCXogEuhmLPRauYP1HEWnAY/hLW93v10QJXY6ALlbkL0
sd5WU8Ces7T04b/p4/12yxqYqV68QePyfHpegdraDq3vRfopSwrUxtL9cisP
jsQcJ5FL/SfloFbmld4CKIjMsromsEWqo6rfo3JqNizgTVIIWExy3jDT9VvK
d9ADH0g3JCbuFzaWVOZMmZ0wlo28PKkLQ8FkW8CG/Lq/Q/bHLPM+sPdLN+ke
gpA6fvL4wpku4ST7hmeN1vWbRLlCfuFijux77hdM7knO9/MawICsA4XdzR78
p0C2hJlc6p46IWZaINQXGstTbJMh+mJ7i1lrbG2kvZ2Twf9R+RaLp2mPHjb1
+P+3f2L3tOoC31oJ18u/L1MXEWxLEZHB0+ANg+N/0/icwImcI0D+wVN2puU4
m58j81sGZUEAB3aFEbPxoX3y+qYlOnt1OfdY7WnNdyr9ZzI09fkrTvujF4LU
nycqE+MXerf0PxkNu1qv9bQvCoH8x3J2EVdMxPBtH1Fb7SbE66cNyh//qzZo
B9Je
-----END ENCRYPTED PRIVATE KEY-----
'''

mycert = '''-----BEGIN CERTIFICATE-----
MIICLDCCAZWgAwIBAgIQRjRrx4AAVrwR024uxBCzsDANBgkqhkiG9w0BAQUFADAS
MRAwDgYDVQQDEwdDYXJsUlNBMB4XDTk5MDkxOTAxMDg0N1oXDTM5MTIzMTIzNTk1
OVowEzERMA8GA1UEAxMIQWxpY2VSU0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
AoGBAOCJczmN2PX16Id2OX9OsAW7U4PeD7er3H3HdSkNBS5tEt+mhibU0m+qWCn8
l+z6glEPMIC+sVCeRkTxLLvYMs/GaG8H2bBgrL7uNAlqE/X3BQWT3166NVbZYf8Z
f8mB5vhs6odAcO+sbSx0ny36VTq5mXcCpkhSjE7zVzhXdFdfAgMBAAGjgYEwfzAM
BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAfBgNVHSMEGDAWgBTp4JAnrHgg
eprTTPJCN04irp44uzAdBgNVHQ4EFgQUd9K00bdMioqjzkWdzuw8oDrj/1AwHwYD
VR0RBBgwFoEUQWxpY2VSU0FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADgYEA
PnBHqEjME1iPylFxa042GF0EfoCxjU3MyqOPzH1WyLzPbrMcWakgqgWBqE4lradw
FHUv9ceb0Q7pY9Jkt8ZmbnMhVN/0uiVdfUnTlGsiNnRzuErsL2Tt0z3Sp0LF6DeK
tNufZ+S9n/n+dO/q+e5jatg/SyUJtdgadq7rm9tJsCI=
-----END CERTIFICATE-----
'''

mypassword = "password"   # Best security practice!!


def sign_string(s):
    # Hard-coded key and password (useful in IDE mode)
    sigval = pki.Sig.sign_data(s.encode(), myprikey, mypassword, pki.Sig.Alg.RSA_SHA1)
    return sigval


def sign_digest(digval):
    # Hard-coded key and password (useful in IDE mode)
    sigval = pki.Sig.sign_digest(pki.Cnv.frombase64(digval), myprikey, mypassword, pki.Sig.Alg.RSA_SHA1)
    return sigval


def rsa_key_value():
    """Extract RSAKeyValue from public certificate in XML style"""
    return pki.Rsa.to_xmlstring(pki.Rsa.read_public_key(mycert))


def sha1_from_file_base64(fname):
    """Compute SHA-1 digest of file in base64 encoding"""
    return pki.Cnv.tobase64(pki.Hash.file(fname))


# FILE-RELATED UTILITIES
def read_binary_file(fname):
    with open(fname, "rb") as f:
        return bytearray(f.read())


def write_binary_file(fname, data):
    with open(fname, "wb") as f:
        f.write(data)


def read_text_file(fname, enc='utf8'):
    with open(fname, encoding=enc) as f:
        return f.read()


def write_text_file(fname, s, enc='utf8'):
    with open(fname, "w", encoding=enc) as f:
        f.write(s)


def _print_file(fname):
    """Print contents of text file"""
    s = read_text_file(fname)
    print(s)


def split2len(s, n):
    """Split up string s into lines of length n."""
    def _f(_s, _n):
        while _s:
            yield _s[:_n]
            _s = _s[_n:]
    return "\n".join(_f(s, n))


def make_signed_file_latin1(outfile, basefile, okdigest):
    """
    Compute and replace %digval%, %sigval% and %keyval%
    in a simple XML-DSIG file - expecting here original to be Latin-1 encoded.
    With lots of verbose debugging checks and alternative ways to do things.

    :param outfile: new file to be created
    :param basefile: base XML file Latin-1 encoded with %% parameters
    :param okdigest: expected SHA-1 digest of final file (in base64)
    :return: N/A
    """

    print("FILE:", basefile)

    # Read in c14n'd data excluding Signature element
    s = C14n.file2string(basefile, "Signature", Tran.OMITBYTAG)
    print("EXCLUDE <Signature>:\n", s)

    # Check digest of this string (optional)
    print("DIG(string):", pki.Cnv.tobase64(pki.Hash.data(s.encode())))

    # Compute required digest directly
    digval = C14n.file2digest(basefile, "Signature", Tran.OMITBYTAG)
    print("DIG(bytag) :", digval)

    # Extract SignedInfo from base file
    s = C14n.file2string(basefile, "SignedInfo", Tran.SUBSETBYTAG)
    print("SUBSET <SignedInfo>:\n", s)

    # Insert the digest value into the SignedInfo
    siginfo = s.replace("%digval%", digval)
    print(siginfo)

    # Compute digest value of this new string (optional)
    print("DIG(string):", pki.Cnv.tobase64(pki.Hash.data(siginfo.encode())))
    # Compute digest value directly using SC14N (optional)
    print("DIG(bytag) :", C14n.string2digest(siginfo))

    # Compute the signature value directly over the SignedInfo element
    sigval = sign_string(siginfo)
    print("SIG:\n", sigval)

    # Get RSA Key Value in XML form
    keyval = rsa_key_value()
    print("keyval=\n", keyval)

    # If we only had the private key, we could get the RSA Key Value from that as well,
    # but be careful not to expose your private key! Two ways to do that:
    print(pki.Rsa.to_xmlstring(pki.Rsa.publickey_from_private(pki.Rsa.read_private_key(myprikey, mypassword))))
    print(pki.Rsa.to_xmlstring(pki.Rsa.read_private_key(myprikey, mypassword), pki.Rsa.XmlOptions.EXCLPRIVATE))

    # If we needed it we can get the <X509Certificate> value as well
    x509cert = pki.X509.read_string_from_file(mycert)
    print("<X509Certificate>=\n", split2len(x509cert, 80))

    # Substitute the 3 values (digval, sigval and keyval) in the original file (NB Latin-1 encoded)
    s = read_text_file(basefile, enc="iso-8859-1")
    news = s.replace("%digval%", digval).replace("%sigval%", sigval).replace("%keyval%", keyval)
    print("New XML=\n", news)
    write_text_file(outfile, news, enc='iso-8859-1')
    print("Created new file", outfile)

    # Check digest of final file matches what we expected
    newdig = sha1_from_file_base64(outfile)
    print("SHA1(newfile)=", newdig)
    if (len(okdigest) > 0 and newdig != okdigest ):
        print("ERROR: digest values do not match")


def make_signed_xml(outfile, basefile, prikey, password, cert, enc='utf8'):
    """
    Compute and replace %digval%, %sigval% and %keyval% in XML-DSIG document
    :param outfile: New file to be created
    :param basefile: Base file containing %digval%, %sigval% and %keyval% to be repleaced
    :param prikey: Encrypted private key (either filename or PEM-style string)
    :param password: Password for encrypted private key
    :param cert: Matching X509 certificate (not checked)
    :param enc: Encoding for input file (must match).
    :return: N/A
    """

    # Compute digest value of base XML excluding Signature element
    digval = C14n.file2digest(basefile, "Signature", Tran.OMITBYTAG)

    # Read in incomplete SignedInfo element that needs the DigestValue filled in
    s = C14n.file2string(basefile, "SignedInfo", Tran.SUBSETBYTAG)

    # Insert the digest value in the SignedInfo string
    siginfo = s.replace("%digval%", digval)

    # Compute the signature value over the completed SignedInfo element
    sigval = pki.Sig.sign_data(siginfo.encode(), prikey, password, pki.Sig.Alg.RSA_SHA1)

    # Get RSA Key Value in XML form
    keyval = pki.Rsa.to_xmlstring(pki.Rsa.read_public_key(cert))

    # Substitute these 3 values in the original file
    s = read_text_file(basefile, enc)
    news = s.replace("%digval%", digval).replace("%sigval%", sigval).replace("%keyval%", keyval)
    write_text_file(outfile, news, enc)


def test_olamundo():
    # File with Latin-1-encoded character (ISO-8859-1)
    # Long-winded way with checks and debugging
    make_signed_file_latin1("olamundo-new.xml", "olamundo-base.xml", "IyaPyNs1fMIR59UfcRQbHY4AZLE=")

    # Quicker way...
    basefile = "olamundo-base.xml"
    newfile = "olamundo-new1.xml"
    make_signed_xml(newfile, basefile, myprikey, mypassword, mycert, enc='iso-8859-1')
    print("SHA1(" + newfile + ")=" + sha1_from_file_base64(newfile))


def test_daiwei():
    # File with UTF-8-encoded chinese characters
    basefile = "daiwei-base.xml"
    newfile = "diawei-new.xml"
    make_signed_xml(newfile, basefile, myprikey, mypassword, mycert)

    # Compare to reference document
    # Note there can be some differences between valid signed files
    # e.g. whitespace in the Signature element not in SignedInfo
    print("SHA1(" + newfile + ")=" + C14n.file2digest(newfile))
    okfile = "daiwei.xml"
    print("SHA1(" + okfile + ")=" + C14n.file2digest(okfile))
    _print_file(newfile)


def main():
    test_olamundo()
    test_daiwei()

    print("ALL DONE.")


if __name__ == "__main__":
    main()