source: TI12-security/trunk/python/Tests/xmlsec/WS-Security/wsSecurity.py @ 1461

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/Tests/xmlsec/WS-Security/wsSecurity.py@1461
Revision 1461, 15.4 KB checked in by pjkersha, 15 years ago (diff)

Working version passes SignatureHandler? instance to ZSI.Binding.Send via sig_handler keyword to sign messages
from WS client.

Still to do:

  • Fix SignatureHandler? verify to handle replies from server
  • Server side - fix appropriate place for signature and verify code.
  • Property svn:executable set to *
Line 
1#!/bin/env python
2
3import re
4
5# Digest and signature/verify
6from sha import sha
7from M2Crypto import X509, BIO, RSA
8import base64
9
10import cElementTree as ElementTree
11import ZSI
12from ZSI.wstools.Namespaces import DSIG, OASIS, GLOBUS, WSU, WSA200403, BEA, \
13                                   SOAP
14                                   
15from ZSI.TC import ElementDeclaration,TypeDefinition
16from ZSI.generate.pyclass import pyclass_type
17
18# XML Parsing
19from cStringIO import StringIO
20from Ft.Xml.Domlette import NonvalidatingReaderBase, NonvalidatingReader
21from Ft.Xml import XPath
22
23# Canonicalization
24from ZSI.wstools.c14n import Canonicalize
25from xml.dom import Node
26
27# Type codes for signature elements
28#from ZSI.TC import _get_global_element_declaration as getGlobalElemDecl
29
30#Security = getGlobalElemDecl(OASIS.WSSE, "Security").pyclass
31#KeyInfo = getGlobalElemDecl(DSIG.BASE, "KeyInfo").pyclass
32#Reference = GED(OASIS.WSSE, "Reference").pyclass
33#SecurityReference = GED(OASIS.WSSE, "SecurityTokenReference").pyclass
34#Signature = GED(DSIG.BASE, "Signature").pyclass
35
36
37
38
39class SignatureHandler(object):
40   
41    # Unique token reference
42    tokenRef = 10101
43   
44    def __init__(self,
45                 certFilePath=None, 
46                 priKeyFilePath=None, 
47                 priKeyPwd=None, 
48                 sign_body=True, 
49                 sign_headers=True):
50       
51        self.__certFilePath = certFilePath
52        self.__priKeyFilePath = priKeyFilePath
53        self.__priKeyPwd = priKeyPwd
54       
55        self._can_algo = DSIG.C14N
56        self._sig_algo = GLOBUS.SIG
57        self._dig_algo = DSIG.DIGEST_SHA1
58        self._docCtxtID = None
59        self._expire_time = 300
60        self._setProtocol = False
61        self._sign_headers = sign_headers
62        self._sign_body = sign_body
63
64
65    def sign(self, soapWriter):
66
67        # Use wsse:TransformationParameters with ds:Transforms??
68        msgTmpl = """<?xml version="1.0" encoding="UTF-8"?>
69<!--
70SOAP Message with WSSE Signature
71-->
72<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
73        xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing"
74        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
75        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
76    <soapenv:Header>
77        <wsse:Security
78                xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/04/secext"
79                xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
80                soapenv:mustUnderstand="1">
81            <wsse:BinarySecurityToken
82                wsu:Id="binaryToken"
83                ValueType="wsse:X509v3"
84                EncodingType="wsse:Base64Binary">
85%s
86            </wsse:BinarySecurityToken>
87            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
88                <ds:SignedInfo>
89                    <ds:CanonicalizationMethod
90                    Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
91                    <ds:SignatureMethod
92                    Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
93                </ds:SignedInfo>
94                <ds:SignatureValue>%s</ds:SignatureValue>
95                <ds:KeyInfo>
96                    <wsse:SecurityTokenReference>
97                        <wsse:Reference URI="#binaryToken"/>
98                    </wsse:SecurityTokenReference>
99                </ds:KeyInfo>
100            </ds:Signature>
101        </wsse:Security>
102    </soapenv:Header>
103    <soapenv:Body xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
104            wsu:Id="body">
105    Hello, World!
106    </soapenv:Body>
107</soapenv:Envelope>"""
108
109
110        # Add X.509 cert as binary security token
111        x509Cert = X509.load_cert(self.__certFilePath)
112       
113        x509CertPat = re.compile(\
114            '-----BEGIN CERTIFICATE-----\n?(.*?)\n?-----END CERTIFICATE-----',
115            re.S)
116        x509CertStr = x509CertPat.findall(x509Cert.as_pem())[0]
117
118#        attrTypeCodeDict = \
119#        {
120#            (OASIS.UTILITY, 'Id'):  ZSI.TC.String(),
121#            'ValueType':            ZSI.TC.String(),
122#            'EncodingType':         ZSI.TC.String()
123#        }
124#        binSecurityTokTC = \
125#                getGlobalElemDecl(OASIS.WSSE, "BinarySecurityToken")
126#
127#        binSecurityTokTC.attribute_typecode_dict.update(attrTypeCodeDict)
128#       
129#        securityTC = getGlobalElemDecl(OASIS.WSSE, "Security")
130#        securityPyObj = securityTC.pyclass()
131#        securityPyObj._any = []
132#        securityPyObj._any.append(binSecurityTokTC.pyclass())   
133#       
134        #soapWriter.serialize_header(securityPyObj, securityTC)
135       
136        # Namespaces for XPath searches
137        soapWriter.dom.processorNss = \
138        {
139            'ds':     DSIG.BASE, 
140            'wsu':    WSU.UTILITY, 
141            'wsse':   OASIS.WSSE, 
142            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
143        }
144
145        soapWriter._header.setNamespaceAttribute('wsse', OASIS.WSSE)
146        soapWriter._header.setNamespaceAttribute('wsu', WSU.UTILITY)
147        soapWriter._header.setNamespaceAttribute('ds', DSIG.BASE)
148       
149        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
150                                                         'Security')
151        wsseElem.setNamespaceAttribute('wsse', OASIS.WSSE)
152        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
153       
154        binSecTokElem = wsseElem.createAppendElement(OASIS.WSSE, 
155                                                     'BinarySecurityToken')
156        binSecTokElem.node.setAttribute('ValueType', "wsse:X509v3")
157        binSecTokElem.node.setAttribute('EncodingType', "wsse:Base64Binary")
158        binSecTokElem.createAppendTextNode(x509CertStr)
159       
160        # Add ID
161        binSecTokElem.node.setAttribute('wsu:Id', "binaryToken")
162       
163        signatureElem = wsseElem.createAppendElement(DSIG.BASE, 'Signature')
164        signatureElem.setNamespaceAttribute('ds', DSIG.BASE)
165       
166        signedInfoElem = signatureElem.createAppendElement(DSIG.BASE, 
167                                                           'SignedInfo')
168        signatureValueElem = signatureElem.createAppendElement(DSIG.BASE, 
169                                                             'SignatureValue')
170       
171        KeyInfoElem = signatureElem.createAppendElement(DSIG.BASE, 'KeyInfo')
172        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
173                                                  'SecurityTokenReference')
174       
175        wsseRefElem = secTokRefElem.createAppendElement(OASIS.WSSE, 
176                                                        'Reference')
177        wsseRefElem.node.setAttribute('URI', "#binaryToken")
178       
179        # Add Reference to body
180        soapWriter.body.node.setAttribute('wsu:Id', "body")
181        soapWriter.body.node.setAttribute('xmlns:wsu', WSU.UTILITY)
182       
183        # 1) Reference Generation
184        #
185        # Find references
186        #idNodes = XPath.Compile('//*[@wsu:Id]').evaluate(docCtxt)
187        idElems = [binSecTokElem, soapWriter.body]
188        for idElem in idElems:
189           
190            # Set URI attribute to point to reference to be signed
191            uri = u"#" + idElem.node.getAttribute('wsu:Id')
192           
193            # Canonicalize reference
194            c14nRef = idElem.canonicalize()
195           
196            # Calculate digest for reference and base 64 encode
197            #
198            # Nb. encodestring adds a trailing newline char
199            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
200
201            # Add a new reference element to SignedInfo
202            refElem = signedInfoElem.createAppendElement(DSIG.BASE, 
203                                                         'Reference')
204            refElem.node.setAttribute('URI', uri)
205            refElem.createAppendTextNode(digestValue)
206
207       
208        # 2) Signature Generation
209        #
210
211        # Test against signature generated by pyXMLSec version
212        #xmlTxt = open('./wsseSign-xmlsec-res.xml').read()
213        #dom = NonvalidatingReader.parseStream(StringIO(xmlTxt))
214       
215        # Canonicalize the signedInfo node
216        #
217        # Nb. When extracted the code adds the namespace attribute to the
218        # signedInfo!  This has important consequences for validation -
219        #
220        # 1) Do you strip the namespace attribute before taking the digest to
221        # ensure the text is exactly the same as what is displayed in the
222        # message?
223        #
224        # 2) Leave it in and assume the validation algorithm will expect to
225        # add in the namespace attribute?!
226        #
227        # http://www.w3.org/TR/xml-c14n#NoNSPrefixRewriting implies you need
228        # to include namespace declarations for namespaces referenced in a doc
229        # subset - yes to 2)
230        c14nSignedInfo = signedInfoElem.canonicalize()
231
232        # Calculate digest of SignedInfo
233        signedInfoDigestValue = sha(c14nSignedInfo).digest().strip()
234       
235        # Read Private key to sign with   
236        priKeyFile = BIO.File(open(self.__priKeyFilePath))                                           
237        priKey = RSA.load_key_bio(priKeyFile, 
238                                  callback=lambda *ar, **kw: self.__priKeyPwd)
239       
240        # Sign using the private key and base 64 encode the result
241        signatureValue = priKey.sign(signedInfoDigestValue)
242        b64EncSignatureValue = base64.encodestring(signatureValue).strip()
243
244        # Add to <SignatureValue>
245        signatureValueElem.createAppendTextNode(b64EncSignatureValue)
246       
247        # Extract RSA public key from the cert
248        rsaPubKey = x509Cert.get_pubkey().get_rsa()
249       
250        # Check the signature
251        verify = bool(rsaPubKey.verify(signedInfoDigestValue, signatureValue))
252        print soapWriter.dom.node.toprettyxml()
253       
254        from xml.dom.ext.reader.PyExpat import Reader
255        Reader().fromString(str(soapWriter))
256        import pdb;pdb.set_trace()       
257
258
259    def verify(self, soapWriter):
260        """Verify signature"""
261        import pdb;pdb.set_trace()
262        return
263        # Parse file
264        doc = NonvalidatingReader.parseStream(StringIO(xmlTxt))
265       
266        # Set namespaces for XPath queries
267        processorNss = \
268        {
269            'ds':     DSIG.BASE, 
270            'wsu':    WSU.UTILITY, 
271            'wsse':   OASIS.WSSE, 
272            'wsa':    WSA200403.ADDRESS,
273            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
274        }
275        docCtxt = XPath.Context.Context(doc, processorNss=processorNss)
276       
277        # Two stagfe process: reference validation followed by signature
278        # validation
279       
280        # 1) Reference Validation       
281        refNodes = XPath.Compile('//ds:Reference').evaluate(docCtxt)
282           
283        # Make a lambda to pick out node child or children with a given
284        # name
285        getElements = lambda node, nameList: \
286                [n for n in node.childNodes if str(n.localName) in nameList] 
287       
288        for refNode in refNodes:
289            # Get the URI for the reference
290            refURI = refNode.getAttributeNodeNS(None, 'URI').value
291                           
292            transformsNode = getElements(refNode, "Transforms")[0]
293            transforms = getElements(transformsNode, "Transform")
294   
295            algorithm = transforms[0].getAttributeNodeNS(None, 
296                                                         "Algorithm").value
297           
298            # Add extra keyword for Exclusive canonicalization method
299            kw = {}
300            if algorithm == DSIG.C14N_EXCL:
301                try:
302                    inclusiveNS = transforms[0].getElement(DSIG.C14N_EXCL, 
303                                                       "InclusiveNamespaces")
304                    kw['unsuppressedPrefixes'] = \
305                    inclusiveNS.getAttributeValue(None, "PrefixList").split()
306                except:
307                    raise Exception, \
308                'failed to handle transform (%s) in <ds:Reference URI="%s">'%\
309                        (transforms[0], uri)
310       
311            # Canonicalize the reference data and calculate the digest
312            if refURI[0] != "#":
313                raise Exception, \
314                    "Expecting # identifier for Reference URI \"%s\"" % refURI
315                   
316            # XPath reference
317            uriXPath = '//*[@wsu:Id="%s"]' % refURI[1:]
318            uriNode = XPath.Compile(uriXPath).evaluate(docCtxt)[0]
319
320            c14nRef = Canonicalize(uriNode, **kw)
321            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
322           
323            # Extract the digest value that was stored           
324            digestNode = getElements(refNode, "DigestValue")[0]
325            nodeDigestValue = str(digestNode.childNodes[0].nodeValue).strip()   
326           
327            # Reference validates if the two digest values are the same
328            if digestValue != nodeDigestValue:
329                raise Exception, "DigestValues do not match: URI=%s" % uri
330       
331       
332        # 2) Signature Validation
333        signedInfoNode = XPath.Compile('//ds:SignedInfo').evaluate(docCtxt)[0]
334
335        # Get the canonicalization method - change later to check this and
336        # make sure it's an algorithm supported by this code
337        c14nMethodNode = getElements(signedInfoNode, 
338                                     "CanonicalizationMethod")[0]
339                                             
340        algorithm = c14nMethodNode.getAttributeNodeNS(None, 'Algorithm').value
341        if algorithm != DSIG.C14N:
342            raise Exception, \
343                "Only \"%s\" canonicalization algorithm supported" % DSIG.C14N
344               
345        # Canonicalize the SignedInfo node and take digest
346        c14nSignedInfo = Canonicalize(signedInfoNode)       
347        signedInfoDigestValue = sha(c14nSignedInfo).digest()
348       
349        # Get the signature value in order to check against the digest just
350        # calculated
351        signatureValueNode = \
352                    XPath.Compile('//ds:SignatureValue').evaluate(docCtxt)[0]
353
354        # Remove base 64 encoding
355        b64EncSignatureValue = \
356                    str(signatureValueNode.childNodes[0].nodeValue).strip()
357                   
358        signatureValue = base64.decodestring(b64EncSignatureValue)
359
360
361        # Read X.509 Cert from wsse:BinarySecurityToken node
362        # - leave out for now and read direct from hard coded pem file
363        x509Cert = X509.load_cert(self.__certFilePath)
364       
365        # Extract RSA public key from the cert
366        rsaPubKey = x509Cert.get_pubkey().get_rsa()
367       
368        # Apply the signature verification
369        try:
370            verify = bool(rsaPubKey.verify(signedInfoDigestValue, 
371                                           signatureValue))
372        except RSA.RSAError:
373            verify = False
374           
375        return verify
376
377
378    def getTokenRefString(self):
379        """
380        Return an unique number to identify an element
381        for the element's id attribute
382        """
383        Signature.tokenRef +=1
384        return str(Signature.tokenRef)
385
386   
387     
388if __name__ == "__main__":
389    import sys
390    txt = None
391   
392    s = SignatureHandler(certFilePath='../Junk-cert.pem',
393                         priKeyFilePath='../Junk-key.pem',
394                         priKeyPwd=open('../tmp2').read().strip())
395   
396    if 'sign' in sys.argv:
397        txt = s.sign()     
398        print txt
399
400    if 'verify' in sys.argv:
401        if txt is None:
402            txt = open('./wsseSign-test-res.xml').read()
403           
404        print "Signature OK? %s" % s.verify(txt)
Note: See TracBrowser for help on using the repository browser.