source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/wsSecurity.py @ 2092

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/wsSecurity.py@2092
Revision 2092, 45.3 KB checked in by pjkersha, 12 years ago (diff)

python/ndg.security.test/ndg/security/test/SessionMgr/SessionMgrClientTest.py:
uncommented disconnect call arguments

python/ndg.security.common/ndg/security/common/wsSecurity.py:
Added 'ec' into prefix list for exclusive canonicalization of the SignedInfo? section. This
fixes bug with signature for test3ProxyCertConnect test. TODO: fix test4CookieDisconnect
'block type is not 01' verify error.

  • Property svn:executable set to *
Line 
1#!/bin/env python
2
3"""WS-Security test class includes digital signature handler
4
5NERC Data Grid Project
6
7P J Kershaw 01/09/06
8
9Copyright (C) 2006 CCLRC & NERC
10
11This software may be distributed under the terms of the Q Public License,
12version 1.0 or later.
13"""
14
15reposID = '$Id$'
16
17import re
18
19# Digest and signature/verify
20from sha import sha
21from M2Crypto import X509, BIO, RSA
22import base64
23
24# For shared key encryption
25from Crypto.Cipher import AES, DES3
26import os
27
28import ZSI
29from ZSI.wstools.Namespaces import DSIG, ENCRYPTION, OASIS, WSU, WSA200403, \
30                                   SOAP, SCHEMA # last included for xsi
31                                   
32from ZSI.TC import ElementDeclaration,TypeDefinition
33from ZSI.generate.pyclass import pyclass_type
34
35from ZSI.wstools.Utility import DOMException, SplitQName
36from ZSI.wstools.Utility import NamespaceError, MessageInterface, ElementProxy
37
38# XML Parsing
39from cStringIO import StringIO
40from Ft.Xml.Domlette import NonvalidatingReaderBase, NonvalidatingReader
41from Ft.Xml import XPath
42
43# Canonicalization
44from ZSI.wstools.c14n import Canonicalize
45from xml.dom import Node
46from xml.xpath.Context import Context
47from xml import xpath
48
49# Include for re-parsing doc ready for canonicalization in sign method - see
50# associated note
51from xml.dom.ext.reader.PyExpat import Reader
52
53
54from ndg.security.common.X509 import X509Cert, X509CertParse, X509CertRead
55
56
57class _ENCRYPTION(ENCRYPTION):
58    '''Derived from ENCRYPTION class to add in extra 'tripledes-cbc' - is this
59    any different to 'des-cbc'?  ENCRYPTION class implies that it is the same
60    because it's assigned to 'BLOCK_3DES' ??'''
61    BLOCK_TRIPLEDES = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc"
62
63class _WSU(WSU):
64    '''Try different utility namespace for use with WebSphere'''
65    #UTILITY = "http://schemas.xmlsoap.org/ws/2003/06/utility"
66    UTILITY = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
67   
68def getElements(node, nameList):
69    '''DOM Helper function for getting child elements from a given node'''
70    # Avoid sub-string matches
71    nameList = isinstance(nameList, basestring) and [nameList] or nameList
72    return [n for n in node.childNodes if str(n.localName) in nameList]
73
74
75class VerifyError(Exception):
76    """Raised from SignatureHandler.verify if an error occurs in the signature
77    verification"""
78   
79class InvalidSignature(Exception):
80    """Raised from verify method for an invalid signature"""
81
82class SignatureError(Exception):
83    """Flag if an error occurs during signature generation"""
84       
85class SignatureHandler(object):
86    """class to handle signature and verification of signature with
87    WS-Security
88   
89    @type __beginCert: string
90    @param __beginCert: delimiter for beginning of base64 encoded portion of
91    a PEM encoded X.509 certificate
92    @type __endCert: string
93    @cvar: __endCert: equivalent end delimiter
94   
95    @type __x509CertPat: regular expression pattern object
96    @cvar __x509CertPat: regular expression for extracting the base64 encoded
97    portion of a PEM encoded X.509 certificate"""
98   
99    __beginCert = '-----BEGIN CERTIFICATE-----\n'
100    __endCert = '\n-----END CERTIFICATE-----'
101    __x509CertPat = re.compile(__beginCert + \
102                               '?(.*?)\n?-----END CERTIFICATE-----',
103                               re.S)
104   
105   
106    #_________________________________________________________________________
107    def __init__(self,
108                 verifyingCert=None,
109                 verifyingCertFilePath=None,
110                 signingCert=None,
111                 signingCertFilePath=None, 
112                 signingPriKey=None,
113                 signingPriKeyFilePath=None, 
114                 signingPriKeyPwd=None):
115 
116        self.__setVerifyingCert(verifyingCert)
117        self.__setVerifyingCertFilePath(verifyingCertFilePath)
118       
119        self.__setSigningCert(signingCert)
120        self.__setSigningCertFilePath(signingCertFilePath)
121
122        # MUST be set before __setSigningPriKeyFilePath / __setSigningPriKey
123        # are called
124        self.__setSigningPriKeyPwd(signingPriKeyPwd)
125       
126        if signingPriKey is not None:
127            # Don't allow None for private key setting
128            self.__setSigningPriKey(signingPriKey)
129           
130        self.__setSigningPriKeyFilePath(signingPriKeyFilePath)
131       
132
133    #_________________________________________________________________________
134    def __setCert(self, cert):
135        """filter and convert input cert to signing verifying cert set
136        property methods.  For signingCert, set to None if it is not to be
137        included in the SOAP header.  For verifyingCert, set to None if this
138        cert can be expected to be retrieved from the SOAP header of the
139        message to be verified
140       
141        @type: ndg.security.common.X509.X509Cert / M2Crypto.X509.X509 /
142        string or None
143        @param cert: X.509 certificate. 
144       
145        @rtype ndg.security.common.X509.X509Cert
146        @return X.509 certificate object"""
147       
148        if cert is None or isinstance(cert, X509Cert):
149            # ndg.security.common.X509.X509Cert type / None
150            return cert
151           
152        elif isinstance(cert, X509.X509):
153            # M2Crypto.X509.X509 type
154            return X509Cert(m2CryptoX509=cert)
155           
156        elif isinstance(cert, basestring):
157            return X509CertParse(cert)
158       
159        else:
160            raise AttributeError, "X.509 Cert. must be type: " + \
161                "ndg.security.common.X509.X509Cert, M2Crypto.X509.X509 or " +\
162                "a base64 encoded string"
163
164   
165    #_________________________________________________________________________
166    def __getVerifyingCert(self):
167        '''Return X.509 cert object corresponding to cert used to verify the
168        signature in the last call to verify
169       
170         * Cert will correspond to one used in the LATEST call to verify, on
171         the next call it will be replaced
172         * if verify hasn't been called, the cert will be None
173       
174        @rtype: M2Crypto.X509.X509
175        @return: certificate object
176        '''
177        return self.__verifyingCert
178
179
180    #_________________________________________________________________________
181    def __setVerifyingCert(self, verifyingCert):
182        "Set property method for X.509 cert. used to verify a signature"
183        self.__verifyingCert = self.__setCert(verifyingCert)
184   
185        # Reset file path as it may no longer apply
186        self.__verifyingCertFilePath = None
187       
188    verifyingCert = property(fset=__setVerifyingCert,
189                             fget=__getVerifyingCert,
190                             doc="Set X.509 Cert. for verifying signature")
191
192
193    #_________________________________________________________________________
194    def __setVerifyingCertFilePath(self, verifyingCertFilePath):
195        "Set method for Service X.509 cert. file path property"
196       
197        if isinstance(verifyingCertFilePath, basestring):
198            self.__verifyingCert = X509CertRead(verifyingCertFilePath)
199           
200        elif verifyingCertFilePath is not None:
201            raise AttributeError, \
202            "Verifying X.509 Cert. file path must be None or a valid string"
203       
204        self.__verifyingCertFilePath = verifyingCertFilePath
205       
206    verifyingCertFilePath = property(fset=__setVerifyingCertFilePath,
207                    doc="file path of X.509 Cert. for verifying signature")
208
209
210    #_________________________________________________________________________
211    def __setSigningCert(self, signingCert):
212        "Set property method for X.509 cert. to be included with signature"
213        self.__signingCert = self.__setCert(signingCert)
214   
215        # Reset file path as it may no longer apply
216        self.__signingCertFilePath = None
217       
218    signingCert = property(fset=__setSigningCert,
219                             doc="Set X.509 Cert. to include signature")
220
221 
222    #_________________________________________________________________________
223    def __setSigningCertFilePath(self, signingCertFilePath):
224        "Set signature X.509 cert property method"
225       
226        if isinstance(signingCertFilePath, basestring):
227            self.__signingCert = X509CertRead(signingCertFilePath)
228           
229        elif signingCertFilePath is not None:
230            raise AttributeError, \
231                "Signature X.509 cert. file path must be a valid string"
232       
233        self.__signingCertFilePath = signingCertFilePath
234       
235       
236    signingCertFilePath = property(fset=__setSigningCertFilePath,
237                   doc="File path X.509 cert. to include with signed message")
238
239 
240    #_________________________________________________________________________
241    def __setSigningPriKeyPwd(self, signingPriKeyPwd):
242        "Set method for private key file password used to sign message"
243        if signingPriKeyPwd is not None and \
244           not isinstance(signingPriKeyPwd, basestring):
245            raise AttributeError, \
246                "Signing private key password must be None or a valid string"
247       
248        self.__signingPriKeyPwd = signingPriKeyPwd
249       
250    signingPriKeyPwd = property(fset=__setSigningPriKeyPwd,
251             doc="Password protecting private key file used to sign message")
252
253 
254    #_________________________________________________________________________
255    def __setSigningPriKey(self, signingPriKey):
256        """Set method for client private key
257       
258        Nb. if input is a string, signingPriKeyPwd will need to be set if
259        the key is password protected.
260       
261        @type signingPriKey: M2Crypto.RSA.RSA / string
262        @param signingPriKey: private key used to sign message"""
263       
264        if isinstance(signingPriKey, basestring):
265            pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd
266            self.__signingPriKey = RSA.load_key_string(signingPriKey,
267                                                       callback=pwdCallback)
268
269        elif isinstance(signingPriKey, RSA.RSA):
270            self.__signingPriKey = signingPriKey
271                   
272        else:
273            raise AttributeError, "Signing private key must be a valid " + \
274                                  "M2Crypto.RSA.RSA type or a string"
275               
276    signingPriKey = property(fset=__setSigningPriKey,
277                             doc="Private key used to sign outbound message")
278
279 
280    #_________________________________________________________________________
281    def __setSigningPriKeyFilePath(self, signingPriKeyFilePath):
282        """Set method for client private key file path
283       
284        signingPriKeyPwd MUST be set prior to a call to this method"""
285        if isinstance(signingPriKeyFilePath, basestring):                           
286            try:
287                # Read Private key to sign with   
288                priKeyFile = BIO.File(open(signingPriKeyFilePath)) 
289                pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                           
290                self.__signingPriKey = RSA.load_key_bio(priKeyFile, 
291                                                        callback=pwdCallback)           
292            except Exception, e:
293                raise AttributeError, \
294                                "Setting private key for signature: %s" % e
295       
296        elif signingPriKeyFilePath is not None:
297            raise AttributeError, \
298                        "Private key file path must be a valid string or None"
299       
300        self.__signingPriKeyFilePath = signingPriKeyFilePath
301       
302    signingPriKeyFilePath = property(fset=__setSigningPriKeyFilePath,
303                      doc="File path for private key used to sign message")
304
305
306    #_________________________________________________________________________
307    def sign(self, soapWriter):
308        '''Sign the message body and binary security token of a SOAP message
309        '''
310       
311        # Add X.509 cert as binary security token
312        x509CertStr = self.__class__.__x509CertPat.findall(\
313                                           self.__signingCert.toString())[0]
314
315        soapWriter._header.setNamespaceAttribute('wsse', OASIS.WSSE)
316        soapWriter._header.setNamespaceAttribute('wsu', _WSU.UTILITY)
317        soapWriter._header.setNamespaceAttribute('ds', DSIG.BASE)
318        soapWriter._header.setNamespaceAttribute('ec', DSIG.C14N_EXCL)
319       
320        # TODO: Put in a check to make sure <wsse:security> isn't already
321        # present in header
322        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
323                                                         'Security')
324        wsseElem.setNamespaceAttribute('wsse', OASIS.WSSE)
325        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
326       
327        binSecTokElem = wsseElem.createAppendElement(OASIS.WSSE, 
328                                                     'BinarySecurityToken')
329       
330        # Change value and encoding types to suite WebSphere
331#        binSecTokElem.node.setAttribute('ValueType', "wsse:X509v3")
332        valueType = \
333"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509"
334        binSecTokElem.node.setAttribute('ValueType', valueType)
335#        binSecTokElem.node.setAttribute('EncodingType', "wsse:Base64Binary")
336        encodingType = \
337"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
338        binSecTokElem.node.setAttribute('EncodingType', encodingType)
339       
340        # Add ID so that the binary token can be included in the signature
341        binSecTokElem.node.setAttribute('wsu:Id', "binaryToken")
342
343        binSecTokElem.createAppendTextNode(x509CertStr)
344
345       
346        # Signature
347        signatureElem = wsseElem.createAppendElement(DSIG.BASE, 'Signature')
348        signatureElem.setNamespaceAttribute('ds', DSIG.BASE)
349       
350        # Signature - Signed Info
351        signedInfoElem = signatureElem.createAppendElement(DSIG.BASE, 
352                                                           'SignedInfo')
353       
354        # Signed Info - Canonicalization method
355        #
356        # Added 'ec' to list P J Kershaw 01/02/07
357        signedInfoC14nKw = {}
358        signedInfoC14nKw['unsuppressedPrefixes'] = \
359            ['xsi', 'xsd', 'SOAP-ENV', 'ds', 'wsse', 'ec']
360        c14nMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
361                                                    'CanonicalizationMethod')
362        c14nMethodElem.node.setAttribute('Algorithm', DSIG.C14N_EXCL)
363        c14nInclNamespacesElem = c14nMethodElem.createAppendElement(\
364                                                    DSIG.C14N_EXCL,
365                                                    'InclusiveNamespaces')
366        c14nInclNamespacesElem.node.setAttribute('PrefixList', 
367                        ' '.join(signedInfoC14nKw['unsuppressedPrefixes']))
368       
369        # Signed Info - Signature method
370        sigMethodElem = signedInfoElem.createAppendElement(DSIG.BASE,
371                                                    'SignatureMethod')
372        #sigMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
373        sigMethodElem.node.setAttribute('Algorithm', DSIG.SIG_RSA_SHA1)
374       
375        # Signature - Signature value
376        signatureValueElem = signatureElem.createAppendElement(DSIG.BASE, 
377                                                             'SignatureValue')
378       
379        # Key Info
380        KeyInfoElem = signatureElem.createAppendElement(DSIG.BASE, 'KeyInfo')
381        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
382                                                  'SecurityTokenReference')
383       
384        # Reference back to the binary token included earlier
385        wsseRefElem = secTokRefElem.createAppendElement(OASIS.WSSE, 
386                                                        'Reference')
387        wsseRefElem.node.setAttribute('URI', "#binaryToken")
388       
389        # Add Reference to body so that it can be included in the signature
390        soapWriter.body.node.setAttribute('wsu:Id', "body")
391        soapWriter.body.node.setAttribute('xmlns:wsu', _WSU.UTILITY)
392
393        # Serialize and re-parse prior to reference generation - calculating
394        # canonicalization based on soapWriter.dom.node seems to give an
395        # error: the order of wsu:Id attribute is not correct
396        docNode = Reader().fromString(str(soapWriter))
397       
398        # Namespaces for XPath searches
399        processorNss = \
400        {
401            'ds':     DSIG.BASE, 
402            'wsu':    _WSU.UTILITY, 
403            'wsse':   OASIS.WSSE, 
404            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
405        }
406        ctxt = Context(docNode, processorNss=processorNss)
407        idNodes = xpath.Evaluate('//*[@wsu:Id]', 
408                                 contextNode=docNode, 
409                                 context=ctxt)
410       
411        # Leave out token
412        #idNodes = [idNodes[1]]
413
414        # 1) Reference Generation
415        #
416        # Find references
417        c14nKw = {}
418        c14nKw['unsuppressedPrefixes'] = ['xmlns', 'xsi', 'xsd', 'SOAP-ENV', 'wsu', 'wsse', 'ns1']
419        for idNode in idNodes:
420           
421            # Set URI attribute to point to reference to be signed
422            #uri = u"#" + idNode.getAttribute('wsu:Id')
423            uri = u"#" + idNode.attributes[(_WSU.UTILITY, 'Id')].value
424           
425            # Canonicalize reference
426            c14nRef = Canonicalize(idNode, **c14nKw)
427           
428            # Calculate digest for reference and base 64 encode
429            #
430            # Nb. encodestring adds a trailing newline char
431            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
432
433
434            # Add a new reference element to SignedInfo
435            refElem = signedInfoElem.createAppendElement(DSIG.BASE, 
436                                                         'Reference')
437            refElem.node.setAttribute('URI', uri)
438           
439            # Use ds:Transforms or wsse:TransformationParameters?
440            transformsElem = refElem.createAppendElement(DSIG.BASE, 
441                                                        'Transforms')
442            transformElem = transformsElem.createAppendElement(DSIG.BASE, 
443                                                               'Transform')
444#            transformElem.node.setAttribute('Algorithm', DSIG.C14N)
445            transformElem.node.setAttribute('Algorithm', DSIG.C14N_EXCL)
446
447            inclNamespacesElem = transformElem.createAppendElement(\
448                                                        DSIG.C14N_EXCL,
449                                                       'InclusiveNamespaces')
450            inclNamespacesElem.node.setAttribute('PrefixList',
451                                ' '.join(c14nKw['unsuppressedPrefixes']))
452           
453            # Digest Method
454            digestMethodElem = refElem.createAppendElement(DSIG.BASE, 
455                                                           'DigestMethod')
456            digestMethodElem.node.setAttribute('Algorithm', DSIG.DIGEST_SHA1)
457           
458            # Digest Value
459            digestValueElem = refElem.createAppendElement(DSIG.BASE, 
460                                                          'DigestValue')
461            digestValueElem.createAppendTextNode(digestValue)
462
463   
464        # 2) Signature Generation
465        #
466
467        # Test against signature generated by pyXMLSec version
468        #xmlTxt = open('./wsseSign-xmlsec-res.xml').read()
469        #dom = NonvalidatingReader.parseStream(StringIO(xmlTxt))
470       
471        # Canonicalize the signedInfo node
472        #
473        # Nb. When extracted the code adds the namespace attribute to the
474        # signedInfo!  This has important consequences for validation -
475        #
476        # 1) Do you strip the namespace attribute before taking the digest to
477        # ensure the text is exactly the same as what is displayed in the
478        # message?
479        #
480        # 2) Leave it in and assume the validation algorithm will expect to
481        # add in the namespace attribute?!
482        #
483        # http://www.w3.org/TR/xml-c14n#NoNSPrefixRewriting implies you need
484        # to include namespace declarations for namespaces referenced in a doc
485        # subset - yes to 2)
486        #c14nSignedInfo = signedInfoElem.canonicalize()
487        c14nSignedInfo = Canonicalize(signedInfoElem.node, **signedInfoC14nKw)
488
489        # Calculate digest of SignedInfo
490        signedInfoDigestValue = sha(c14nSignedInfo).digest().strip()
491       
492        # Sign using the private key and base 64 encode the result
493        signatureValue = self.__signingPriKey.sign(signedInfoDigestValue)
494        b64EncSignatureValue = base64.encodestring(signatureValue).strip()
495
496        # Add to <SignatureValue>
497        signatureValueElem.createAppendTextNode(b64EncSignatureValue)
498       
499#        # Extract RSA public key from the cert
500#        rsaPubKey = self.__signingCert.m2CryptoX509.get_pubkey().get_rsa()
501       
502        # Check the signature
503#        verify = bool(rsaPubKey.verify(signedInfoDigestValue, signatureValue))
504#       
505#        open('soap.xml', 'w').write(str(soapWriter))
506#        import pdb;pdb.set_trace()
507#        print "Signature Generated"
508#        print str(soapWriter)
509
510
511    def verify(self, parsedSOAP):
512        """Verify signature"""
513
514        processorNss = \
515        {
516            'ds':     DSIG.BASE, 
517            'wsu':    _WSU.UTILITY, 
518            'wsse':   OASIS.WSSE, 
519            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
520        }
521        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
522       
523
524        signatureNodes = xpath.Evaluate('//ds:Signature', 
525                                        contextNode=parsedSOAP.dom, 
526                                        context=ctxt)
527        if len(signatureNodes) > 1:
528            raise VerifyError, 'Multiple ds:Signature elements found'
529       
530        try:
531            signatureNodes = signatureNodes[0]
532        except:
533            # Message wasn't signed
534            return
535       
536        # Two stage process: reference validation followed by signature
537        # validation
538       
539        # 1) Reference Validation
540       
541        # Check for canonicalization set via ds:CanonicalizationMethod -
542        # Use this later as a back up in case no Canonicalization was set in
543        # the transforms elements
544        c14nMethodNode = xpath.Evaluate('//ds:CanonicalizationMethod', 
545                                        contextNode=parsedSOAP.dom, 
546                                        context=ctxt)[0]
547       
548        refNodes = xpath.Evaluate('//ds:Reference', 
549                                  contextNode=parsedSOAP.dom, 
550                                  context=ctxt)
551           
552        for refNode in refNodes:
553            # Get the URI for the reference
554            refURI = refNode.getAttributeNodeNS(None, 'URI').value
555                           
556            try:
557                transformsNode = getElements(refNode, "Transforms")[0]
558                transforms = getElements(transformsNode, "Transform")
559   
560                refAlgorithm = transforms[0].getAttributeNodeNS(None, 
561                                                         "Algorithm").value
562            except Exception, e:
563                raise VerifyError, \
564            'failed to get transform algorithm for <ds:Reference URI="%s">'%\
565                        (refURI, str(e))
566               
567            # Add extra keyword for Exclusive canonicalization method
568            c14nKw = {}
569            if refAlgorithm == DSIG.C14N_EXCL:
570                try:
571                    inclusiveNS = getElements(transforms[0], 
572                                              "InclusiveNamespaces")
573                   
574                    pfxListAttNode = inclusiveNS[0].getAttributeNodeNS(None, 
575                                                               'PrefixList')
576                    c14nKw['unsuppressedPrefixes'] = \
577                                                pfxListAttNode.value.split()
578                except:
579                    raise VerifyError, \
580                'failed to handle transform (%s) in <ds:Reference URI="%s">'%\
581                        (transforms[0], refURI)
582       
583            # Canonicalize the reference data and calculate the digest
584            if refURI[0] != "#":
585                raise VerifyError, \
586                    "Expecting # identifier for Reference URI \"%s\"" % refURI
587                   
588            # XPath reference
589            uriXPath = '//*[@wsu:Id="%s"]' % refURI[1:]
590            uriNode = xpath.Evaluate(uriXPath, 
591                                     contextNode=parsedSOAP.dom, 
592                                     context=ctxt)[0]
593
594            c14nRef = Canonicalize(uriNode, **c14nKw)
595            digestValue = base64.encodestring(sha(c14nRef).digest()).strip()
596           
597            # Extract the digest value that was stored           
598            digestNode = getElements(refNode, "DigestValue")[0]
599            nodeDigestValue = str(digestNode.childNodes[0].nodeValue).strip()   
600           
601            # Reference validates if the two digest values are the same
602            if digestValue != nodeDigestValue:
603                raise InvalidSignature, \
604                        'Digest Values do not match for URI: "%s"' % refURI
605               
606        # 2) Signature Validation
607        signedInfoNode = xpath.Evaluate('//ds:SignedInfo',
608                                        contextNode=parsedSOAP.dom, 
609                                        context=ctxt)[0]
610
611        #import pdb;pdb.set_trace()
612        # Get algorithm used for canonicalization of the SignedInfo
613        # element.  Nb. This is NOT necessarily the same as that used to
614        # canonicalize the reference elements checked above!
615        signedInfoC14nAlg = c14nMethodNode.getAttributeNodeNS(None, 
616                                                         "Algorithm").value
617        signedInfoC14nKw = {}
618        if signedInfoC14nAlg == DSIG.C14N_EXCL:
619            try:
620                inclusiveNS = getElements(c14nMethodNode,
621                                          "InclusiveNamespaces")
622               
623                pfxListAttNode = inclusiveNS[0].getAttributeNodeNS(None, 
624                                                           'PrefixList')
625                signedInfoC14nKw['unsuppressedPrefixes'] = \
626                                            pfxListAttNode.value.split()
627            except Exception, e:
628                raise VerifyError, \
629            'failed to handle exclusive canonicalisation for SignedInfo: %s'%\
630                        str(e)
631
632        # Canonicalize the SignedInfo node and take digest
633        c14nSignedInfo = Canonicalize(signedInfoNode, **signedInfoC14nKw)       
634        signedInfoDigestValue = sha(c14nSignedInfo).digest()
635       
636        # Get the signature value in order to check against the digest just
637        # calculated
638        signatureValueNode = xpath.Evaluate('//ds:SignatureValue',
639                                            contextNode=parsedSOAP.dom, 
640                                            context=ctxt)[0]
641
642        # Remove base 64 encoding
643        b64EncSignatureValue = \
644                    str(signatureValueNode.childNodes[0].nodeValue).strip()
645                   
646        signatureValue = base64.decodestring(b64EncSignatureValue)
647
648
649        # Look for X.509 Cert in wsse:BinarySecurityToken node
650        try:
651            binSecTokNode = xpath.Evaluate('//wsse:BinarySecurityToken',
652                                           contextNode=parsedSOAP.dom,
653                                           context=ctxt)[0]
654        except:
655            # Signature may not have included the Binary Security Token in
656            # which case the verifying cert will need to have been set
657            # elsewhere
658            binSecTokNode = None
659            pass 
660       
661       
662        if binSecTokNode:
663            try:
664                b64EncX509Cert = self.__class__.__beginCert + \
665                         str(binSecTokNode.childNodes[0]._get_nodeValue()) + \
666                         self.__class__.__endCert
667                             
668                self.__setVerifyingCert(b64EncX509Cert)
669            except Exception, e:
670                raise VerifyError, "Error extracting BinarySecurityToken " + \
671                                   "from WSSE header: " + str(e)
672       
673        if self.__verifyingCert is None:
674            raise VerifyError, "No certificate set for verification " + \
675                "of the signature"
676       
677        import pdb;pdb.set_trace()
678        # Extract RSA public key from the cert
679        rsaPubKey = self.__verifyingCert.m2CryptoX509.get_pubkey().get_rsa()
680       
681        # Apply the signature verification
682        try:
683            verify = bool(rsaPubKey.verify(signedInfoDigestValue, 
684                                           signatureValue))
685        except RSA.RSAError, e:
686            raise VerifyError, "Error in Signature: " + str(e)
687       
688        if not verify:
689            raise InvalidSignature, "Invalid signature"
690       
691        #print "Signature OK"
692
693
694class EncryptionError(Exception):
695    """Flags an error in the encryption process"""
696
697class DecryptionError(Exception):
698    """Raised from EncryptionHandler.decrypt if an error occurs with the
699    decryption process"""
700
701
702class EncryptionHandler(object):
703    """Encrypt/Decrypt SOAP messages using WS-Security""" 
704   
705    # Map namespace URIs to Crypto algorithm module and mode
706    cryptoAlg = \
707    {
708         _ENCRYPTION.WRAP_AES256:      {'module':       AES, 
709                                        'mode':         AES.MODE_ECB,
710                                        'blockSize':    16},
711         
712         # CBC (Cipher Block Chaining) modes
713         _ENCRYPTION.BLOCK_AES256:     {'module':       AES, 
714                                        'mode':         AES.MODE_CBC,
715                                        'blockSize':    16},
716                                       
717         _ENCRYPTION.BLOCK_TRIPLEDES:  {'module':       DES3, 
718                                        'mode':         DES3.MODE_CBC,
719                                        'blockSize':    8}   
720    }
721
722     
723    def __init__(self,
724                 signingCertFilePath=None, 
725                 signingPriKeyFilePath=None, 
726                 signingPriKeyPwd=None,
727                 chkSecurityTokRef=False,
728                 encrNS=_ENCRYPTION.BLOCK_AES256):
729       
730        self.__signingCertFilePath = signingCertFilePath
731        self.__signingPriKeyFilePath = signingPriKeyFilePath
732        self.__signingPriKeyPwd = signingPriKeyPwd
733       
734        self.__chkSecurityTokRef = chkSecurityTokRef
735       
736        # Algorithm for shared key encryption
737        try:
738            self.__encrAlg = self.cryptoAlg[encrNS]
739           
740        except KeyError:
741            raise EncryptionError, \
742        'Input encryption algorithm namespace "%s" is not supported' % encrNS
743
744        self.__encrNS = encrNS
745       
746       
747    def encrypt(self, soapWriter):
748        """Encrypt an outbound SOAP message
749       
750        Use Key Wrapping - message is encrypted using a shared key which
751        itself is encrypted with the public key provided by the X.509 cert.
752        signingCertFilePath"""
753       
754        # Use X.509 Cert to encrypt
755        x509Cert = X509.load_cert(self.__signingCertFilePath)
756       
757        soapWriter.dom.setNamespaceAttribute('wsse', OASIS.WSSE)
758        soapWriter.dom.setNamespaceAttribute('xenc', _ENCRYPTION.BASE)
759        soapWriter.dom.setNamespaceAttribute('ds', DSIG.BASE)
760       
761        # TODO: Put in a check to make sure <wsse:security> isn't already
762        # present in header
763        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
764                                                         'Security')
765        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
766       
767        encrKeyElem = wsseElem.createAppendElement(_ENCRYPTION.BASE, 
768                                                   'EncryptedKey')
769       
770        # Encryption method used to encrypt the shared key
771        keyEncrMethodElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE, 
772                                                        'EncryptionMethod')
773       
774        keyEncrMethodElem.node.setAttribute('Algorithm', 
775                                            _ENCRYPTION.KT_RSA_1_5)
776
777
778        # Key Info
779        KeyInfoElem = encrKeyElem.createAppendElement(DSIG.BASE, 'KeyInfo')
780       
781        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
782                                                  'SecurityTokenReference')
783       
784        x509IssSerialElem = secTokRefElem.createAppendElement(DSIG.BASE, 
785                                                          'X509IssuerSerial')
786
787       
788        x509IssNameElem = x509IssSerialElem.createAppendElement(DSIG.BASE, 
789                                                          'X509IssuerName')
790        x509IssNameElem.createAppendTextNode(x509Cert.get_issuer().as_text())
791
792       
793        x509IssSerialNumElem = x509IssSerialElem.createAppendElement(
794                                                  DSIG.BASE, 
795                                                  'X509IssuerSerialNumber')
796       
797        x509IssSerialNumElem.createAppendTextNode(
798                                          str(x509Cert.get_serial_number()))
799
800        # References to what has been encrypted
801        encrKeyCiphDataElem = encrKeyElem.createAppendElement(
802                                                          _ENCRYPTION.BASE,
803                                                          'CipherData')
804       
805        encrKeyCiphValElem = encrKeyCiphDataElem.createAppendElement(
806                                                          _ENCRYPTION.BASE,
807                                                          'CipherValue')
808
809        # References to what has been encrypted
810        refListElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE,
811                                                      'ReferenceList')
812       
813        dataRefElem = refListElem.createAppendElement(_ENCRYPTION.BASE,
814                                                      'DataReference')
815        dataRefElem.node.setAttribute('URI', "#encrypted")
816
817                     
818        # Add Encrypted data to SOAP body
819        encrDataElem = soapWriter.body.createAppendElement(_ENCRYPTION.BASE, 
820                                                           'EncryptedData')
821        encrDataElem.node.setAttribute('Id', 'encrypted')
822        encrDataElem.node.setAttribute('Type', _ENCRYPTION.BASE) 
823             
824        # Encryption method used to encrypt the target data
825        dataEncrMethodElem = encrDataElem.createAppendElement(
826                                                      _ENCRYPTION.BASE, 
827                                                      'EncryptionMethod')
828       
829        dataEncrMethodElem.node.setAttribute('Algorithm', self.__encrNS)
830       
831        # Cipher data
832        ciphDataElem = encrDataElem.createAppendElement(_ENCRYPTION.BASE,
833                                                        'CipherData')
834       
835        ciphValueElem = ciphDataElem.createAppendElement(_ENCRYPTION.BASE,
836                                                         'CipherValue')
837
838
839        # Get elements from SOAP body for encryption
840        dataElem = soapWriter.body.node.childNodes[0]
841        data = dataElem.toxml()
842     
843        # Pad data to nearest multiple of encryption algorithm's block size   
844        modData = len(data) % self.__encrAlg['blockSize']
845        nPad = modData and self.__encrAlg['blockSize'] - modData or 0
846       
847        # PAd with random junk but ...
848        data += os.urandom(nPad-1)
849       
850        # Last byte should be number of padding bytes
851        # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
852        data += chr(nPad)       
853       
854        # Generate shared key and input vector - for testing use hard-coded
855        # values to allow later comparison             
856        sharedKey = os.urandom(self.__encrAlg['blockSize'])
857        iv = os.urandom(self.__encrAlg['blockSize'])
858       
859        alg = self.__encrAlg['module'].new(sharedKey,
860                                           self.__encrAlg['mode'],
861                                           iv)
862 
863        # Encrypt required elements - prepend input vector
864        encryptedData = alg.encrypt(iv + data)
865        dataCiphValue = base64.encodestring(encryptedData).strip()
866
867        ciphValueElem.createAppendTextNode(dataCiphValue)
868       
869       
870        # ! Delete unencrypted message body elements !
871        soapWriter.body.node.removeChild(dataElem)
872
873       
874        # Use X.509 cert public key to encrypt the shared key - Extract key
875        # from the cert
876        rsaPubKey = x509Cert.get_pubkey().get_rsa()
877       
878        # Encrypt the shared key
879        encryptedSharedKey = rsaPubKey.public_encrypt(sharedKey, 
880                                                      RSA.pkcs1_padding)
881       
882        encrKeyCiphVal = base64.encodestring(encryptedSharedKey).strip()
883       
884        # Add the encrypted shared key to the EncryptedKey section in the SOAP
885        # header
886        encrKeyCiphValElem.createAppendTextNode(encrKeyCiphVal)
887
888#        print soapWriter.dom.node.toprettyxml()
889#        import pdb;pdb.set_trace()
890       
891       
892    def decrypt(self, parsedSOAP):
893        """Decrypt an inbound SOAP message"""
894       
895        processorNss = \
896        {
897            'xenc':   _ENCRYPTION.BASE,
898            'ds':     DSIG.BASE, 
899            'wsu':    _WSU.UTILITY, 
900            'wsse':   OASIS.WSSE, 
901            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
902        }
903        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
904       
905        refListNodes = xpath.Evaluate('//xenc:ReferenceList', 
906                                      contextNode=parsedSOAP.dom, 
907                                      context=ctxt)
908        if len(refListNodes) > 1:
909            raise DecryptionError, 'Expecting a single ReferenceList element'
910       
911        try:
912            refListNode = refListNodes[0]
913        except:
914            # Message wasn't encrypted - is this OK or is a check needed for
915            # encryption info in SOAP body - enveloped form?
916            return
917
918
919        # Check for wrapped key encryption
920        encrKeyNodes = xpath.Evaluate('//xenc:EncryptedKey', 
921                                      contextNode=parsedSOAP.dom, 
922                                      context=ctxt)
923        if len(encrKeyNodes) > 1:
924            raise DecryptionError, 'This implementation can only handle ' + \
925                                   'single EncryptedKey element'
926       
927        try:
928            encrKeyNode = encrKeyNodes[0]
929        except:
930            # Shared key encryption used - leave out for the moment
931            raise DecryptionError, 'This implementation can only handle ' + \
932                                   'wrapped key encryption'
933
934       
935        # Check encryption method
936        keyEncrMethodNode = getElements(encrKeyNode, 'EncryptionMethod')[0]     
937        keyAlgorithm = keyEncrMethodNode.getAttributeNodeNS(None, 
938                                                            "Algorithm").value
939        if keyAlgorithm != _ENCRYPTION.KT_RSA_1_5:
940            raise DecryptionError, \
941            'Encryption algorithm for wrapped key is "%s", expecting "%s"' % \
942                (keyAlgorithm, _ENCRYPTION.KT_RSA_1_5)
943
944                                                           
945        if self.__chkSecurityTokRef and self.__signingCertFilePath:
946             
947            # Check input cert. against SecurityTokenReference
948            securityTokRefXPath = '/ds:KeyInfo/wsse:SecurityTokenReference'
949            securityTokRefNode = xpath.Evaluate(securityTokRefXPath, 
950                                                contextNode=encrKeyNode, 
951                                                context=ctxt)
952            # TODO: Look for ds:X509* elements to check against X.509 cert
953            # input
954
955
956        # Look for cipher data for wrapped key
957        keyCiphDataNode = getElements(encrKeyNode, 'CipherData')[0]
958        keyCiphValNode = getElements(keyCiphDataNode, 'CipherValue')[0]
959
960        keyCiphVal = str(keyCiphValNode.childNodes[0].nodeValue)
961        encryptedKey = base64.decodestring(keyCiphVal)
962
963        # Read RSA Private key in order to decrypt wrapped key 
964        priKeyFile = BIO.File(open(self.__signingPriKeyFilePath))         
965        pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                       
966        priKey = RSA.load_key_bio(priKeyFile, callback=pwdCallback)
967       
968        sharedKey = priKey.private_decrypt(encryptedKey, RSA.pkcs1_padding)
969       
970
971        # Check list of data elements that have been encrypted
972        for dataRefNode in refListNode.childNodes:
973
974            # Get the URI for the reference
975            dataRefURI = dataRefNode.getAttributeNodeNS(None, 'URI').value                           
976            if dataRefURI[0] != "#":
977                raise VerifyError, \
978                    "Expecting # identifier for DataReference URI \"%s\"" % \
979                    dataRefURI
980
981            # XPath reference - need to check for wsu namespace qualified?
982            #encrNodeXPath = '//*[@wsu:Id="%s"]' % dataRefURI[1:]
983            encrNodeXPath = '//*[@Id="%s"]' % dataRefURI[1:]
984            encrNode = xpath.Evaluate(encrNodeXPath, 
985                                      contextNode=parsedSOAP.dom, 
986                                      context=ctxt)[0]
987               
988            dataEncrMethodNode = getElements(encrNode, 'EncryptionMethod')[0]     
989            dataAlgorithm = dataEncrMethodNode.getAttributeNodeNS(None, 
990                                                            "Algorithm").value
991            try:       
992                # Match algorithm name to Crypto module
993                CryptoAlg = self.cryptoAlg[dataAlgorithm]
994               
995            except KeyError:
996                raise DecryptionError, \
997'Encryption algorithm for data is "%s", supported algorithms are:\n "%s"' % \
998                    (keyAlgorithm, "\n".join(self.cryptoAlg.keys()))
999
1000            # Get Data
1001            dataCiphDataNode = getElements(encrNode, 'CipherData')[0]
1002            dataCiphValNode = getElements(dataCiphDataNode, 'CipherValue')[0]
1003       
1004            dataCiphVal = str(dataCiphValNode.childNodes[0].nodeValue)
1005            encryptedData = base64.decodestring(dataCiphVal)
1006           
1007            alg = CryptoAlg['module'].new(sharedKey, CryptoAlg['mode'])
1008            decryptedData = alg.decrypt(encryptedData)
1009           
1010            # Strip prefix - assume is block size
1011            decryptedData = decryptedData[CryptoAlg['blockSize']:]
1012           
1013            # Strip any padding suffix - Last byte should be number of padding
1014            # bytes
1015            # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
1016            lastChar = decryptedData[-1]
1017            nPad = ord(lastChar)
1018           
1019            # Sanity check - there may be no padding at all - the last byte
1020            # being the end of the encrypted XML?
1021            #
1022            # TODO: are there better sanity checks than this?!
1023            if nPad < CryptoAlg['blockSize'] and nPad > 0 and \
1024               lastChar != '\n' and lastChar != '>':
1025               
1026                # Follow http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block -
1027                # last byte gives number of padding bytes
1028                decryptedData = decryptedData[:-nPad]
1029
1030
1031            # Parse the encrypted data - inherit from Reader as a fudge to
1032            # enable relevant namespaces to be added prior to parse
1033            processorNss.update({'xsi': SCHEMA.XSI3, 'ns1': 'urn:ZSI:examples'})
1034            class _Reader(Reader):
1035                def initState(self, ownerDoc=None):
1036                    Reader.initState(self, ownerDoc=ownerDoc)
1037                    self._namespaces.update(processorNss)
1038                   
1039            rdr = _Reader()
1040            dataNode = rdr.fromString(decryptedData, ownerDoc=parsedSOAP.dom)
1041           
1042            # Add decrypted element to parent and remove encrypted one
1043            parentNode = encrNode._get_parentNode()
1044            parentNode.appendChild(dataNode)
1045            parentNode.removeChild(encrNode)
1046           
1047            from xml.dom.ext import ReleaseNode
1048            ReleaseNode(encrNode)
1049           
1050            # Ensure body_root attribute is up to date in case it was
1051            # previously encrypted
1052            parsedSOAP.body_root = parsedSOAP.body.childNodes[0]
1053            #print decryptedData
1054            #import pdb;pdb.set_trace()
1055
1056
1057#_____________________________________________________________________________
1058from zope.interface import classProvides, implements, Interface
1059import twisted.web.http
1060from twisted.python import log, failure
1061
1062from ZSI.twisted.WSresource import DefaultHandlerChain, \
1063    DefaultCallbackHandler, CallbackChainInterface, HandlerChainInterface, \
1064    DataHandler
1065   
1066from ZSI import _get_element_nsuri_name, EvaluateException, ParseException
1067   
1068   
1069class WSSecurityHandlerChainFactory:
1070    protocol = DefaultHandlerChain
1071   
1072    @classmethod
1073    def newInstance(cls):
1074        return cls.protocol(DefaultCallbackHandler, 
1075                            DataHandler,
1076                            WSSecurityHandler)
1077   
1078
1079class WSSecurityHandler:
1080    classProvides(HandlerChainInterface)
1081
1082    signatureHandler = None
1083   
1084    @classmethod
1085    def processRequest(cls, ps, **kw):
1086        """invokes callback that should return a (request,response) tuple.
1087        representing the SOAP request and response respectively.
1088        ps -- ParsedSoap instance representing HTTP Body.
1089        request -- twisted.web.server.Request
1090        """
1091        if cls.signatureHandler:
1092            cls.signatureHandler.verify(ps)
1093           
1094        return ps
1095   
1096    @classmethod
1097    def processResponse(cls, sw, **kw):
1098       
1099        if cls.signatureHandler:
1100            cls.signatureHandler.sign(sw)
1101           
1102        return sw
Note: See TracBrowser for help on using the repository browser.