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