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

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

python/www/html/sessionMgr.wsdl,
python/ndg.security.server/ndg/security/server/SessionMgr/SessionMgr_services_server.py,
python/ndg.security.common/ndg/security/common/SessionMgr/SessionMgr_services.py,
python/www/html/attAuthority.wsdl,
python/ndg.security.server/ndg/security/server/AttAuthority/AttAuthority_services_server.py,
python/ndg.security.common/ndg/security/common/AttAuthority/AttAuthority_services.py:
Fixes for WebSphere? WSDL check - added targetNamespace attribute to xsd:schema element,
added 'wsdl' prefix to operation, part and message elements.

  • 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                # Ignore exception for now to allow for problems with
671                # WebSphere client setting X.509 cert
672                #
673                # P J Kershaw 05/02/07
674                pass                 
675#                raise VerifyError, "Error extracting BinarySecurityToken " + \
676#                                   "from WSSE header: " + str(e)
677
678        if self.__verifyingCert is None:
679            raise VerifyError, "No certificate set for verification " + \
680                "of the signature"
681       
682        # Extract RSA public key from the cert
683        rsaPubKey = self.__verifyingCert.m2CryptoX509.get_pubkey().get_rsa()
684       
685        # Apply the signature verification
686        try:
687            verify = bool(rsaPubKey.verify(signedInfoDigestValue, 
688                                           signatureValue))
689        except RSA.RSAError, e:
690            raise VerifyError, "Error in Signature: " + str(e)
691       
692        if not verify:
693            raise InvalidSignature, "Invalid signature"
694       
695        #print "Signature OK"
696
697
698class EncryptionError(Exception):
699    """Flags an error in the encryption process"""
700
701class DecryptionError(Exception):
702    """Raised from EncryptionHandler.decrypt if an error occurs with the
703    decryption process"""
704
705
706class EncryptionHandler(object):
707    """Encrypt/Decrypt SOAP messages using WS-Security""" 
708   
709    # Map namespace URIs to Crypto algorithm module and mode
710    cryptoAlg = \
711    {
712         _ENCRYPTION.WRAP_AES256:      {'module':       AES, 
713                                        'mode':         AES.MODE_ECB,
714                                        'blockSize':    16},
715         
716         # CBC (Cipher Block Chaining) modes
717         _ENCRYPTION.BLOCK_AES256:     {'module':       AES, 
718                                        'mode':         AES.MODE_CBC,
719                                        'blockSize':    16},
720                                       
721         _ENCRYPTION.BLOCK_TRIPLEDES:  {'module':       DES3, 
722                                        'mode':         DES3.MODE_CBC,
723                                        'blockSize':    8}   
724    }
725
726     
727    def __init__(self,
728                 signingCertFilePath=None, 
729                 signingPriKeyFilePath=None, 
730                 signingPriKeyPwd=None,
731                 chkSecurityTokRef=False,
732                 encrNS=_ENCRYPTION.BLOCK_AES256):
733       
734        self.__signingCertFilePath = signingCertFilePath
735        self.__signingPriKeyFilePath = signingPriKeyFilePath
736        self.__signingPriKeyPwd = signingPriKeyPwd
737       
738        self.__chkSecurityTokRef = chkSecurityTokRef
739       
740        # Algorithm for shared key encryption
741        try:
742            self.__encrAlg = self.cryptoAlg[encrNS]
743           
744        except KeyError:
745            raise EncryptionError, \
746        'Input encryption algorithm namespace "%s" is not supported' % encrNS
747
748        self.__encrNS = encrNS
749       
750       
751    def encrypt(self, soapWriter):
752        """Encrypt an outbound SOAP message
753       
754        Use Key Wrapping - message is encrypted using a shared key which
755        itself is encrypted with the public key provided by the X.509 cert.
756        signingCertFilePath"""
757       
758        # Use X.509 Cert to encrypt
759        x509Cert = X509.load_cert(self.__signingCertFilePath)
760       
761        soapWriter.dom.setNamespaceAttribute('wsse', OASIS.WSSE)
762        soapWriter.dom.setNamespaceAttribute('xenc', _ENCRYPTION.BASE)
763        soapWriter.dom.setNamespaceAttribute('ds', DSIG.BASE)
764       
765        # TODO: Put in a check to make sure <wsse:security> isn't already
766        # present in header
767        wsseElem = soapWriter._header.createAppendElement(OASIS.WSSE, 
768                                                         'Security')
769        wsseElem.node.setAttribute('SOAP-ENV:mustUnderstand', "1")
770       
771        encrKeyElem = wsseElem.createAppendElement(_ENCRYPTION.BASE, 
772                                                   'EncryptedKey')
773       
774        # Encryption method used to encrypt the shared key
775        keyEncrMethodElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE, 
776                                                        'EncryptionMethod')
777       
778        keyEncrMethodElem.node.setAttribute('Algorithm', 
779                                            _ENCRYPTION.KT_RSA_1_5)
780
781
782        # Key Info
783        KeyInfoElem = encrKeyElem.createAppendElement(DSIG.BASE, 'KeyInfo')
784       
785        secTokRefElem = KeyInfoElem.createAppendElement(OASIS.WSSE, 
786                                                  'SecurityTokenReference')
787       
788        x509IssSerialElem = secTokRefElem.createAppendElement(DSIG.BASE, 
789                                                          'X509IssuerSerial')
790
791       
792        x509IssNameElem = x509IssSerialElem.createAppendElement(DSIG.BASE, 
793                                                          'X509IssuerName')
794        x509IssNameElem.createAppendTextNode(x509Cert.get_issuer().as_text())
795
796       
797        x509IssSerialNumElem = x509IssSerialElem.createAppendElement(
798                                                  DSIG.BASE, 
799                                                  'X509IssuerSerialNumber')
800       
801        x509IssSerialNumElem.createAppendTextNode(
802                                          str(x509Cert.get_serial_number()))
803
804        # References to what has been encrypted
805        encrKeyCiphDataElem = encrKeyElem.createAppendElement(
806                                                          _ENCRYPTION.BASE,
807                                                          'CipherData')
808       
809        encrKeyCiphValElem = encrKeyCiphDataElem.createAppendElement(
810                                                          _ENCRYPTION.BASE,
811                                                          'CipherValue')
812
813        # References to what has been encrypted
814        refListElem = encrKeyElem.createAppendElement(_ENCRYPTION.BASE,
815                                                      'ReferenceList')
816       
817        dataRefElem = refListElem.createAppendElement(_ENCRYPTION.BASE,
818                                                      'DataReference')
819        dataRefElem.node.setAttribute('URI', "#encrypted")
820
821                     
822        # Add Encrypted data to SOAP body
823        encrDataElem = soapWriter.body.createAppendElement(_ENCRYPTION.BASE, 
824                                                           'EncryptedData')
825        encrDataElem.node.setAttribute('Id', 'encrypted')
826        encrDataElem.node.setAttribute('Type', _ENCRYPTION.BASE) 
827             
828        # Encryption method used to encrypt the target data
829        dataEncrMethodElem = encrDataElem.createAppendElement(
830                                                      _ENCRYPTION.BASE, 
831                                                      'EncryptionMethod')
832       
833        dataEncrMethodElem.node.setAttribute('Algorithm', self.__encrNS)
834       
835        # Cipher data
836        ciphDataElem = encrDataElem.createAppendElement(_ENCRYPTION.BASE,
837                                                        'CipherData')
838       
839        ciphValueElem = ciphDataElem.createAppendElement(_ENCRYPTION.BASE,
840                                                         'CipherValue')
841
842
843        # Get elements from SOAP body for encryption
844        dataElem = soapWriter.body.node.childNodes[0]
845        data = dataElem.toxml()
846     
847        # Pad data to nearest multiple of encryption algorithm's block size   
848        modData = len(data) % self.__encrAlg['blockSize']
849        nPad = modData and self.__encrAlg['blockSize'] - modData or 0
850       
851        # PAd with random junk but ...
852        data += os.urandom(nPad-1)
853       
854        # Last byte should be number of padding bytes
855        # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
856        data += chr(nPad)       
857       
858        # Generate shared key and input vector - for testing use hard-coded
859        # values to allow later comparison             
860        sharedKey = os.urandom(self.__encrAlg['blockSize'])
861        iv = os.urandom(self.__encrAlg['blockSize'])
862       
863        alg = self.__encrAlg['module'].new(sharedKey,
864                                           self.__encrAlg['mode'],
865                                           iv)
866 
867        # Encrypt required elements - prepend input vector
868        encryptedData = alg.encrypt(iv + data)
869        dataCiphValue = base64.encodestring(encryptedData).strip()
870
871        ciphValueElem.createAppendTextNode(dataCiphValue)
872       
873       
874        # ! Delete unencrypted message body elements !
875        soapWriter.body.node.removeChild(dataElem)
876
877       
878        # Use X.509 cert public key to encrypt the shared key - Extract key
879        # from the cert
880        rsaPubKey = x509Cert.get_pubkey().get_rsa()
881       
882        # Encrypt the shared key
883        encryptedSharedKey = rsaPubKey.public_encrypt(sharedKey, 
884                                                      RSA.pkcs1_padding)
885       
886        encrKeyCiphVal = base64.encodestring(encryptedSharedKey).strip()
887       
888        # Add the encrypted shared key to the EncryptedKey section in the SOAP
889        # header
890        encrKeyCiphValElem.createAppendTextNode(encrKeyCiphVal)
891
892#        print soapWriter.dom.node.toprettyxml()
893#        import pdb;pdb.set_trace()
894       
895       
896    def decrypt(self, parsedSOAP):
897        """Decrypt an inbound SOAP message"""
898       
899        processorNss = \
900        {
901            'xenc':   _ENCRYPTION.BASE,
902            'ds':     DSIG.BASE, 
903            'wsu':    _WSU.UTILITY, 
904            'wsse':   OASIS.WSSE, 
905            'soapenv':"http://schemas.xmlsoap.org/soap/envelope/" 
906        }
907        ctxt = Context(parsedSOAP.dom, processorNss=processorNss)
908       
909        refListNodes = xpath.Evaluate('//xenc:ReferenceList', 
910                                      contextNode=parsedSOAP.dom, 
911                                      context=ctxt)
912        if len(refListNodes) > 1:
913            raise DecryptionError, 'Expecting a single ReferenceList element'
914       
915        try:
916            refListNode = refListNodes[0]
917        except:
918            # Message wasn't encrypted - is this OK or is a check needed for
919            # encryption info in SOAP body - enveloped form?
920            return
921
922
923        # Check for wrapped key encryption
924        encrKeyNodes = xpath.Evaluate('//xenc:EncryptedKey', 
925                                      contextNode=parsedSOAP.dom, 
926                                      context=ctxt)
927        if len(encrKeyNodes) > 1:
928            raise DecryptionError, 'This implementation can only handle ' + \
929                                   'single EncryptedKey element'
930       
931        try:
932            encrKeyNode = encrKeyNodes[0]
933        except:
934            # Shared key encryption used - leave out for the moment
935            raise DecryptionError, 'This implementation can only handle ' + \
936                                   'wrapped key encryption'
937
938       
939        # Check encryption method
940        keyEncrMethodNode = getElements(encrKeyNode, 'EncryptionMethod')[0]     
941        keyAlgorithm = keyEncrMethodNode.getAttributeNodeNS(None, 
942                                                            "Algorithm").value
943        if keyAlgorithm != _ENCRYPTION.KT_RSA_1_5:
944            raise DecryptionError, \
945            'Encryption algorithm for wrapped key is "%s", expecting "%s"' % \
946                (keyAlgorithm, _ENCRYPTION.KT_RSA_1_5)
947
948                                                           
949        if self.__chkSecurityTokRef and self.__signingCertFilePath:
950             
951            # Check input cert. against SecurityTokenReference
952            securityTokRefXPath = '/ds:KeyInfo/wsse:SecurityTokenReference'
953            securityTokRefNode = xpath.Evaluate(securityTokRefXPath, 
954                                                contextNode=encrKeyNode, 
955                                                context=ctxt)
956            # TODO: Look for ds:X509* elements to check against X.509 cert
957            # input
958
959
960        # Look for cipher data for wrapped key
961        keyCiphDataNode = getElements(encrKeyNode, 'CipherData')[0]
962        keyCiphValNode = getElements(keyCiphDataNode, 'CipherValue')[0]
963
964        keyCiphVal = str(keyCiphValNode.childNodes[0].nodeValue)
965        encryptedKey = base64.decodestring(keyCiphVal)
966
967        # Read RSA Private key in order to decrypt wrapped key 
968        priKeyFile = BIO.File(open(self.__signingPriKeyFilePath))         
969        pwdCallback = lambda *ar, **kw: self.__signingPriKeyPwd                                       
970        priKey = RSA.load_key_bio(priKeyFile, callback=pwdCallback)
971       
972        sharedKey = priKey.private_decrypt(encryptedKey, RSA.pkcs1_padding)
973       
974
975        # Check list of data elements that have been encrypted
976        for dataRefNode in refListNode.childNodes:
977
978            # Get the URI for the reference
979            dataRefURI = dataRefNode.getAttributeNodeNS(None, 'URI').value                           
980            if dataRefURI[0] != "#":
981                raise VerifyError, \
982                    "Expecting # identifier for DataReference URI \"%s\"" % \
983                    dataRefURI
984
985            # XPath reference - need to check for wsu namespace qualified?
986            #encrNodeXPath = '//*[@wsu:Id="%s"]' % dataRefURI[1:]
987            encrNodeXPath = '//*[@Id="%s"]' % dataRefURI[1:]
988            encrNode = xpath.Evaluate(encrNodeXPath, 
989                                      contextNode=parsedSOAP.dom, 
990                                      context=ctxt)[0]
991               
992            dataEncrMethodNode = getElements(encrNode, 'EncryptionMethod')[0]     
993            dataAlgorithm = dataEncrMethodNode.getAttributeNodeNS(None, 
994                                                            "Algorithm").value
995            try:       
996                # Match algorithm name to Crypto module
997                CryptoAlg = self.cryptoAlg[dataAlgorithm]
998               
999            except KeyError:
1000                raise DecryptionError, \
1001'Encryption algorithm for data is "%s", supported algorithms are:\n "%s"' % \
1002                    (keyAlgorithm, "\n".join(self.cryptoAlg.keys()))
1003
1004            # Get Data
1005            dataCiphDataNode = getElements(encrNode, 'CipherData')[0]
1006            dataCiphValNode = getElements(dataCiphDataNode, 'CipherValue')[0]
1007       
1008            dataCiphVal = str(dataCiphValNode.childNodes[0].nodeValue)
1009            encryptedData = base64.decodestring(dataCiphVal)
1010           
1011            alg = CryptoAlg['module'].new(sharedKey, CryptoAlg['mode'])
1012            decryptedData = alg.decrypt(encryptedData)
1013           
1014            # Strip prefix - assume is block size
1015            decryptedData = decryptedData[CryptoAlg['blockSize']:]
1016           
1017            # Strip any padding suffix - Last byte should be number of padding
1018            # bytes
1019            # (http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block)
1020            lastChar = decryptedData[-1]
1021            nPad = ord(lastChar)
1022           
1023            # Sanity check - there may be no padding at all - the last byte
1024            # being the end of the encrypted XML?
1025            #
1026            # TODO: are there better sanity checks than this?!
1027            if nPad < CryptoAlg['blockSize'] and nPad > 0 and \
1028               lastChar != '\n' and lastChar != '>':
1029               
1030                # Follow http://www.w3.org/TR/xmlenc-core/#sec-Alg-Block -
1031                # last byte gives number of padding bytes
1032                decryptedData = decryptedData[:-nPad]
1033
1034
1035            # Parse the encrypted data - inherit from Reader as a fudge to
1036            # enable relevant namespaces to be added prior to parse
1037            processorNss.update({'xsi': SCHEMA.XSI3, 'ns1': 'urn:ZSI:examples'})
1038            class _Reader(Reader):
1039                def initState(self, ownerDoc=None):
1040                    Reader.initState(self, ownerDoc=ownerDoc)
1041                    self._namespaces.update(processorNss)
1042                   
1043            rdr = _Reader()
1044            dataNode = rdr.fromString(decryptedData, ownerDoc=parsedSOAP.dom)
1045           
1046            # Add decrypted element to parent and remove encrypted one
1047            parentNode = encrNode._get_parentNode()
1048            parentNode.appendChild(dataNode)
1049            parentNode.removeChild(encrNode)
1050           
1051            from xml.dom.ext import ReleaseNode
1052            ReleaseNode(encrNode)
1053           
1054            # Ensure body_root attribute is up to date in case it was
1055            # previously encrypted
1056            parsedSOAP.body_root = parsedSOAP.body.childNodes[0]
1057            #print decryptedData
1058            #import pdb;pdb.set_trace()
1059
1060
1061#_____________________________________________________________________________
1062from zope.interface import classProvides, implements, Interface
1063import twisted.web.http
1064from twisted.python import log, failure
1065
1066from ZSI.twisted.WSresource import DefaultHandlerChain, \
1067    DefaultCallbackHandler, CallbackChainInterface, HandlerChainInterface, \
1068    DataHandler
1069   
1070from ZSI import _get_element_nsuri_name, EvaluateException, ParseException
1071   
1072   
1073class WSSecurityHandlerChainFactory:
1074    protocol = DefaultHandlerChain
1075   
1076    @classmethod
1077    def newInstance(cls):
1078        return cls.protocol(DefaultCallbackHandler, 
1079                            DataHandler,
1080                            WSSecurityHandler)
1081   
1082
1083class WSSecurityHandler:
1084    classProvides(HandlerChainInterface)
1085
1086    signatureHandler = None
1087   
1088    @classmethod
1089    def processRequest(cls, ps, **kw):
1090        """invokes callback that should return a (request,response) tuple.
1091        representing the SOAP request and response respectively.
1092        ps -- ParsedSoap instance representing HTTP Body.
1093        request -- twisted.web.server.Request
1094        """
1095        if cls.signatureHandler:
1096            cls.signatureHandler.verify(ps)
1097           
1098        return ps
1099   
1100    @classmethod
1101    def processResponse(cls, sw, **kw):
1102       
1103        if cls.signatureHandler:
1104            cls.signatureHandler.sign(sw)
1105           
1106        return sw
Note: See TracBrowser for help on using the repository browser.