1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published by the
5# Free Software Foundation; either version 3, or (at your option) any later
6# version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
10# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11# for more details.
12
13"""Pythonic XML Security Library implementation"""
14
15import base64
16import hashlib
17import os
18from cStringIO import StringIO
19from M2Crypto import BIO, EVP, RSA, X509, m2
20
21# if lxml is not installed, use c14n.py native implementation
22try:
23    import lxml.etree
24except ImportError:
25    lxml = None
26
27# Features:
28#  * Uses M2Crypto and lxml (libxml2) but it is independent from libxmlsec1
29#  * Sign, Verify, Encrypt & Decrypt XML documents
30
31# Enveloping templates ("by reference": signature is parent):
32SIGN_REF_TMPL = """
33<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
34  <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
35  <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
36  <Reference URI="%(ref_uri)s">
37    <Transforms>
38      <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
39    </Transforms>
40    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
41    <DigestValue>%(digest_value)s</DigestValue>
42  </Reference>
43</SignedInfo>
44"""
45SIGNED_TMPL = """
46<?xml version="1.0" encoding="UTF-8"?>
47<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
48%(signed_info)s
49<SignatureValue>%(signature_value)s</SignatureValue>
50%(key_info)s
51%(ref_xml)s
52</Signature>
53"""
54
55# Enveloped templates (signature is child, the reference is the root object):
56SIGN_ENV_TMPL = """
57<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
58  <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
59  <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
60  <Reference URI="">
61    <Transforms>
62       <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
63       <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
64    </Transforms>
65    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
66    <DigestValue>%(digest_value)s</DigestValue>
67  </Reference>
68</SignedInfo>
69"""
70SIGNATURE_TMPL = """<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
71%(signed_info)s
72<SignatureValue>%(signature_value)s</SignatureValue>
73%(key_info)s
74</Signature>"""
75
76KEY_INFO_RSA_TMPL = """
77<KeyInfo>
78  <KeyValue>
79    <RSAKeyValue>
80      <Modulus>%(modulus)s</Modulus>
81      <Exponent>%(exponent)s</Exponent>
82    </RSAKeyValue>
83  </KeyValue>
84</KeyInfo>
85"""
86
87KEY_INFO_X509_TMPL = """
88<KeyInfo>
89    <X509Data>
90        <X509IssuerSerial>
91            <X509IssuerName>%(issuer_name)s</X509IssuerName>
92            <X509SerialNumber>%(serial_number)s</X509SerialNumber>
93        </X509IssuerSerial>
94    </X509Data>
95</KeyInfo>
96"""
97
98def canonicalize(xml, c14n_exc=True):
99    "Return the canonical (c14n) form of the xml document for hashing"
100    # UTF8, normalization of line feeds/spaces, quoting, attribute ordering...
101    output = StringIO()
102    if lxml is not None:
103        # use faster libxml2 / lxml canonicalization function if available
104        et = lxml.etree.parse(StringIO(xml))
105        et.write_c14n(output, exclusive=c14n_exc)
106    else:
107        # use pure-python implementation: c14n.py (avoid recursive import)
108        from .simplexml import SimpleXMLElement
109        SimpleXMLElement(xml).write_c14n(output, exclusive=c14n_exc)
110    return output.getvalue()
111
112
113def sha1_hash_digest(payload):
114    "Create a SHA1 hash and return the base64 string"
115    return base64.b64encode(hashlib.sha1(payload).digest())
116
117
118def rsa_sign(xml, ref_uri, private_key, password=None, cert=None, c14n_exc=True,
119             sign_template=SIGN_REF_TMPL, key_info_template=KEY_INFO_RSA_TMPL):
120    "Sign an XML document usign RSA (templates: enveloped -ref- or enveloping)"
121
122    # normalize the referenced xml (to compute the SHA1 hash)
123    ref_xml = canonicalize(xml, c14n_exc)
124    # create the signed xml normalized (with the referenced uri and hash value)
125    signed_info = sign_template % {'ref_uri': ref_uri,
126                                   'digest_value': sha1_hash_digest(ref_xml)}
127    signed_info = canonicalize(signed_info, c14n_exc)
128    # Sign the SHA1 digest of the signed xml using RSA cipher
129    pkey = RSA.load_key(private_key, lambda *args, **kwargs: password)
130    signature = pkey.sign(hashlib.sha1(signed_info).digest())
131    # build the mapping (placeholders) to create the final xml signed message
132    return {
133            'ref_xml': ref_xml, 'ref_uri': ref_uri,
134            'signed_info': signed_info,
135            'signature_value': base64.b64encode(signature),
136            'key_info': key_info(pkey, cert, key_info_template),
137            }
138
139
140def rsa_verify(xml, signature, key, c14n_exc=True):
141    "Verify a XML document signature usign RSA-SHA1, return True if valid"
142
143    # load the public key (from buffer or filename)
144    if key.startswith("-----BEGIN PUBLIC KEY-----"):
145        bio = BIO.MemoryBuffer(key)
146        rsa = RSA.load_pub_key_bio(bio)
147    else:
148        rsa = RSA.load_pub_key(certificate)
149    # create the digital envelope
150    pubkey = EVP.PKey()
151    pubkey.assign_rsa(rsa)
152    # do the cryptographic validation (using the default sha1 hash digest)
153    pubkey.reset_context(md='sha1')
154    pubkey.verify_init()
155    # normalize and feed the signed xml to be verified
156    pubkey.verify_update(canonicalize(xml, c14n_exc))
157    ret = pubkey.verify_final(base64.b64decode(signature))
158    return ret == 1
159
160
161def key_info(pkey, cert, key_info_template):
162    "Convert private key (PEM) to XML Signature format (RSAKeyValue/X509Data)"
163    exponent = base64.b64encode(pkey.e[4:])
164    modulus = m2.bn_to_hex(m2.mpi_to_bn(pkey.n)).decode("hex").encode("base64")
165    x509 = x509_parse_cert(cert) if cert else None
166    return key_info_template % {
167        'modulus': modulus,
168        'exponent': exponent,
169        'issuer_name': x509.get_issuer().as_text() if x509 else "",
170        'serial_number': x509.get_serial_number() if x509 else "",
171        }
172
173
174# Miscellaneous certificate utility functions:
175
176
177def x509_parse_cert(cert, binary=False):
178    "Create a X509 certificate from binary DER, plain text PEM or filename"
179    if binary:
180        bio = BIO.MemoryBuffer(cert)
181        x509 = X509.load_cert_bio(bio, X509.FORMAT_DER)
182    elif cert.startswith("-----BEGIN CERTIFICATE-----"):
183        bio = BIO.MemoryBuffer(cert)
184        x509 = X509.load_cert_bio(bio, X509.FORMAT_PEM)
185    else:
186        x509 = X509.load_cert(cert, 1)
187    return x509
188
189
190def x509_extract_rsa_public_key(cert, binary=False):
191    "Return the public key (PEM format) from a X509 certificate"
192    x509 = x509_parse_cert(cert, binary)
193    return x509.get_pubkey().get_rsa().as_pem()
194
195
196def x509_verify(cacert, cert, binary=False):
197    "Validate the certificate's authenticity using a certification authority"
198    ca = x509_parse_cert(cacert)
199    crt = x509_parse_cert(cert, binary)
200    return crt.verify(ca.get_pubkey())
201
202
203if __name__ == "__main__":
204    # basic test of enveloping signature (the reference is a part of the xml)
205    sample_xml = """<Object xmlns="http://www.w3.org/2000/09/xmldsig#" Id="object">data</Object>"""
206    print(canonicalize(sample_xml))
207    vars = rsa_sign(sample_xml, '#object', "no_encriptada.key", "password")
208    print(SIGNED_TMPL % vars)
209
210    # basic test of enveloped signature (the reference is the document itself)
211    sample_xml = """<?xml version="1.0" encoding="UTF-8"?><Object>data%s</Object>"""
212    vars = rsa_sign(sample_xml % "", '', "no_encriptada.key", "password",
213                    sign_template=SIGN_ENV_TMPL, c14n_exc=False)
214    print(sample_xml % (SIGNATURE_TMPL % vars))
215
216    # basic signature verification:
217    public_key = x509_extract_rsa_public_key(open("zunimercado.crt").read())
218    assert rsa_verify(vars['signed_info'], vars['signature_value'], public_key,
219                      c14n_exc=False)
220